| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 |
- /** Test-owned Remote face: `$on` subscriptions with an explicit test event driver. */
- import type { Context } from '@deepseek-ai/cordis'
- /**
- * Remote service test double for the forwarded-event path. Feature specs need
- * `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
- * Host events to reach those subscribers, but not the wire — so this double
- * implements subscription plus an explicit `emit` driver available only on the
- * concrete test object. A spec that also calls one namespace scripts it through
- * the constructor rather than reaching the real Client Remote service.
- *
- * `$mount` rejects: a spec that needs a real generated contribution installed —
- * codecs, descriptors, and the wire — has outgrown this double and needs the
- * real Client Remote service.
- *
- * One deliberate asymmetry with production: a throwing listener propagates out
- * of the emit instead of being contained and logged, so a spec cannot lean on
- * this double for the containment guarantee `$on` documents — assert that
- * against the real service.
- */
- export class TestRemote {
- private readonly subscriptions = new Map<string, Set<(...args: never[]) => void>>()
- /**
- * Register the double as `ctx.remote`, plus one service per scripted
- * namespace so a plugin injecting `remote.<name>` also unparks.
- * @param ctx - the spec's root Context.
- * @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
- */
- constructor(ctx: Context, namespaces: Readonly<Record<string, object>> = {}) {
- for (const name of Object.keys(namespaces)) {
- // A namespace named after one of the double's own members would replace
- // it, and `$mount`'s rejection is the contract a spec relies on.
- if (name in TestRemote.prototype || name === 'subscriptions') {
- throw new TypeError(`TestRemote: scripted namespace "${name}" would shadow the double's own member`)
- }
- }
- Object.assign(this, namespaces)
- ctx.provide('remote', this)
- for (const [name, face] of Object.entries(namespaces)) ctx.provide(`remote.${name}`, face)
- }
- /**
- * Deliver one forwarded host event to its subscribers, standing in for the
- * carrier that owns the frame sink.
- * @param event - forwarded host event name.
- * @param args - the Host argument list, verbatim.
- */
- emit(event: string, args: readonly unknown[]): void {
- const listeners = this.subscriptions.get(event)
- if (listeners === undefined) return
- for (const listener of [...listeners]) listener(...args as never[])
- }
- /**
- * Subscribe to one forwarded host event.
- * @param event - forwarded host event name.
- * @param listener - receives the Host argument list verbatim.
- * @returns disposer removing this subscription.
- */
- $on(event: string, listener: (...args: never[]) => void): () => void {
- const listeners = this.subscriptions.get(event) ?? new Set()
- this.subscriptions.set(event, listeners)
- listeners.add(listener)
- return () => { listeners.delete(listener) }
- }
- /**
- * Generated-namespace mount, unsupported by this double.
- * @returns never; always rejects.
- */
- $mount(): Promise<() => Promise<void>> {
- return Promise.reject(new Error('TestRemote: $mount needs the real Client Remote service'))
- }
- }
|