feat(remote): deliver allowlisted Host events through ctx.remote.$on
api/remotes owns the allowlist and its type projection; type-meta owns the shape predicate, the selection seat, and the internal remote/host-event carrier signal; api/gateway's Client half turns that signal into $on callbacks through a private dispatch. apiproxy forwards each allowlisted emission verbatim in one host/remote-event frame, registered ahead of the derived invalidation frames so frame order is unchanged, and drops the three per-event variants it replaces. Owner packages move their Events declarations into client-safe ./types exports, so a consumer's listener signature is the Host's own declaration.
This commit is contained in:
@@ -49,6 +49,7 @@ export type {
|
||||
TypeRTContextRegistry,
|
||||
TypeRTContextWire,
|
||||
TypeRTDisposer,
|
||||
TypeRTForwardableEvent,
|
||||
TypeRTHostContextProvider,
|
||||
TypeRTHostContextResolver,
|
||||
TypeRTLocalRegistry,
|
||||
@@ -64,6 +65,8 @@ export type {
|
||||
TypeRTRemoteScopeMap,
|
||||
TypeRTRemoteScopeNamespace,
|
||||
TypeRTRemoteContribution,
|
||||
TypeRTRemoteEvent,
|
||||
TypeRTRemoteEventSelection,
|
||||
TypeRTRemoteMap,
|
||||
TypeRTRemoteNamespace,
|
||||
TypeRTRemoteNamespaceMap,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @module @deepseek-ai/dsh-type-meta/types
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Context, Events } from '@deepseek-ai/cordis'
|
||||
|
||||
declare const LOOKUP_HOST: unique symbol
|
||||
declare const LOOKUP_WIRE: unique symbol
|
||||
@@ -42,6 +42,24 @@ export interface TypeRTRemoteMap {}
|
||||
/** Merge-extensible scoped Remote method signatures generated for consumers. */
|
||||
export interface TypeRTRemoteScopeMap {}
|
||||
|
||||
/**
|
||||
* Cordis event names whose shape a one-way Remote delivery can carry: unbound
|
||||
* from any Scope and returning `void`. Which ones are actually forwarded is the
|
||||
* Host assembly's selection; this predicate only excludes shapes the carrier
|
||||
* cannot represent.
|
||||
*/
|
||||
export type TypeRTForwardableEvent = {
|
||||
[Event in keyof Events]: unknown extends ThisParameterType<Events[Event]>
|
||||
? ReturnType<Events[Event]> extends void ? Event : never
|
||||
: never
|
||||
}[keyof Events]
|
||||
|
||||
/** Merge-extensible forwarding selection declared once by the Host assembly. */
|
||||
export interface TypeRTRemoteEventSelection {}
|
||||
|
||||
/** Legal `$on` keys: selected events that exist in the current compilation face. */
|
||||
export type TypeRTRemoteEvent = Extract<keyof Events, keyof TypeRTRemoteEventSelection>
|
||||
|
||||
/**
|
||||
* Resolve one direct Remote namespace from the generated flat endpoint map.
|
||||
* @template Namespace - wire namespace before the endpoint slash.
|
||||
@@ -184,6 +202,15 @@ export interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap {
|
||||
* @returns disposer after namespace services and concrete methods are ready.
|
||||
*/
|
||||
$mount(contribution: TypeRTRemoteContribution): Promise<TypeRTDisposer>
|
||||
/**
|
||||
* Subscribe to one forwarded Host event; delivery is one-way, in registration
|
||||
* order, and isolates a throwing listener from the rest.
|
||||
* @template Event - forwarded event name selected by the Host assembly.
|
||||
* @param event - forwarded Host event name, unchanged on the wire.
|
||||
* @param listener - receives the Host's argument list as declared by Cordis `Events`.
|
||||
* @returns disposer owned by the calling fiber.
|
||||
*/
|
||||
$on<Event extends TypeRTRemoteEvent>(event: Event, listener: Events[Event]): () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,4 +452,18 @@ declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
typert: TypeRTService
|
||||
}
|
||||
|
||||
interface Events {
|
||||
/**
|
||||
* The carrier received one allowlisted host event forwarded over the wire.
|
||||
* Declared here because both compilation faces share this package; only the
|
||||
* consumer side participates, where the Client half owning the host frame
|
||||
* sink emits it and the Remote service is its only subscriber, turning it
|
||||
* into `$on` callbacks. The Host neither emits nor observes it.
|
||||
* @mode emit
|
||||
* @param event - forwarded host event name, exactly as the Host emitted it.
|
||||
* @param args - the Host argument list, already JSON-decoded.
|
||||
*/
|
||||
'remote/host-event'(event: string, args: readonly unknown[]): void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import {
|
||||
bindTypeRTGateway,
|
||||
GatewayService,
|
||||
@@ -9,12 +9,38 @@ import {
|
||||
RemoteScope,
|
||||
remoteMethods,
|
||||
type TypeRTContext,
|
||||
type TypeRTForwardableEvent,
|
||||
type TypeRTRemoteEvent,
|
||||
} from '@deepseek-ai/dsh-type-meta'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Events {
|
||||
/**
|
||||
* Test-only one-way event: bound to no Scope and returning nothing.
|
||||
* @param value - marker payload.
|
||||
*/
|
||||
'meta-fixture/forwardable'(value: string): void
|
||||
/**
|
||||
* Test-only Scope-bound event, which no carrier can deliver one-way.
|
||||
* @param value - marker payload.
|
||||
*/
|
||||
'meta-fixture/scoped'(this: Context, value: string): void
|
||||
/**
|
||||
* Test-only answered event, whose result no one-way delivery can return.
|
||||
* @param value - marker payload.
|
||||
* @returns the replacement value.
|
||||
*/
|
||||
'meta-fixture/answered'(value: string): string
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-type-meta' {
|
||||
interface TypeRTContextMap {
|
||||
metaFixture: TypeRTContext<string>
|
||||
}
|
||||
|
||||
interface TypeRTRemoteEventSelection extends
|
||||
Record<'meta-fixture/forwardable' | 'meta-fixture/absent', true> {}
|
||||
}
|
||||
|
||||
describe('type-meta Remote declarations', () => {
|
||||
@@ -209,6 +235,16 @@ describe('type-meta Remote declarations', () => {
|
||||
expect(() => bindTypeRTGateway({}, 'goals', { namespace: 'api/goals' })).toThrow('namespace')
|
||||
expect(() => bindTypeRTGateway({}, 'goals', { namespace: 'api goals' })).toThrow('namespace')
|
||||
})
|
||||
|
||||
it('admits only one-way event shapes and only selected events that exist', () => {
|
||||
expectTypeOf<'meta-fixture/forwardable'>().toExtend<TypeRTForwardableEvent>()
|
||||
expectTypeOf<'meta-fixture/scoped'>().not.toExtend<TypeRTForwardableEvent>()
|
||||
expectTypeOf<'meta-fixture/answered'>().not.toExtend<TypeRTForwardableEvent>()
|
||||
|
||||
expectTypeOf<'meta-fixture/forwardable'>().toExtend<TypeRTRemoteEvent>()
|
||||
expectTypeOf<'meta-fixture/scoped'>().not.toExtend<TypeRTRemoteEvent>()
|
||||
expectTypeOf<'meta-fixture/absent'>().not.toExtend<TypeRTRemoteEvent>()
|
||||
})
|
||||
})
|
||||
|
||||
function methodContext<This extends object>(
|
||||
|
||||
Reference in New Issue
Block a user