remote.ts 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. /** Test-owned Remote face: `$on` subscriptions with an explicit test event driver. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. // Value re-export for spec-side failure construction: the api-remotes facade
  4. // cannot carry it — its src top-level imports owner /remote lib artifacts, so a
  5. // value import from a spec would load the unbuilt assembly chain.
  6. export { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
  7. /**
  8. * Remote service test double for the forwarded-event path. Feature specs need
  9. * `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
  10. * Host events to reach those subscribers, but not the wire — so this double
  11. * implements subscription plus an explicit `emit` driver available only on the
  12. * concrete test object. A spec that also calls one namespace scripts it through
  13. * the constructor rather than reaching the real Client Remote service.
  14. *
  15. * `$mount` rejects: a spec that needs a real generated contribution installed —
  16. * codecs, descriptors, and the wire — has outgrown this double and needs the
  17. * real Client Remote service.
  18. *
  19. * One deliberate asymmetry with production: a throwing listener propagates out
  20. * of the emit instead of being contained and logged, so a spec cannot lean on
  21. * this double for the containment guarantee `$on` documents — assert that
  22. * against the real service.
  23. */
  24. export class TestRemote {
  25. private readonly subscriptions = new Map<string, Set<(...args: never[]) => void>>()
  26. /**
  27. * Fixed Host facts mirrored from the production `ctx.remote.$host`. Plain
  28. * mutable field: a spec assigns it to script a non-loopback or homed Host.
  29. */
  30. $host: { home: string | undefined; isLoopback: boolean } = { home: undefined, isLoopback: true }
  31. /**
  32. * Register the double as `ctx.remote`, plus one service per scripted
  33. * namespace so a plugin injecting `remote.<name>` also unparks.
  34. * @param ctx - the spec's root Context.
  35. * @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
  36. */
  37. constructor(ctx: Context, namespaces: Readonly<Record<string, object>> = {}) {
  38. for (const name of Object.keys(namespaces)) {
  39. // A namespace named after one of the double's own members would replace
  40. // it, and `$mount`'s rejection is the contract a spec relies on.
  41. if (name in TestRemote.prototype || name === 'subscriptions' || name === '$host') {
  42. throw new TypeError(`TestRemote: scripted namespace "${name}" would shadow the double's own member`)
  43. }
  44. }
  45. Object.assign(this, namespaces)
  46. ctx.provide('remote', this)
  47. for (const [name, face] of Object.entries(namespaces)) ctx.provide(`remote.${name}`, face)
  48. }
  49. /**
  50. * Deliver one forwarded host event to its subscribers, standing in for the
  51. * carrier that owns the frame sink.
  52. * @param event - forwarded host event name.
  53. * @param args - the Host argument list, verbatim.
  54. */
  55. emit(event: string, args: readonly unknown[]): void {
  56. const listeners = this.subscriptions.get(event)
  57. if (listeners === undefined) return
  58. for (const listener of [...listeners]) listener(...args as never[])
  59. }
  60. /**
  61. * Subscribe to one forwarded host event.
  62. * @param event - forwarded host event name.
  63. * @param listener - receives the Host argument list verbatim.
  64. * @returns disposer removing this subscription.
  65. */
  66. $on(event: string, listener: (...args: never[]) => void): () => void {
  67. const listeners = this.subscriptions.get(event) ?? new Set()
  68. this.subscriptions.set(event, listeners)
  69. listeners.add(listener)
  70. return () => { listeners.delete(listener) }
  71. }
  72. /**
  73. * Generated-namespace mount, unsupported by this double.
  74. * @returns never; always rejects.
  75. */
  76. $mount(): Promise<() => Promise<void>> {
  77. return Promise.reject(new Error('TestRemote: $mount needs the real Client Remote service'))
  78. }
  79. }