fix(mobile): let runRpcOperation send a params-less method

A3 declares `mobileWeb.bundle.manifest` with `params: null`, so the generated
catalog types its send params as `void` and the two call sites that pass an
explicit `null` stopped compiling.

`bindDeferredRpcOperation.request` already solved this: `RpcSendArguments`
admits `null` exactly where the catalog declares no params, because
`params: null` is not the frame that omits the key and narrowing it would
rewrite bytes shipped senders already put on the wire. `runRpcOperation` was
the one send entry point that never adopted the tuple, having had no
params-less caller until now. The compile fence pins all three accepted
shapes and that a params-bearing object is still refused.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-18 01:12:51 -04:00
parent a48a446589
commit 5be50beb41
3 changed files with 25 additions and 3 deletions
@@ -9,6 +9,7 @@ import {
} from './rpc-operation'
import { rpcResultVariants } from './rpc-operation-result-reader'
import {
pushTestWithoutParams,
workspaceListAtBarrier,
workspaceListOrNull,
workspaceRowsReader,
@@ -146,6 +147,17 @@ export async function fenceBarrierAndParams(): Promise<void> {
)
}
// A method the catalog declares params-less keeps every shape a shipped sender may use. An
// explicit `null` is the one that matters: `params: null` is not the frame that omits the key,
// so narrowing this to omission would rewrite bytes main already puts on the wire.
export async function fenceParamlessSend(): Promise<void> {
await runRpcOperation(client, pushTestWithoutParams, null)
await runRpcOperation(client, pushTestWithoutParams, undefined)
await runRpcOperation(client, pushTestWithoutParams)
// @ts-expect-error a method that declares no params accepts none
await runRpcOperation(client, pushTestWithoutParams, { path: 'main.js' })
}
export async function fenceVerdictTypes(): Promise<void> {
// @ts-expect-error the probe's policy yields a boolean, not the other family's rows
const rows: WorkspaceRows = await runRpcOperation(client, worktreePsProbe, {})
@@ -54,6 +54,14 @@ export const worktreePsProbe = defineRpcOperation({
barrier: 'on-settle'
})
/** A method the catalog declares with no params at all, so `RpcSendParams` reads `void`. */
export const pushTestWithoutParams = defineRpcOperation({
name: 'test.pushTestWithoutParams',
method: 'notifications.testPush',
acceptance: 'method-not-found-refusal',
barrier: 'on-settle'
})
export const terminalStreamOpener = defineRpcOperation({
name: 'test.terminalStreamOpener',
method: 'terminal.subscribe',
+5 -3
View File
@@ -173,10 +173,12 @@ export async function runRpcOperation<
>(
client: UnvalidatedRpcRequestPort,
operation: RpcOperation<Method, Acceptance, Variant, Value, 'on-settle'>,
params: RpcSendParams<Method>,
options?: SendRequestOptions
// Shares the deferred sender's tuple so the two cannot disagree about what a params-less
// method may be called with: the catalog types those `void`, and an explicit `null` is the
// frame several shipped senders already put on the wire.
...args: RpcSendArguments<Method>
): Promise<RpcVerdict<Acceptance, Value>> {
const outcome = await request(client, operation, params, options)
const outcome = await request(client, operation, args[0], args[1])
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary.
return interpretRpcOutcome(operation, outcome) as RpcVerdict<Acceptance, Value>
}