remote.ts 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. /** Test-owned Remote face: `$on` subscriptions with an explicit test event driver. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. /**
  4. * Remote service test double for the forwarded-event path. Feature specs need
  5. * `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
  6. * Host events to reach those subscribers, but not the generated namespaces or
  7. * the wire — so this double implements subscription plus an explicit `emit`
  8. * driver available only on the concrete test object.
  9. *
  10. * `$mount` rejects: a spec that reaches a generated namespace through this
  11. * double has outgrown it and needs the real Client Remote service.
  12. *
  13. * One deliberate asymmetry with production: a throwing listener propagates out
  14. * of the emit instead of being contained and logged, so a spec cannot lean on
  15. * this double for the containment guarantee `$on` documents — assert that
  16. * against the real service.
  17. */
  18. export class TestRemote {
  19. private readonly subscriptions = new Map<string, Set<(...args: never[]) => void>>()
  20. /**
  21. * Register the double as `ctx.remote`.
  22. * @param ctx - the spec's root Context.
  23. */
  24. constructor(ctx: Context) {
  25. ctx.provide('remote', this)
  26. }
  27. /**
  28. * Deliver one forwarded host event to its subscribers, standing in for the
  29. * carrier that owns the frame sink.
  30. * @param event - forwarded host event name.
  31. * @param args - the Host argument list, verbatim.
  32. */
  33. emit(event: string, args: readonly unknown[]): void {
  34. const listeners = this.subscriptions.get(event)
  35. if (listeners === undefined) return
  36. for (const listener of [...listeners]) listener(...args as never[])
  37. }
  38. /**
  39. * Subscribe to one forwarded host event.
  40. * @param event - forwarded host event name.
  41. * @param listener - receives the Host argument list verbatim.
  42. * @returns disposer removing this subscription.
  43. */
  44. $on(event: string, listener: (...args: never[]) => void): () => void {
  45. const listeners = this.subscriptions.get(event) ?? new Set()
  46. this.subscriptions.set(event, listeners)
  47. listeners.add(listener)
  48. return () => { listeners.delete(listener) }
  49. }
  50. /**
  51. * Generated-namespace mount, unsupported by this double.
  52. * @returns never; always rejects.
  53. */
  54. $mount(): Promise<() => Promise<void>> {
  55. return Promise.reject(new Error('TestRemote: $mount needs the real Client Remote service'))
  56. }
  57. }