remote.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. /** Test-owned Remote face: `$on` subscriptions driven by the internal forwarded-event plumbing. */
  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 and dispatch only.
  8. *
  9. * Dispatch is driven the same way production drives it: `client/runtime` owns the
  10. * host frame sink and hands each decoded `host/remote-event` frame to
  11. * `$dispatch`. A spec therefore exercises its refresh chains by calling
  12. * `$dispatch(name, args)` on this double.
  13. *
  14. * `$mount` rejects: a spec that reaches a generated namespace through this
  15. * double has outgrown it and needs the real Client Remote service.
  16. *
  17. * One deliberate asymmetry with production: a throwing listener propagates out
  18. * of the emit instead of being contained and logged, so a spec cannot lean on
  19. * this double for the containment guarantee `$on` documents — assert that
  20. * against the real service.
  21. */
  22. export class TestRemote {
  23. private readonly subscriptions = new Map<string, Set<(...args: never[]) => void>>()
  24. /**
  25. * Register the double as `ctx.remote`.
  26. * @param ctx - the spec's root Context.
  27. */
  28. constructor(ctx: Context) {
  29. ctx.provide('remote', this)
  30. }
  31. /**
  32. * Deliver one forwarded host event to its subscribers, standing in for the
  33. * carrier that owns the frame sink.
  34. * @param event - forwarded host event name.
  35. * @param args - the Host argument list, verbatim.
  36. */
  37. $dispatch(event: string, args: readonly unknown[]): void {
  38. const listeners = this.subscriptions.get(event)
  39. if (listeners === undefined) return
  40. for (const listener of [...listeners]) listener(...args as never[])
  41. }
  42. /**
  43. * Subscribe to one forwarded host event.
  44. * @param event - forwarded host event name.
  45. * @param listener - receives the Host argument list verbatim.
  46. * @returns disposer removing this subscription.
  47. */
  48. $on(event: string, listener: (...args: never[]) => void): () => void {
  49. const listeners = this.subscriptions.get(event) ?? new Set()
  50. this.subscriptions.set(event, listeners)
  51. listeners.add(listener)
  52. return () => { listeners.delete(listener) }
  53. }
  54. /**
  55. * Generated-namespace mount, unsupported by this double.
  56. * @returns never; always rejects.
  57. */
  58. $mount(): Promise<() => Promise<void>> {
  59. return Promise.reject(new Error('TestRemote: $mount needs the real Client Remote service'))
  60. }
  61. }