mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +00:00
fix(mobile): preserve raw RPC rejection timing
Return the transport promise directly and interpret replies separately so sibling Promise.all rejection order cannot change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -406,7 +406,7 @@ async function loadMobileResumeMetadata(client: Pick<RpcClient, 'sendRequest'>):
|
||||
projectGroupResponse?.ok === true
|
||||
? (projectGroupResponse.result as { groups?: MobileAiVaultResumeProjectGroup[] })
|
||||
: null
|
||||
const settingsResult = settingsResponse?.interpret()
|
||||
const settingsResult = settingsResponse ? optionalSettingsRead.interpret(settingsResponse) : null
|
||||
const settings = settingsResult?.accepted
|
||||
? (settingsResult.value as MobileAiVaultResumeSettings | null | undefined)
|
||||
: null
|
||||
|
||||
@@ -89,7 +89,7 @@ export function useNewWorkspaceCreateSubmit(args: {
|
||||
let latestRuntimeSettings = args.runtimeSettings
|
||||
try {
|
||||
const settingsReply = await settingsRead.request(client)
|
||||
const settings = settingsReply.interpret()
|
||||
const settings = settingsRead.interpret(settingsReply)
|
||||
if (settings.accepted) {
|
||||
latestRuntimeSettings = settings.value as NewWorktreeRuntimeSettings
|
||||
args.setRuntimeSettings(latestRuntimeSettings)
|
||||
|
||||
@@ -82,7 +82,7 @@ export function fetchMobileHomeTaskProviders(
|
||||
if (disposed()) {
|
||||
return
|
||||
}
|
||||
const settingsResult = settingsResponse.interpret()
|
||||
const settingsResult = settingsRead.interpret(settingsResponse)
|
||||
const settings = settingsResult.accepted
|
||||
? ((settingsResult.value ?? {}) as HomeTaskSettings)
|
||||
: {}
|
||||
|
||||
@@ -133,7 +133,9 @@ export function useHostRepoMetadata(args: {
|
||||
if (clientRef.current !== requestClient || hostId !== requestHostId) {
|
||||
return
|
||||
}
|
||||
const hostSettingsResult = hostSettings?.interpret()
|
||||
const hostSettingsResult = hostSettings
|
||||
? optionalSettingsRead.interpret(hostSettings)
|
||||
: null
|
||||
setHostLabelById(
|
||||
buildHostLabelById({
|
||||
sshTargets: readSshTargets(sshTargets?.ok ? sshTargets.result : null),
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function loadMobileNewTabAgentOptions(args: {
|
||||
newTabSettingsRead.request(client),
|
||||
detectedAgentsRequest
|
||||
])
|
||||
const readSettings = settingsResponse.interpret()
|
||||
const readSettings = newTabSettingsRead.interpret(settingsResponse)
|
||||
if (!detectedResponse.ok) {
|
||||
throw new Error((detectedResponse as RpcFailure).error.message)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function usePRBotAuthorOverrides(
|
||||
if (stale) {
|
||||
return
|
||||
}
|
||||
const overrides = response.interpret()
|
||||
const overrides = botOverridesRead.interpret(response)
|
||||
if (overrides.accepted) {
|
||||
setLogins(overrides.value)
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export function useMobileTasksRuntimeHydration(model: ClientSettingsActionsModel
|
||||
return
|
||||
}
|
||||
|
||||
const settingsResult = settingsResponse.interpret()
|
||||
const settingsResult = settingsRead.interpret(settingsResponse)
|
||||
const settings = settingsResult.accepted
|
||||
? ((settingsResult.value ?? {}) as RuntimeTaskSettings)
|
||||
: {}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod
|
||||
let latestRuntimeTaskSettings = runtimeTaskSettings
|
||||
try {
|
||||
const settingsReply = await settingsRead.request(client)
|
||||
const settingsResult = settingsReply.interpret()
|
||||
const settingsResult = settingsRead.interpret(settingsReply)
|
||||
if (settingsResult.accepted) {
|
||||
latestRuntimeTaskSettings = (settingsResult.value ?? {}) as RuntimeTaskSettings
|
||||
setRuntimeTaskSettings(latestRuntimeTaskSettings)
|
||||
|
||||
@@ -178,6 +178,3 @@ export type LegacyResultRpcDefinition<
|
||||
acceptance: Acceptance
|
||||
read: RpcCompatibleReader<unknown, Variant, Value>
|
||||
}
|
||||
|
||||
/** Only the bound policy can turn this opaque reply into a value, after caller guards. */
|
||||
export type DeferredRpcInterpretation<Value> = { readonly interpret: () => Value }
|
||||
|
||||
@@ -25,8 +25,7 @@ import type {
|
||||
RequireResultRpcDefinition,
|
||||
StreamOpenerRpcDefinition,
|
||||
RpcVerdict,
|
||||
LegacyResultRpcDefinition,
|
||||
DeferredRpcInterpretation
|
||||
LegacyResultRpcDefinition
|
||||
} from './rpc-operation-contract'
|
||||
|
||||
type RpcOperationDefinitionInput =
|
||||
@@ -253,7 +252,7 @@ type RpcSendArguments<Method extends RpcMethodName> =
|
||||
? [params?: RpcSendParams<Method>, options?: SendRequestOptions]
|
||||
: [params: RpcSendParams<Method>, options?: SendRequestOptions]
|
||||
|
||||
/** Binds sending and interpretation without exposing envelopes or capturing transport failures. */
|
||||
/** Binds sending and interpretation while preserving the transport promise identity. */
|
||||
export function bindDeferredRpcOperation<
|
||||
Method extends RpcMethodName,
|
||||
Acceptance extends RpcAcceptanceName,
|
||||
@@ -261,14 +260,10 @@ export function bindDeferredRpcOperation<
|
||||
Value
|
||||
>(operation: RpcOperation<Method, Acceptance, Variant, Value, 'after-caller-barrier'>) {
|
||||
type Verdict = RpcVerdict<Acceptance, Value>
|
||||
const defer = (response: RpcResponse): DeferredRpcInterpretation<Verdict> => ({
|
||||
interpret: () =>
|
||||
interpretRpcOutcome(operation, classifyRpcReply(operation, response)) as Verdict
|
||||
})
|
||||
return Object.freeze({
|
||||
operation,
|
||||
request(client: UnvalidatedRpcRequestPort, ...args: RpcSendArguments<Method>) {
|
||||
return client.sendRequest(operation.method, ...args).then(defer)
|
||||
return client.sendRequest(operation.method, ...args)
|
||||
},
|
||||
requestSingleFlight(
|
||||
client: RpcClient,
|
||||
@@ -277,7 +272,10 @@ export function bindDeferredRpcOperation<
|
||||
? [params?: RpcSendParams<Method>]
|
||||
: [params: RpcSendParams<Method>]
|
||||
) {
|
||||
return sendSingleFlightRequest(client, hostId, operation.method, args[0]).then(defer)
|
||||
return sendSingleFlightRequest(client, hostId, operation.method, args[0])
|
||||
},
|
||||
interpret(response: RpcResponse): Verdict {
|
||||
return interpretRpcOutcome(operation, classifyRpcReply(operation, response)) as Verdict
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,14 +51,14 @@ describe('settings historical acceptance', () => {
|
||||
it('distinguishes a skipped refusal from an accepted absent settings member', async () => {
|
||||
const skipped = await settingsRead.request(replyWith(refusal()))
|
||||
const missing = await settingsRead.request(replyWith(success({})))
|
||||
expect(skipped.interpret()).toEqual({ accepted: false })
|
||||
expect(missing.interpret()).toEqual({ accepted: true, value: undefined })
|
||||
expect(settingsRead.interpret(skipped)).toEqual({ accepted: false })
|
||||
expect(settingsRead.interpret(missing)).toEqual({ accepted: true, value: undefined })
|
||||
})
|
||||
|
||||
it('retains opaque settings fields and reference identity without tightening acceptance', async () => {
|
||||
const value = { futureField: { nested: ['kept'] }, disabledTuiAgents: 'legacy-value' }
|
||||
const reply = await settingsRead.request(replyWith(success({ settings: value })))
|
||||
const result = reply.interpret()
|
||||
const result = settingsRead.interpret(reply)
|
||||
expect(result.accepted && result.value).toBe(value)
|
||||
})
|
||||
|
||||
@@ -66,29 +66,29 @@ describe('settings historical acceptance', () => {
|
||||
'preserves the unguarded settings read for %s only when interpreted',
|
||||
async (value) => {
|
||||
const reply = await settingsRead.request(replyWith(success(value)))
|
||||
expect(() => reply.interpret()).toThrow(TypeError)
|
||||
expect(() => reply.interpret()).toThrow(
|
||||
expect(() => settingsRead.interpret(reply)).toThrow(TypeError)
|
||||
expect(() => settingsRead.interpret(reply)).toThrow(
|
||||
`Cannot read properties of ${String(value)} (reading 'settings')`
|
||||
)
|
||||
const optional = await optionalSettingsRead.request(replyWith(success(value)))
|
||||
expect(optional.interpret()).toEqual({ accepted: true, value: undefined })
|
||||
expect(optionalSettingsRead.interpret(optional)).toEqual({ accepted: true, value: undefined })
|
||||
}
|
||||
)
|
||||
|
||||
it.each([true, false, 0, 'text', []])('preserves property boxing for %j', async (value) => {
|
||||
const reply = await settingsRead.request(replyWith(success(value)))
|
||||
expect(reply.interpret()).toEqual({ accepted: true, value: undefined })
|
||||
expect(settingsRead.interpret(reply)).toEqual({ accepted: true, value: undefined })
|
||||
})
|
||||
|
||||
it('filters bot logins while distinguishing a refused refresh', async () => {
|
||||
const reply = await botOverridesRead.request(
|
||||
replyWith(success({ settings: { prBotAuthorOverrides: ['bot', 3, null, ''] } }))
|
||||
)
|
||||
expect(reply.interpret()).toEqual({ accepted: true, value: ['bot', ''] })
|
||||
expect(botOverridesRead.interpret(reply)).toEqual({ accepted: true, value: ['bot', ''] })
|
||||
const refused = await botOverridesRead.request(replyWith(refusal()))
|
||||
expect(refused.interpret()).toEqual({ accepted: false })
|
||||
expect(botOverridesRead.interpret(refused)).toEqual({ accepted: false })
|
||||
const empty = await botOverridesRead.request(replyWith(success(null)))
|
||||
expect(empty.interpret()).toEqual({ accepted: true, value: [] })
|
||||
expect(botOverridesRead.interpret(empty)).toEqual({ accepted: true, value: [] })
|
||||
})
|
||||
|
||||
it('does not read a stale payload until its caller permits interpretation', async () => {
|
||||
@@ -103,7 +103,7 @@ describe('settings historical acceptance', () => {
|
||||
)
|
||||
)
|
||||
expect(read).not.toHaveBeenCalled()
|
||||
reply.interpret()
|
||||
settingsRead.interpret(reply)
|
||||
expect(read).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('new-tab settlement barriers', () => {
|
||||
)
|
||||
await expect(load(client)).rejects.toThrow('agents refused')
|
||||
const reply = await newTabSettingsRead.request(replyWith(success(null)))
|
||||
const readSettings = reply.interpret()
|
||||
const readSettings = newTabSettingsRead.interpret(reply)
|
||||
expect(() => readSettings()).toThrow(TypeError)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user