|
@@ -8,8 +8,10 @@ import type {
|
|
|
} from '@deepseek-ai/dsh-client-connection/client'
|
|
} from '@deepseek-ai/dsh-client-connection/client'
|
|
|
import type {
|
|
import type {
|
|
|
TypertClientEventListener,
|
|
TypertClientEventListener,
|
|
|
|
|
+ TypertOwnedValue,
|
|
|
TypertRemoteEvent,
|
|
TypertRemoteEvent,
|
|
|
} from '@deepseek-ai/dsh-typert-protocol'
|
|
} from '@deepseek-ai/dsh-typert-protocol'
|
|
|
|
|
+import { isTypertOwnedValue } from '@deepseek-ai/dsh-typert-protocol'
|
|
|
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
|
|
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
|
|
|
import {
|
|
import {
|
|
|
REMOTE_EVENT_RESULT_ENDPOINT,
|
|
REMOTE_EVENT_RESULT_ENDPOINT,
|
|
@@ -188,36 +190,42 @@ export class ClientRemoteEvents {
|
|
|
signal: AbortSignal,
|
|
signal: AbortSignal,
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
const adapter = this.ownerCtx.typert.contexts.getClient('agent')
|
|
const adapter = this.ownerCtx.typert.contexts.getClient('agent')
|
|
|
- let target: Context | undefined
|
|
|
|
|
|
|
+ let resolved: Context | TypertOwnedValue<Context> | undefined
|
|
|
try {
|
|
try {
|
|
|
- target = adapter?.resolve(frame.agentId)
|
|
|
|
|
|
|
+ resolved = adapter?.resolve(frame.agentId)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
this.reportError(frame.event, error)
|
|
this.reportError(frame.event, error)
|
|
|
}
|
|
}
|
|
|
- let outcome: RemoteEventReplyOutcome = { kind: 'next' }
|
|
|
|
|
- if (target !== undefined) {
|
|
|
|
|
- try {
|
|
|
|
|
- outcome = await this.dispatchWaterfall(target, frame, signal)
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- if (signal.aborted) return
|
|
|
|
|
- outcome = { kind: 'rejected', error: projectRemoteEventRejection(error) }
|
|
|
|
|
|
|
+ const owned = isTypertOwnedValue(resolved) ? resolved : undefined
|
|
|
|
|
+ try {
|
|
|
|
|
+ const target = isTypertOwnedValue(resolved) ? resolved.value : resolved
|
|
|
|
|
+ let outcome: RemoteEventReplyOutcome = { kind: 'next' }
|
|
|
|
|
+ if (target !== undefined) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ outcome = await this.dispatchWaterfall(target, frame, signal)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (signal.aborted) return
|
|
|
|
|
+ outcome = { kind: 'rejected', error: projectRemoteEventRejection(error) }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ if (signal.aborted) return
|
|
|
|
|
+ const result: RemoteEventResult = {
|
|
|
|
|
+ clientId,
|
|
|
|
|
+ eventId: frame.eventId,
|
|
|
|
|
+ outcome: outcome.kind === 'result' && outcome.value === undefined
|
|
|
|
|
+ ? { kind: 'result' }
|
|
|
|
|
+ : outcome,
|
|
|
|
|
+ }
|
|
|
|
|
+ const response = await this.connection.rpc.call(
|
|
|
|
|
+ '/api',
|
|
|
|
|
+ REMOTE_EVENT_RESULT_ENDPOINT,
|
|
|
|
|
+ { args: result },
|
|
|
|
|
+ signal,
|
|
|
|
|
+ )
|
|
|
|
|
+ if (!response.ok) throw new Error(response.error.message)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ owned?.[Symbol.dispose]()
|
|
|
}
|
|
}
|
|
|
- if (signal.aborted) return
|
|
|
|
|
- const result: RemoteEventResult = {
|
|
|
|
|
- clientId,
|
|
|
|
|
- eventId: frame.eventId,
|
|
|
|
|
- outcome: outcome.kind === 'result' && outcome.value === undefined
|
|
|
|
|
- ? { kind: 'result' }
|
|
|
|
|
- : outcome,
|
|
|
|
|
- }
|
|
|
|
|
- const response = await this.connection.rpc.call(
|
|
|
|
|
- '/api',
|
|
|
|
|
- REMOTE_EVENT_RESULT_ENDPOINT,
|
|
|
|
|
- { args: result },
|
|
|
|
|
- signal,
|
|
|
|
|
- )
|
|
|
|
|
- if (!response.ok) throw new Error(response.error.message)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async dispatchWaterfall(
|
|
private async dispatchWaterfall(
|
|
@@ -230,14 +238,12 @@ export class ClientRemoteEvents {
|
|
|
agent: target,
|
|
agent: target,
|
|
|
signal,
|
|
signal,
|
|
|
}
|
|
}
|
|
|
- const value = await abortable(
|
|
|
|
|
- Promise.resolve(privateEvents(target).waterfall(
|
|
|
|
|
- target,
|
|
|
|
|
- this.eventKey(frame.event),
|
|
|
|
|
- request,
|
|
|
|
|
- () => Promise.resolve(REMOTE_EVENT_NEXT),
|
|
|
|
|
- )),
|
|
|
|
|
- signal,
|
|
|
|
|
|
|
+ // Cancellation reaches the handler, whose Context remains owned until it settles.
|
|
|
|
|
+ const value = await privateEvents(target).waterfall(
|
|
|
|
|
+ target,
|
|
|
|
|
+ this.eventKey(frame.event),
|
|
|
|
|
+ request,
|
|
|
|
|
+ () => Promise.resolve(REMOTE_EVENT_NEXT),
|
|
|
)
|
|
)
|
|
|
if (value !== REMOTE_EVENT_NEXT && value !== undefined && !isRemoteJsonValue(value)) {
|
|
if (value !== REMOTE_EVENT_NEXT && value !== undefined && !isRemoteJsonValue(value)) {
|
|
|
throw new TypeError('Remote event listener result is not lossless JSON data')
|
|
throw new TypeError('Remote event listener result is not lossless JSON data')
|
|
@@ -327,20 +333,6 @@ function invalidRemoteEventFrame(): never {
|
|
|
throw new TypeError('client api: invalid forwarded Remote event frame')
|
|
throw new TypeError('client api: invalid forwarded Remote event frame')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** Race listener completion against its delivery lifetime. */
|
|
|
|
|
-async function abortable<T>(value: T | PromiseLike<T>, signal: AbortSignal): Promise<T> {
|
|
|
|
|
- signal.throwIfAborted()
|
|
|
|
|
- let rejectAbort: ((reason: unknown) => void) | undefined
|
|
|
|
|
- const aborted = new Promise<never>((_resolve, reject) => { rejectAbort = reject })
|
|
|
|
|
- const onAbort = (): void => { rejectAbort?.(signal.reason) }
|
|
|
|
|
- signal.addEventListener('abort', onAbort, { once: true })
|
|
|
|
|
- try {
|
|
|
|
|
- return await Promise.race([Promise.resolve(value), aborted])
|
|
|
|
|
- } finally {
|
|
|
|
|
- signal.removeEventListener('abort', onAbort)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function privateEvents(ctx: Context): PrivateEventContext {
|
|
function privateEvents(ctx: Context): PrivateEventContext {
|
|
|
return ctx
|
|
return ctx
|
|
|
}
|
|
}
|