@noego/wood 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/bin/wood.js +6 -1
  2. package/dist/client/app_env.cjs +39 -0
  3. package/dist/client/app_env.cjs.map +1 -0
  4. package/dist/client/app_env.d.cts +7 -0
  5. package/dist/client/app_env.d.ts +7 -0
  6. package/dist/client/app_env.js +14 -0
  7. package/dist/client/app_env.js.map +1 -0
  8. package/dist/client/index.cjs +33 -9
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.cts +5 -1
  11. package/dist/client/index.d.ts +5 -1
  12. package/dist/client/index.js +32 -9
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/codegen/channel_map_generator.cjs +102 -0
  15. package/dist/codegen/channel_map_generator.cjs.map +1 -0
  16. package/dist/codegen/channel_map_generator.d.cts +6 -0
  17. package/dist/codegen/channel_map_generator.d.ts +6 -0
  18. package/dist/codegen/channel_map_generator.js +78 -0
  19. package/dist/codegen/channel_map_generator.js.map +1 -0
  20. package/dist/codegen/controller_registration_generator.cjs +6 -1
  21. package/dist/codegen/controller_registration_generator.cjs.map +1 -1
  22. package/dist/codegen/controller_registration_generator.js +6 -1
  23. package/dist/codegen/controller_registration_generator.js.map +1 -1
  24. package/dist/codegen/generated_file_notice.cjs +40 -0
  25. package/dist/codegen/generated_file_notice.cjs.map +1 -0
  26. package/dist/codegen/generated_file_notice.d.cts +3 -0
  27. package/dist/codegen/generated_file_notice.d.ts +3 -0
  28. package/dist/codegen/generated_file_notice.js +16 -0
  29. package/dist/codegen/generated_file_notice.js.map +1 -0
  30. package/dist/codegen/index.cjs +3 -0
  31. package/dist/codegen/index.cjs.map +1 -1
  32. package/dist/codegen/index.d.cts +1 -0
  33. package/dist/codegen/index.d.ts +1 -0
  34. package/dist/codegen/index.js +2 -0
  35. package/dist/codegen/index.js.map +1 -1
  36. package/dist/codegen/preload_generator.cjs +97 -1
  37. package/dist/codegen/preload_generator.cjs.map +1 -1
  38. package/dist/codegen/preload_generator.js +97 -1
  39. package/dist/codegen/preload_generator.js.map +1 -1
  40. package/dist/codegen/renderer_app_generator.cjs +6 -1
  41. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  42. package/dist/codegen/renderer_app_generator.js +6 -1
  43. package/dist/codegen/renderer_app_generator.js.map +1 -1
  44. package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
  45. package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
  46. package/dist/codegen/renderer_controller_registration_generator.js +6 -1
  47. package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
  48. package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
  49. package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
  50. package/dist/codegen/renderer_view_manifest_generator.js +6 -1
  51. package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
  52. package/dist/codegen/runtime_generator.cjs +6 -1
  53. package/dist/codegen/runtime_generator.cjs.map +1 -1
  54. package/dist/codegen/runtime_generator.js +6 -1
  55. package/dist/codegen/runtime_generator.js.map +1 -1
  56. package/dist/codegen/test_mock_generator.cjs +5 -1
  57. package/dist/codegen/test_mock_generator.cjs.map +1 -1
  58. package/dist/codegen/test_mock_generator.js +5 -1
  59. package/dist/codegen/test_mock_generator.js.map +1 -1
  60. package/dist/codegen/type_generator.cjs +6 -1
  61. package/dist/codegen/type_generator.cjs.map +1 -1
  62. package/dist/codegen/type_generator.js +6 -1
  63. package/dist/codegen/type_generator.js.map +1 -1
  64. package/dist/index.cjs +19 -6
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +7 -1
  67. package/dist/index.d.ts +7 -1
  68. package/dist/index.js +13 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/main/index.cjs +133 -55
  71. package/dist/main/index.cjs.map +1 -1
  72. package/dist/main/index.d.cts +30 -2
  73. package/dist/main/index.d.ts +30 -2
  74. package/dist/main/index.js +134 -55
  75. package/dist/main/index.js.map +1 -1
  76. package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
  77. package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
  78. package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
  79. package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
  80. package/dist/main/renderer_trace_drain_coordinator.js +101 -0
  81. package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
  82. package/dist/router/wood_router.cjs +9 -1
  83. package/dist/router/wood_router.cjs.map +1 -1
  84. package/dist/router/wood_router.d.cts +1 -1
  85. package/dist/router/wood_router.d.ts +1 -1
  86. package/dist/router/wood_router.js +9 -1
  87. package/dist/router/wood_router.js.map +1 -1
  88. package/dist/runtime/app_env.cjs +52 -0
  89. package/dist/runtime/app_env.cjs.map +1 -0
  90. package/dist/runtime/app_env.d.cts +10 -0
  91. package/dist/runtime/app_env.d.ts +10 -0
  92. package/dist/runtime/app_env.js +18 -0
  93. package/dist/runtime/app_env.js.map +1 -0
  94. package/dist/runtime/index.cjs +9 -2
  95. package/dist/runtime/index.cjs.map +1 -1
  96. package/dist/runtime/index.d.cts +5 -1
  97. package/dist/runtime/index.d.ts +5 -1
  98. package/dist/runtime/index.js +9 -1
  99. package/dist/runtime/index.js.map +1 -1
  100. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  101. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  102. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  103. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  104. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
  105. package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
  106. package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
  107. package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
  108. package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
  109. package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
  110. package/dist/runtime/runtime.cjs +75 -57
  111. package/dist/runtime/runtime.cjs.map +1 -1
  112. package/dist/runtime/runtime.d.cts +8 -1
  113. package/dist/runtime/runtime.d.ts +8 -1
  114. package/dist/runtime/runtime.js +80 -58
  115. package/dist/runtime/runtime.js.map +1 -1
  116. package/dist/testing/browser.cjs +540 -11
  117. package/dist/testing/browser.cjs.map +1 -1
  118. package/dist/testing/browser.d.cts +219 -3
  119. package/dist/testing/browser.d.ts +219 -3
  120. package/dist/testing/browser.js +540 -11
  121. package/dist/testing/browser.js.map +1 -1
  122. package/dist/testing/create_controller_harness.d.cts +1 -1
  123. package/dist/testing/create_controller_harness.d.ts +1 -1
  124. package/dist/testing/index.cjs +13 -0
  125. package/dist/testing/index.cjs.map +1 -1
  126. package/dist/testing/index.d.cts +7 -2
  127. package/dist/testing/index.d.ts +7 -2
  128. package/dist/testing/index.js +14 -0
  129. package/dist/testing/index.js.map +1 -1
  130. package/dist/trace-testing/index.d.cts +1 -1
  131. package/dist/trace-testing/index.d.ts +1 -1
  132. package/dist/trace-testing/trace_recorder.d.cts +1 -1
  133. package/dist/trace-testing/trace_recorder.d.ts +1 -1
  134. package/dist/trace-transport/index.cjs +32 -0
  135. package/dist/trace-transport/index.cjs.map +1 -0
  136. package/dist/trace-transport/index.d.cts +6 -0
  137. package/dist/trace-transport/index.d.ts +6 -0
  138. package/dist/trace-transport/index.js +7 -0
  139. package/dist/trace-transport/index.js.map +1 -0
  140. package/dist/trace_hub-Cob54qRx.d.cts +56 -0
  141. package/dist/trace_hub-Cob54qRx.d.ts +56 -0
  142. package/dist/tracing/index.cjs +14 -0
  143. package/dist/tracing/index.cjs.map +1 -1
  144. package/dist/tracing/index.d.cts +7 -1
  145. package/dist/tracing/index.d.ts +7 -1
  146. package/dist/tracing/index.js +12 -0
  147. package/dist/tracing/index.js.map +1 -1
  148. package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
  149. package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
  150. package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
  151. package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
  152. package/dist/tracing/renderer_trace_batch_host.js +56 -0
  153. package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
  154. package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
  155. package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
  156. package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
  157. package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
  158. package/dist/tracing/renderer_trace_batch_transport.js +292 -0
  159. package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
  160. package/dist/tracing/trace_commit_writer.cjs +159 -0
  161. package/dist/tracing/trace_commit_writer.cjs.map +1 -0
  162. package/dist/tracing/trace_commit_writer.d.cts +97 -0
  163. package/dist/tracing/trace_commit_writer.d.ts +97 -0
  164. package/dist/tracing/trace_commit_writer.js +135 -0
  165. package/dist/tracing/trace_commit_writer.js.map +1 -0
  166. package/dist/tracing/trace_hub.cjs +58 -3
  167. package/dist/tracing/trace_hub.cjs.map +1 -1
  168. package/dist/tracing/trace_hub.d.cts +2 -46
  169. package/dist/tracing/trace_hub.d.ts +2 -46
  170. package/dist/tracing/trace_hub.js +56 -3
  171. package/dist/tracing/trace_hub.js.map +1 -1
  172. package/dist/tracing/trace_provider.cjs +42 -12
  173. package/dist/tracing/trace_provider.cjs.map +1 -1
  174. package/dist/tracing/trace_provider.d.cts +15 -1
  175. package/dist/tracing/trace_provider.d.ts +15 -1
  176. package/dist/tracing/trace_provider.js +43 -12
  177. package/dist/tracing/trace_provider.js.map +1 -1
  178. package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
  179. package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
  180. package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
  181. package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
  182. package/dist/tracing/types/wood_trace_batch.types.js +1 -0
  183. package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
  184. package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
  185. package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
  186. package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
  187. package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
  188. package/dist/tracing/types/wood_trace_sink.types.js +1 -0
  189. package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
  190. package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
  191. package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
  192. package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
  193. package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
  194. package/dist/tracing/wood_trace_batch_contract.js +205 -0
  195. package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
  196. package/dist/transport/build_transport_bridge.cjs +62 -0
  197. package/dist/transport/build_transport_bridge.cjs.map +1 -0
  198. package/dist/transport/build_transport_bridge.d.cts +29 -0
  199. package/dist/transport/build_transport_bridge.d.ts +29 -0
  200. package/dist/transport/build_transport_bridge.js +38 -0
  201. package/dist/transport/build_transport_bridge.js.map +1 -0
  202. package/dist/transport/in_process_router_transport.cjs +41 -0
  203. package/dist/transport/in_process_router_transport.cjs.map +1 -0
  204. package/dist/transport/in_process_router_transport.d.cts +31 -0
  205. package/dist/transport/in_process_router_transport.d.ts +31 -0
  206. package/dist/transport/in_process_router_transport.js +17 -0
  207. package/dist/transport/in_process_router_transport.js.map +1 -0
  208. package/dist/transport/index.cjs +42 -0
  209. package/dist/transport/index.cjs.map +1 -0
  210. package/dist/transport/index.d.cts +8 -0
  211. package/dist/transport/index.d.ts +8 -0
  212. package/dist/transport/index.js +19 -0
  213. package/dist/transport/index.js.map +1 -0
  214. package/dist/transport/loopback_remote_transport.cjs +270 -0
  215. package/dist/transport/loopback_remote_transport.cjs.map +1 -0
  216. package/dist/transport/loopback_remote_transport.d.cts +49 -0
  217. package/dist/transport/loopback_remote_transport.d.ts +49 -0
  218. package/dist/transport/loopback_remote_transport.js +237 -0
  219. package/dist/transport/loopback_remote_transport.js.map +1 -0
  220. package/dist/transport/loopback_serialization.cjs +116 -0
  221. package/dist/transport/loopback_serialization.cjs.map +1 -0
  222. package/dist/transport/loopback_serialization.d.cts +28 -0
  223. package/dist/transport/loopback_serialization.d.ts +28 -0
  224. package/dist/transport/loopback_serialization.js +91 -0
  225. package/dist/transport/loopback_serialization.js.map +1 -0
  226. package/dist/transport/transport_contract_suite.cjs +110 -0
  227. package/dist/transport/transport_contract_suite.cjs.map +1 -0
  228. package/dist/transport/transport_contract_suite.d.cts +23 -0
  229. package/dist/transport/transport_contract_suite.d.ts +23 -0
  230. package/dist/transport/transport_contract_suite.js +85 -0
  231. package/dist/transport/transport_contract_suite.js.map +1 -0
  232. package/dist/transport/wood_rpc_transport.cjs +17 -0
  233. package/dist/transport/wood_rpc_transport.cjs.map +1 -0
  234. package/dist/transport/wood_rpc_transport.d.cts +44 -0
  235. package/dist/transport/wood_rpc_transport.d.ts +44 -0
  236. package/dist/transport/wood_rpc_transport.js +1 -0
  237. package/dist/transport/wood_rpc_transport.js.map +1 -0
  238. package/docs/browser-testing.md +139 -1
  239. package/package.json +22 -3
  240. package/src/components/NavigationShell.browser.test.ts +236 -0
  241. package/src/components/NavigationShell.svelte +162 -49
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transport/loopback_remote_transport.ts"],"sourcesContent":["import http from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport type { WoodRpcTransport } from './wood_rpc_transport.js';\nimport {\n assertLoopbackSerializable,\n LoopbackSerializationError,\n} from './loopback_serialization.js';\n\n/**\n * Loopback (127.0.0.1) HTTP transport — TEST ONLY.\n *\n * `invoke` is a plain HTTP request/response. Push events use HTTP long-polling\n * (no WebSocket dependency: `ws` is not a wood dependency and the loopback\n * transport is not a product feature, so long-polling over `node:http` is the\n * simplest faithful choice). JSON is the wire format, so both ends run the\n * strict `assertLoopbackSerializable` guard: anything JSON would coerce or drop\n * (Date, undefined, binary, class instances, non-finite numbers) is rejected\n * with a clear error rather than silently mangled. Electron structured-clone is\n * the ground-truth contract this guard defends.\n *\n * The server binds to `127.0.0.1` only and is never wired into production\n * startup (exported solely from the transport/testing entry points, which the\n * main runtime does not import).\n */\n\nconst LONG_POLL_TIMEOUT_MS = 25_000;\nconst LOOPBACK_HOST = '127.0.0.1';\n\ninterface PushEvent {\n seq: number;\n channel: string;\n data: unknown;\n}\n\ninterface InvokeErrorBody {\n message: string;\n name: string;\n}\n\nfunction readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('error', reject);\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf-8');\n if (raw.length === 0) {\n resolve(undefined);\n return;\n }\n try {\n resolve(JSON.parse(raw));\n } catch (error) {\n reject(error);\n }\n });\n });\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown): void {\n const payload = JSON.stringify(body ?? null);\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(payload);\n}\n\n/**\n * The loopback server. Wraps a `WoodRpcTransport` for `invoke` and acts as the\n * push-event sink: wire `WindowEventService.configure((ch, data) => server.emit(ch, data))`.\n */\nexport class LoopbackRemoteTransportServer {\n private readonly transport: WoodRpcTransport;\n private readonly server: http.Server;\n private readonly eventLog: PushEvent[] = [];\n private readonly waiters = new Set<(events: PushEvent[]) => void>();\n private seq = 0;\n private listening = false;\n\n constructor(transport: WoodRpcTransport) {\n this.transport = transport;\n this.server = http.createServer((req, res) => {\n this.handle(req, res).catch((error) => {\n sendJson(res, 500, { message: error instanceof Error ? error.message : String(error) });\n });\n });\n }\n\n get port(): number {\n const address = this.server.address() as AddressInfo | null;\n if (!address || typeof address === 'string') {\n throw new Error('Loopback server is not listening.');\n }\n return address.port;\n }\n\n get url(): string {\n return `http://${LOOPBACK_HOST}:${this.port}`;\n }\n\n async listen(): Promise<string> {\n if (this.listening) return this.url;\n await new Promise<void>((resolve, reject) => {\n this.server.once('error', reject);\n this.server.listen(0, LOOPBACK_HOST, () => {\n this.server.off('error', reject);\n resolve();\n });\n });\n this.listening = true;\n return this.url;\n }\n\n /**\n * Push-event sink. Matches the `WindowEventService` sender signature: the\n * channel arrives with the `__event:` prefix, which is stripped here.\n */\n emit(channel: string, data: unknown): void {\n const stripped = channel.startsWith('__event:')\n ? channel.slice('__event:'.length)\n : channel;\n assertLoopbackSerializable(data, `event:${stripped}`);\n const event: PushEvent = { seq: ++this.seq, channel: stripped, data };\n this.eventLog.push(event);\n for (const waiter of this.waiters) {\n waiter([event]);\n }\n this.waiters.clear();\n }\n\n async close(): Promise<void> {\n for (const waiter of this.waiters) {\n waiter([]);\n }\n this.waiters.clear();\n if (!this.listening) return;\n await new Promise<void>((resolve, reject) => {\n this.server.close((error) => (error ? reject(error) : resolve()));\n });\n this.listening = false;\n }\n\n private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', this.url);\n if (req.method === 'POST' && url.pathname === '/invoke') {\n await this.handleInvoke(req, res);\n return;\n }\n if (req.method === 'GET' && url.pathname === '/events') {\n await this.handleEvents(url, res);\n return;\n }\n sendJson(res, 404, { message: `No loopback route for ${req.method} ${url.pathname}` });\n }\n\n private async handleInvoke(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const body = (await readJsonBody(req)) as { channel?: string; payload?: unknown } | undefined;\n if (!body || typeof body.channel !== 'string') {\n sendJson(res, 400, { message: 'Loopback invoke requires a \"channel\".' });\n return;\n }\n\n try {\n const result = await this.transport.invoke(body.channel, body.payload);\n assertLoopbackSerializable(result, `result:${body.channel}`);\n sendJson(res, 200, { ok: true, result: result ?? null });\n } catch (error) {\n const errorBody: InvokeErrorBody = {\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : 'Error',\n };\n sendJson(res, 200, { ok: false, error: errorBody });\n }\n }\n\n private handleEvents(url: URL, res: http.ServerResponse): Promise<void> {\n const cursor = Number.parseInt(url.searchParams.get('cursor') ?? '0', 10) || 0;\n const pending = this.eventLog.filter((event) => event.seq > cursor);\n if (pending.length > 0) {\n sendJson(res, 200, { events: pending });\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.waiters.delete(waiter);\n sendJson(res, 200, { events: [] });\n resolve();\n }, LONG_POLL_TIMEOUT_MS);\n\n const waiter = (events: PushEvent[]): void => {\n clearTimeout(timer);\n sendJson(res, 200, { events });\n resolve();\n };\n\n this.waiters.add(waiter);\n res.on('close', () => {\n clearTimeout(timer);\n this.waiters.delete(waiter);\n resolve();\n });\n });\n }\n}\n\n/** Client-side loopback transport. Talks to a `LoopbackRemoteTransportServer`. */\nexport class LoopbackRemoteTransport implements WoodRpcTransport {\n private readonly baseUrl: string;\n private readonly listeners = new Map<string, Set<(payload: unknown) => void>>();\n private cursor = 0;\n private polling = false;\n private closed = false;\n private pollController: AbortController | null = null;\n\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '');\n }\n\n async invoke(channel: string, payload?: unknown): Promise<unknown> {\n assertLoopbackSerializable(payload, `payload:${channel}`);\n const response = await fetch(`${this.baseUrl}/invoke`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ channel, payload }),\n });\n const body = (await response.json()) as\n | { ok: true; result: unknown }\n | { ok: false; error: InvokeErrorBody };\n\n if (body.ok) {\n return body.result;\n }\n const error = new Error(body.error.message);\n error.name = body.error.name;\n throw error;\n }\n\n subscribe(channel: string, listener: (payload: unknown) => void): () => void {\n let set = this.listeners.get(channel);\n if (!set) {\n set = new Set();\n this.listeners.set(channel, set);\n }\n set.add(listener);\n this.ensurePolling();\n\n return () => {\n set?.delete(listener);\n if (set && set.size === 0) {\n this.listeners.delete(channel);\n }\n };\n }\n\n /** Stop the long-poll loop and release the in-flight request. Call in teardown. */\n close(): void {\n this.closed = true;\n this.listeners.clear();\n this.pollController?.abort();\n }\n\n private ensurePolling(): void {\n if (this.polling || this.closed) return;\n this.polling = true;\n void this.pollLoop();\n }\n\n private async pollLoop(): Promise<void> {\n while (!this.closed && this.hasListeners()) {\n try {\n await this.pollOnce();\n } catch (error) {\n if (this.closed || (error as Error)?.name === 'AbortError') break;\n // Transient loopback error: yield a tick and retry.\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n this.polling = false;\n }\n\n private async pollOnce(): Promise<void> {\n this.pollController = new AbortController();\n const response = await fetch(`${this.baseUrl}/events?cursor=${this.cursor}`, {\n signal: this.pollController.signal,\n });\n const body = (await response.json()) as { events: PushEvent[] };\n for (const event of body.events) {\n this.cursor = Math.max(this.cursor, event.seq);\n this.dispatch(event.channel, event.data);\n }\n }\n\n private dispatch(channel: string, data: unknown): void {\n const set = this.listeners.get(channel);\n if (!set) return;\n for (const listener of set) {\n listener(data);\n }\n }\n\n private hasListeners(): boolean {\n return this.listeners.size > 0;\n }\n}\n\nexport { LoopbackSerializationError };\n"],"mappings":"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","names":[]}
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var loopback_serialization_exports = {};
20
+ __export(loopback_serialization_exports, {
21
+ LoopbackSerializationError: () => LoopbackSerializationError,
22
+ assertLoopbackSerializable: () => assertLoopbackSerializable
23
+ });
24
+ module.exports = __toCommonJS(loopback_serialization_exports);
25
+ class LoopbackSerializationError extends Error {
26
+ constructor(message, path) {
27
+ super(message);
28
+ this.path = path;
29
+ this.name = "LoopbackSerializationError";
30
+ }
31
+ }
32
+ function describe(value) {
33
+ if (value === null) return "null";
34
+ if (Array.isArray(value)) return "array";
35
+ const t = typeof value;
36
+ if (t === "object") {
37
+ const ctor = value.constructor;
38
+ return ctor && ctor.name ? `${ctor.name} instance` : "object";
39
+ }
40
+ return t;
41
+ }
42
+ function isPlainObject(value) {
43
+ const proto = Object.getPrototypeOf(value);
44
+ return proto === Object.prototype || proto === null;
45
+ }
46
+ function reject(reason, path) {
47
+ throw new LoopbackSerializationError(
48
+ `Loopback transport cannot faithfully serialize value at ${path}: ${reason}. The loopback transport uses JSON; use InProcessRouterTransport (or the real Electron transport) for values that require structured clone.`,
49
+ path
50
+ );
51
+ }
52
+ function assertNode(value, path, seen) {
53
+ if (value === null) return;
54
+ const t = typeof value;
55
+ if (t === "string" || t === "boolean") return;
56
+ if (t === "number") {
57
+ if (!Number.isFinite(value)) {
58
+ reject("non-finite number (NaN/Infinity) \u2014 JSON coerces it to null", path);
59
+ }
60
+ return;
61
+ }
62
+ if (t === "undefined") {
63
+ reject("undefined \u2014 JSON drops it silently", path);
64
+ }
65
+ if (t === "bigint") {
66
+ reject("bigint \u2014 JSON cannot represent it", path);
67
+ }
68
+ if (t === "function") {
69
+ reject("function \u2014 JSON drops it silently", path);
70
+ }
71
+ if (t === "symbol") {
72
+ reject("symbol \u2014 JSON drops it silently", path);
73
+ }
74
+ const obj = value;
75
+ if (seen.has(obj)) {
76
+ reject("circular reference \u2014 JSON cannot represent it", path);
77
+ }
78
+ if (value instanceof Date) {
79
+ reject("Date \u2014 JSON coerces it to a string, losing the Date type", path);
80
+ }
81
+ if (value instanceof RegExp) {
82
+ reject("RegExp \u2014 JSON coerces it to an empty object", path);
83
+ }
84
+ if (value instanceof Map || value instanceof Set) {
85
+ reject(`${describe(value)} \u2014 JSON coerces it to an empty object`, path);
86
+ }
87
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
88
+ reject(`${describe(value)} (binary) \u2014 JSON does not represent binary faithfully`, path);
89
+ }
90
+ seen.add(obj);
91
+ if (Array.isArray(value)) {
92
+ value.forEach((item, index) => assertNode(item, `${path}[${index}]`, seen));
93
+ seen.delete(obj);
94
+ return;
95
+ }
96
+ if (!isPlainObject(obj)) {
97
+ reject(
98
+ `${describe(value)} \u2014 only plain objects survive JSON; class instances collapse to plain objects`,
99
+ path
100
+ );
101
+ }
102
+ for (const [key, child] of Object.entries(value)) {
103
+ assertNode(child, path === "root" ? key : `${path}.${key}`, seen);
104
+ }
105
+ seen.delete(obj);
106
+ }
107
+ function assertLoopbackSerializable(value, label = "root") {
108
+ if (value === void 0) return;
109
+ assertNode(value, label, /* @__PURE__ */ new WeakSet());
110
+ }
111
+ // Annotate the CommonJS export names for ESM import in node:
112
+ 0 && (module.exports = {
113
+ LoopbackSerializationError,
114
+ assertLoopbackSerializable
115
+ });
116
+ //# sourceMappingURL=loopback_serialization.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transport/loopback_serialization.ts"],"sourcesContent":["/**\n * Strict JSON serialization guard for the loopback transport.\n *\n * The loopback transport moves payloads over HTTP as JSON. JSON cannot\n * faithfully represent many values that Electron's structured-clone IPC can\n * (or that it round-trips differently): `Date` becomes a string, `undefined`\n * keys silently vanish, `NaN`/`Infinity` become `null`, binary is lost, class\n * instances collapse to plain objects. Electron structured-clone is the\n * ground-truth contract; rather than let the loopback transport silently\n * diverge from it, we REJECT anything JSON would coerce or drop, with a clear,\n * path-annotated error.\n *\n * This is deliberately stricter than `JSON.stringify` (which merely drops\n * `undefined`/functions and throws only on `bigint`): faithful representation,\n * not \"did not crash\", is the bar.\n */\n\nexport class LoopbackSerializationError extends Error {\n constructor(\n message: string,\n readonly path: string,\n ) {\n super(message);\n this.name = 'LoopbackSerializationError';\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n const t = typeof value;\n if (t === 'object') {\n const ctor = (value as object).constructor;\n return ctor && ctor.name ? `${ctor.name} instance` : 'object';\n }\n return t;\n}\n\nfunction isPlainObject(value: object): boolean {\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction reject(reason: string, path: string): never {\n throw new LoopbackSerializationError(\n `Loopback transport cannot faithfully serialize value at ${path}: ${reason}. ` +\n `The loopback transport uses JSON; use InProcessRouterTransport (or the real ` +\n `Electron transport) for values that require structured clone.`,\n path,\n );\n}\n\nfunction assertNode(value: unknown, path: string, seen: WeakSet<object>): void {\n if (value === null) return;\n\n const t = typeof value;\n\n if (t === 'string' || t === 'boolean') return;\n\n if (t === 'number') {\n if (!Number.isFinite(value as number)) {\n reject('non-finite number (NaN/Infinity) — JSON coerces it to null', path);\n }\n return;\n }\n\n if (t === 'undefined') {\n reject('undefined — JSON drops it silently', path);\n }\n if (t === 'bigint') {\n reject('bigint — JSON cannot represent it', path);\n }\n if (t === 'function') {\n reject('function — JSON drops it silently', path);\n }\n if (t === 'symbol') {\n reject('symbol — JSON drops it silently', path);\n }\n\n // From here, t === 'object'.\n const obj = value as object;\n\n if (seen.has(obj)) {\n reject('circular reference — JSON cannot represent it', path);\n }\n\n if (value instanceof Date) {\n reject('Date — JSON coerces it to a string, losing the Date type', path);\n }\n if (value instanceof RegExp) {\n reject('RegExp — JSON coerces it to an empty object', path);\n }\n if (value instanceof Map || value instanceof Set) {\n reject(`${describe(value)} — JSON coerces it to an empty object`, path);\n }\n if (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))\n ) {\n reject(`${describe(value)} (binary) — JSON does not represent binary faithfully`, path);\n }\n\n seen.add(obj);\n\n if (Array.isArray(value)) {\n value.forEach((item, index) => assertNode(item, `${path}[${index}]`, seen));\n seen.delete(obj);\n return;\n }\n\n if (!isPlainObject(obj)) {\n reject(\n `${describe(value)} — only plain objects survive JSON; class instances collapse to plain objects`,\n path,\n );\n }\n\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n assertNode(child, path === 'root' ? key : `${path}.${key}`, seen);\n }\n seen.delete(obj);\n}\n\n/**\n * Throw `LoopbackSerializationError` if `value` cannot be faithfully carried\n * over the JSON loopback transport. A top-level `undefined` is allowed because\n * it means \"no payload\" for inputless operations; nested `undefined` is not.\n */\nexport function assertLoopbackSerializable(value: unknown, label = 'root'): void {\n if (value === undefined) return; // \"no payload\" sentinel at the top level only.\n assertNode(value, label, new WeakSet<object>());\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBO,MAAM,mCAAmC,MAAM;AAAA,EACpD,YACE,SACS,MACT;AACA,UAAM,OAAO;AAFJ;AAGT,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,UAAU;AAClB,UAAM,OAAQ,MAAiB;AAC/B,WAAO,QAAQ,KAAK,OAAO,GAAG,KAAK,IAAI,cAAc;AAAA,EACvD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAwB;AAC7C,QAAM,QAAQ,OAAO,eAAe,KAAK;AACzC,SAAO,UAAU,OAAO,aAAa,UAAU;AACjD;AAEA,SAAS,OAAO,QAAgB,MAAqB;AACnD,QAAM,IAAI;AAAA,IACR,2DAA2D,IAAI,KAAK,MAAM;AAAA,IAG1E;AAAA,EACF;AACF;AAEA,SAAS,WAAW,OAAgB,MAAc,MAA6B;AAC7E,MAAI,UAAU,KAAM;AAEpB,QAAM,IAAI,OAAO;AAEjB,MAAI,MAAM,YAAY,MAAM,UAAW;AAEvC,MAAI,MAAM,UAAU;AAClB,QAAI,CAAC,OAAO,SAAS,KAAe,GAAG;AACrC,aAAO,mEAA8D,IAAI;AAAA,IAC3E;AACA;AAAA,EACF;AAEA,MAAI,MAAM,aAAa;AACrB,WAAO,2CAAsC,IAAI;AAAA,EACnD;AACA,MAAI,MAAM,UAAU;AAClB,WAAO,0CAAqC,IAAI;AAAA,EAClD;AACA,MAAI,MAAM,YAAY;AACpB,WAAO,0CAAqC,IAAI;AAAA,EAClD;AACA,MAAI,MAAM,UAAU;AAClB,WAAO,wCAAmC,IAAI;AAAA,EAChD;AAGA,QAAM,MAAM;AAEZ,MAAI,KAAK,IAAI,GAAG,GAAG;AACjB,WAAO,sDAAiD,IAAI;AAAA,EAC9D;AAEA,MAAI,iBAAiB,MAAM;AACzB,WAAO,iEAA4D,IAAI;AAAA,EACzE;AACA,MAAI,iBAAiB,QAAQ;AAC3B,WAAO,oDAA+C,IAAI;AAAA,EAC5D;AACA,MAAI,iBAAiB,OAAO,iBAAiB,KAAK;AAChD,WAAO,GAAG,SAAS,KAAK,CAAC,8CAAyC,IAAI;AAAA,EACxE;AACA,MACE,YAAY,OAAO,KAAK,KACxB,iBAAiB,eAChB,OAAO,WAAW,eAAe,OAAO,SAAS,KAAK,GACvD;AACA,WAAO,GAAG,SAAS,KAAK,CAAC,8DAAyD,IAAI;AAAA,EACxF;AAEA,OAAK,IAAI,GAAG;AAEZ,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAM,QAAQ,CAAC,MAAM,UAAU,WAAW,MAAM,GAAG,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1E,SAAK,OAAO,GAAG;AACf;AAAA,EACF;AAEA,MAAI,CAAC,cAAc,GAAG,GAAG;AACvB;AAAA,MACE,GAAG,SAAS,KAAK,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAgC,GAAG;AAC3E,eAAW,OAAO,SAAS,SAAS,MAAM,GAAG,IAAI,IAAI,GAAG,IAAI,IAAI;AAAA,EAClE;AACA,OAAK,OAAO,GAAG;AACjB;AAOO,SAAS,2BAA2B,OAAgB,QAAQ,QAAc;AAC/E,MAAI,UAAU,OAAW;AACzB,aAAW,OAAO,OAAO,oBAAI,QAAgB,CAAC;AAChD;","names":[]}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Strict JSON serialization guard for the loopback transport.
3
+ *
4
+ * The loopback transport moves payloads over HTTP as JSON. JSON cannot
5
+ * faithfully represent many values that Electron's structured-clone IPC can
6
+ * (or that it round-trips differently): `Date` becomes a string, `undefined`
7
+ * keys silently vanish, `NaN`/`Infinity` become `null`, binary is lost, class
8
+ * instances collapse to plain objects. Electron structured-clone is the
9
+ * ground-truth contract; rather than let the loopback transport silently
10
+ * diverge from it, we REJECT anything JSON would coerce or drop, with a clear,
11
+ * path-annotated error.
12
+ *
13
+ * This is deliberately stricter than `JSON.stringify` (which merely drops
14
+ * `undefined`/functions and throws only on `bigint`): faithful representation,
15
+ * not "did not crash", is the bar.
16
+ */
17
+ declare class LoopbackSerializationError extends Error {
18
+ readonly path: string;
19
+ constructor(message: string, path: string);
20
+ }
21
+ /**
22
+ * Throw `LoopbackSerializationError` if `value` cannot be faithfully carried
23
+ * over the JSON loopback transport. A top-level `undefined` is allowed because
24
+ * it means "no payload" for inputless operations; nested `undefined` is not.
25
+ */
26
+ declare function assertLoopbackSerializable(value: unknown, label?: string): void;
27
+
28
+ export { LoopbackSerializationError, assertLoopbackSerializable };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Strict JSON serialization guard for the loopback transport.
3
+ *
4
+ * The loopback transport moves payloads over HTTP as JSON. JSON cannot
5
+ * faithfully represent many values that Electron's structured-clone IPC can
6
+ * (or that it round-trips differently): `Date` becomes a string, `undefined`
7
+ * keys silently vanish, `NaN`/`Infinity` become `null`, binary is lost, class
8
+ * instances collapse to plain objects. Electron structured-clone is the
9
+ * ground-truth contract; rather than let the loopback transport silently
10
+ * diverge from it, we REJECT anything JSON would coerce or drop, with a clear,
11
+ * path-annotated error.
12
+ *
13
+ * This is deliberately stricter than `JSON.stringify` (which merely drops
14
+ * `undefined`/functions and throws only on `bigint`): faithful representation,
15
+ * not "did not crash", is the bar.
16
+ */
17
+ declare class LoopbackSerializationError extends Error {
18
+ readonly path: string;
19
+ constructor(message: string, path: string);
20
+ }
21
+ /**
22
+ * Throw `LoopbackSerializationError` if `value` cannot be faithfully carried
23
+ * over the JSON loopback transport. A top-level `undefined` is allowed because
24
+ * it means "no payload" for inputless operations; nested `undefined` is not.
25
+ */
26
+ declare function assertLoopbackSerializable(value: unknown, label?: string): void;
27
+
28
+ export { LoopbackSerializationError, assertLoopbackSerializable };
@@ -0,0 +1,91 @@
1
+ class LoopbackSerializationError extends Error {
2
+ constructor(message, path) {
3
+ super(message);
4
+ this.path = path;
5
+ this.name = "LoopbackSerializationError";
6
+ }
7
+ }
8
+ function describe(value) {
9
+ if (value === null) return "null";
10
+ if (Array.isArray(value)) return "array";
11
+ const t = typeof value;
12
+ if (t === "object") {
13
+ const ctor = value.constructor;
14
+ return ctor && ctor.name ? `${ctor.name} instance` : "object";
15
+ }
16
+ return t;
17
+ }
18
+ function isPlainObject(value) {
19
+ const proto = Object.getPrototypeOf(value);
20
+ return proto === Object.prototype || proto === null;
21
+ }
22
+ function reject(reason, path) {
23
+ throw new LoopbackSerializationError(
24
+ `Loopback transport cannot faithfully serialize value at ${path}: ${reason}. The loopback transport uses JSON; use InProcessRouterTransport (or the real Electron transport) for values that require structured clone.`,
25
+ path
26
+ );
27
+ }
28
+ function assertNode(value, path, seen) {
29
+ if (value === null) return;
30
+ const t = typeof value;
31
+ if (t === "string" || t === "boolean") return;
32
+ if (t === "number") {
33
+ if (!Number.isFinite(value)) {
34
+ reject("non-finite number (NaN/Infinity) \u2014 JSON coerces it to null", path);
35
+ }
36
+ return;
37
+ }
38
+ if (t === "undefined") {
39
+ reject("undefined \u2014 JSON drops it silently", path);
40
+ }
41
+ if (t === "bigint") {
42
+ reject("bigint \u2014 JSON cannot represent it", path);
43
+ }
44
+ if (t === "function") {
45
+ reject("function \u2014 JSON drops it silently", path);
46
+ }
47
+ if (t === "symbol") {
48
+ reject("symbol \u2014 JSON drops it silently", path);
49
+ }
50
+ const obj = value;
51
+ if (seen.has(obj)) {
52
+ reject("circular reference \u2014 JSON cannot represent it", path);
53
+ }
54
+ if (value instanceof Date) {
55
+ reject("Date \u2014 JSON coerces it to a string, losing the Date type", path);
56
+ }
57
+ if (value instanceof RegExp) {
58
+ reject("RegExp \u2014 JSON coerces it to an empty object", path);
59
+ }
60
+ if (value instanceof Map || value instanceof Set) {
61
+ reject(`${describe(value)} \u2014 JSON coerces it to an empty object`, path);
62
+ }
63
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
64
+ reject(`${describe(value)} (binary) \u2014 JSON does not represent binary faithfully`, path);
65
+ }
66
+ seen.add(obj);
67
+ if (Array.isArray(value)) {
68
+ value.forEach((item, index) => assertNode(item, `${path}[${index}]`, seen));
69
+ seen.delete(obj);
70
+ return;
71
+ }
72
+ if (!isPlainObject(obj)) {
73
+ reject(
74
+ `${describe(value)} \u2014 only plain objects survive JSON; class instances collapse to plain objects`,
75
+ path
76
+ );
77
+ }
78
+ for (const [key, child] of Object.entries(value)) {
79
+ assertNode(child, path === "root" ? key : `${path}.${key}`, seen);
80
+ }
81
+ seen.delete(obj);
82
+ }
83
+ function assertLoopbackSerializable(value, label = "root") {
84
+ if (value === void 0) return;
85
+ assertNode(value, label, /* @__PURE__ */ new WeakSet());
86
+ }
87
+ export {
88
+ LoopbackSerializationError,
89
+ assertLoopbackSerializable
90
+ };
91
+ //# sourceMappingURL=loopback_serialization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transport/loopback_serialization.ts"],"sourcesContent":["/**\n * Strict JSON serialization guard for the loopback transport.\n *\n * The loopback transport moves payloads over HTTP as JSON. JSON cannot\n * faithfully represent many values that Electron's structured-clone IPC can\n * (or that it round-trips differently): `Date` becomes a string, `undefined`\n * keys silently vanish, `NaN`/`Infinity` become `null`, binary is lost, class\n * instances collapse to plain objects. Electron structured-clone is the\n * ground-truth contract; rather than let the loopback transport silently\n * diverge from it, we REJECT anything JSON would coerce or drop, with a clear,\n * path-annotated error.\n *\n * This is deliberately stricter than `JSON.stringify` (which merely drops\n * `undefined`/functions and throws only on `bigint`): faithful representation,\n * not \"did not crash\", is the bar.\n */\n\nexport class LoopbackSerializationError extends Error {\n constructor(\n message: string,\n readonly path: string,\n ) {\n super(message);\n this.name = 'LoopbackSerializationError';\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n const t = typeof value;\n if (t === 'object') {\n const ctor = (value as object).constructor;\n return ctor && ctor.name ? `${ctor.name} instance` : 'object';\n }\n return t;\n}\n\nfunction isPlainObject(value: object): boolean {\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction reject(reason: string, path: string): never {\n throw new LoopbackSerializationError(\n `Loopback transport cannot faithfully serialize value at ${path}: ${reason}. ` +\n `The loopback transport uses JSON; use InProcessRouterTransport (or the real ` +\n `Electron transport) for values that require structured clone.`,\n path,\n );\n}\n\nfunction assertNode(value: unknown, path: string, seen: WeakSet<object>): void {\n if (value === null) return;\n\n const t = typeof value;\n\n if (t === 'string' || t === 'boolean') return;\n\n if (t === 'number') {\n if (!Number.isFinite(value as number)) {\n reject('non-finite number (NaN/Infinity) — JSON coerces it to null', path);\n }\n return;\n }\n\n if (t === 'undefined') {\n reject('undefined — JSON drops it silently', path);\n }\n if (t === 'bigint') {\n reject('bigint — JSON cannot represent it', path);\n }\n if (t === 'function') {\n reject('function — JSON drops it silently', path);\n }\n if (t === 'symbol') {\n reject('symbol — JSON drops it silently', path);\n }\n\n // From here, t === 'object'.\n const obj = value as object;\n\n if (seen.has(obj)) {\n reject('circular reference — JSON cannot represent it', path);\n }\n\n if (value instanceof Date) {\n reject('Date — JSON coerces it to a string, losing the Date type', path);\n }\n if (value instanceof RegExp) {\n reject('RegExp — JSON coerces it to an empty object', path);\n }\n if (value instanceof Map || value instanceof Set) {\n reject(`${describe(value)} — JSON coerces it to an empty object`, path);\n }\n if (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))\n ) {\n reject(`${describe(value)} (binary) — JSON does not represent binary faithfully`, path);\n }\n\n seen.add(obj);\n\n if (Array.isArray(value)) {\n value.forEach((item, index) => assertNode(item, `${path}[${index}]`, seen));\n seen.delete(obj);\n return;\n }\n\n if (!isPlainObject(obj)) {\n reject(\n `${describe(value)} — only plain objects survive JSON; class instances collapse to plain objects`,\n path,\n );\n }\n\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n assertNode(child, path === 'root' ? key : `${path}.${key}`, seen);\n }\n seen.delete(obj);\n}\n\n/**\n * Throw `LoopbackSerializationError` if `value` cannot be faithfully carried\n * over the JSON loopback transport. A top-level `undefined` is allowed because\n * it means \"no payload\" for inputless operations; nested `undefined` is not.\n */\nexport function assertLoopbackSerializable(value: unknown, label = 'root'): void {\n if (value === undefined) return; // \"no payload\" sentinel at the top level only.\n assertNode(value, label, new WeakSet<object>());\n}\n"],"mappings":"AAiBO,MAAM,mCAAmC,MAAM;AAAA,EACpD,YACE,SACS,MACT;AACA,UAAM,OAAO;AAFJ;AAGT,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,UAAU;AAClB,UAAM,OAAQ,MAAiB;AAC/B,WAAO,QAAQ,KAAK,OAAO,GAAG,KAAK,IAAI,cAAc;AAAA,EACvD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAwB;AAC7C,QAAM,QAAQ,OAAO,eAAe,KAAK;AACzC,SAAO,UAAU,OAAO,aAAa,UAAU;AACjD;AAEA,SAAS,OAAO,QAAgB,MAAqB;AACnD,QAAM,IAAI;AAAA,IACR,2DAA2D,IAAI,KAAK,MAAM;AAAA,IAG1E;AAAA,EACF;AACF;AAEA,SAAS,WAAW,OAAgB,MAAc,MAA6B;AAC7E,MAAI,UAAU,KAAM;AAEpB,QAAM,IAAI,OAAO;AAEjB,MAAI,MAAM,YAAY,MAAM,UAAW;AAEvC,MAAI,MAAM,UAAU;AAClB,QAAI,CAAC,OAAO,SAAS,KAAe,GAAG;AACrC,aAAO,mEAA8D,IAAI;AAAA,IAC3E;AACA;AAAA,EACF;AAEA,MAAI,MAAM,aAAa;AACrB,WAAO,2CAAsC,IAAI;AAAA,EACnD;AACA,MAAI,MAAM,UAAU;AAClB,WAAO,0CAAqC,IAAI;AAAA,EAClD;AACA,MAAI,MAAM,YAAY;AACpB,WAAO,0CAAqC,IAAI;AAAA,EAClD;AACA,MAAI,MAAM,UAAU;AAClB,WAAO,wCAAmC,IAAI;AAAA,EAChD;AAGA,QAAM,MAAM;AAEZ,MAAI,KAAK,IAAI,GAAG,GAAG;AACjB,WAAO,sDAAiD,IAAI;AAAA,EAC9D;AAEA,MAAI,iBAAiB,MAAM;AACzB,WAAO,iEAA4D,IAAI;AAAA,EACzE;AACA,MAAI,iBAAiB,QAAQ;AAC3B,WAAO,oDAA+C,IAAI;AAAA,EAC5D;AACA,MAAI,iBAAiB,OAAO,iBAAiB,KAAK;AAChD,WAAO,GAAG,SAAS,KAAK,CAAC,8CAAyC,IAAI;AAAA,EACxE;AACA,MACE,YAAY,OAAO,KAAK,KACxB,iBAAiB,eAChB,OAAO,WAAW,eAAe,OAAO,SAAS,KAAK,GACvD;AACA,WAAO,GAAG,SAAS,KAAK,CAAC,8DAAyD,IAAI;AAAA,EACxF;AAEA,OAAK,IAAI,GAAG;AAEZ,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAM,QAAQ,CAAC,MAAM,UAAU,WAAW,MAAM,GAAG,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC;AAC1E,SAAK,OAAO,GAAG;AACf;AAAA,EACF;AAEA,MAAI,CAAC,cAAc,GAAG,GAAG;AACvB;AAAA,MACE,GAAG,SAAS,KAAK,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAgC,GAAG;AAC3E,eAAW,OAAO,SAAS,SAAS,MAAM,GAAG,IAAI,IAAI,GAAG,IAAI,IAAI;AAAA,EAClE;AACA,OAAK,OAAO,GAAG;AACjB;AAOO,SAAS,2BAA2B,OAAgB,QAAQ,QAAc;AAC/E,MAAI,UAAU,OAAW;AACzB,aAAW,OAAO,OAAO,oBAAI,QAAgB,CAAC;AAChD;","names":[]}
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var transport_contract_suite_exports = {};
20
+ __export(transport_contract_suite_exports, {
21
+ CONTRACT_OPERATIONS_YAML: () => CONTRACT_OPERATIONS_YAML,
22
+ runTransportContractSuite: () => runTransportContractSuite
23
+ });
24
+ module.exports = __toCommonJS(transport_contract_suite_exports);
25
+ var import_vitest = require("vitest");
26
+ const CONTRACT_OPERATIONS_YAML = `
27
+ operations:
28
+ test.hello:
29
+ controller: test
30
+ action: hello
31
+ chat.send:
32
+ controller: chat
33
+ action: sendMessage
34
+ input:
35
+ type: object
36
+ additionalProperties: false
37
+ required: [message]
38
+ properties:
39
+ message:
40
+ type: string
41
+ `;
42
+ async function waitFor(predicate, timeoutMs = 1e3) {
43
+ const deadline = Date.now() + timeoutMs;
44
+ while (!predicate()) {
45
+ if (Date.now() > deadline) {
46
+ throw new Error("waitFor timed out");
47
+ }
48
+ await new Promise((resolve) => setTimeout(resolve, 5));
49
+ }
50
+ }
51
+ function runTransportContractSuite(name, createSetup) {
52
+ (0, import_vitest.describe)(`WoodRpcTransport contract: ${name}`, () => {
53
+ let setup;
54
+ (0, import_vitest.beforeEach)(async () => {
55
+ setup = await createSetup();
56
+ });
57
+ (0, import_vitest.afterEach)(async () => {
58
+ await setup.destroy();
59
+ });
60
+ (0, import_vitest.test)("invokes an inputless operation", async () => {
61
+ const result = await setup.transport.invoke("test.hello");
62
+ (0, import_vitest.expect)(result).toEqual({ greeting: "Hello from test controller" });
63
+ });
64
+ (0, import_vitest.test)("invokes an input-bearing operation", async () => {
65
+ const result = await setup.transport.invoke("chat.send", { message: "Hi" });
66
+ (0, import_vitest.expect)(result).toMatchObject({ reply: "Echo: Hi" });
67
+ });
68
+ (0, import_vitest.test)("rejects an unknown/additional property via router schema validation", async () => {
69
+ await (0, import_vitest.expect)(
70
+ setup.transport.invoke("chat.send", { message: "Hi", bogus: true })
71
+ ).rejects.toThrow(/validation/i);
72
+ });
73
+ (0, import_vitest.test)("propagates controller/router errors as rejected promises", async () => {
74
+ await (0, import_vitest.expect)(setup.transport.invoke("does.not.exist")).rejects.toThrow(
75
+ /unknown operation/i
76
+ );
77
+ });
78
+ (0, import_vitest.test)("delivers push events to a subscriber and stops after unsubscribe", async () => {
79
+ const received = [];
80
+ const unsubscribe = setup.transport.subscribe("thread.created", (payload) => {
81
+ received.push(payload);
82
+ });
83
+ setup.emit("thread.created", { threadId: 7 });
84
+ await waitFor(() => received.length === 1);
85
+ (0, import_vitest.expect)(received[0]).toEqual({ threadId: 7 });
86
+ unsubscribe();
87
+ setup.emit("thread.created", { threadId: 8 });
88
+ await new Promise((resolve) => setTimeout(resolve, 40));
89
+ (0, import_vitest.expect)(received).toHaveLength(1);
90
+ });
91
+ (0, import_vitest.test)("supports multiple independent subscribers on one channel", async () => {
92
+ const a = [];
93
+ const b = [];
94
+ const unsubA = setup.transport.subscribe("thread.updated", (p) => a.push(p));
95
+ const unsubB = setup.transport.subscribe("thread.updated", (p) => b.push(p));
96
+ setup.emit("thread.updated", { threadId: 1 });
97
+ await waitFor(() => a.length === 1 && b.length === 1);
98
+ (0, import_vitest.expect)(a[0]).toEqual({ threadId: 1 });
99
+ (0, import_vitest.expect)(b[0]).toEqual({ threadId: 1 });
100
+ unsubA();
101
+ unsubB();
102
+ });
103
+ });
104
+ }
105
+ // Annotate the CommonJS export names for ESM import in node:
106
+ 0 && (module.exports = {
107
+ CONTRACT_OPERATIONS_YAML,
108
+ runTransportContractSuite
109
+ });
110
+ //# sourceMappingURL=transport_contract_suite.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transport/transport_contract_suite.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, test } from 'vitest';\nimport type { WoodRpcTransport } from './wood_rpc_transport.js';\n\n/**\n * The serialization/transport contract shared by every `WoodRpcTransport`.\n *\n * This module is intentionally NOT a `*.test.ts` file: it defines a reusable\n * suite that each transport's own test file runs, so `InProcessRouterTransport`\n * and `LoopbackRemoteTransport` are held to byte-identical behavior. Only the\n * loopback JSON edge-case rejections differ, and those live in the loopback\n * test file.\n */\n\n/** Operations used by the contract suite. `chat.send` locks `additionalProperties: false`. */\nexport const CONTRACT_OPERATIONS_YAML = `\noperations:\n test.hello:\n controller: test\n action: hello\n chat.send:\n controller: chat\n action: sendMessage\n input:\n type: object\n additionalProperties: false\n required: [message]\n properties:\n message:\n type: string\n`;\n\nexport interface TransportContractSetup {\n transport: WoodRpcTransport;\n /** Simulate a `WindowEventService.send(channel, data)` push (prefix added internally). */\n emit: (channel: string, data: unknown) => void;\n destroy: () => Promise<void> | void;\n}\n\nexport type TransportContractFactory = () =>\n | Promise<TransportContractSetup>\n | TransportContractSetup;\n\nasync function waitFor(predicate: () => boolean, timeoutMs = 1000): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (!predicate()) {\n if (Date.now() > deadline) {\n throw new Error('waitFor timed out');\n }\n await new Promise((resolve) => setTimeout(resolve, 5));\n }\n}\n\nexport function runTransportContractSuite(\n name: string,\n createSetup: TransportContractFactory,\n): void {\n describe(`WoodRpcTransport contract: ${name}`, () => {\n let setup: TransportContractSetup;\n\n beforeEach(async () => {\n setup = await createSetup();\n });\n\n afterEach(async () => {\n await setup.destroy();\n });\n\n test('invokes an inputless operation', async () => {\n const result = await setup.transport.invoke('test.hello');\n expect(result).toEqual({ greeting: 'Hello from test controller' });\n });\n\n test('invokes an input-bearing operation', async () => {\n const result = await setup.transport.invoke('chat.send', { message: 'Hi' });\n expect(result).toMatchObject({ reply: 'Echo: Hi' });\n });\n\n test('rejects an unknown/additional property via router schema validation', async () => {\n await expect(\n setup.transport.invoke('chat.send', { message: 'Hi', bogus: true }),\n ).rejects.toThrow(/validation/i);\n });\n\n test('propagates controller/router errors as rejected promises', async () => {\n await expect(setup.transport.invoke('does.not.exist')).rejects.toThrow(\n /unknown operation/i,\n );\n });\n\n test('delivers push events to a subscriber and stops after unsubscribe', async () => {\n const received: unknown[] = [];\n const unsubscribe = setup.transport.subscribe('thread.created', (payload) => {\n received.push(payload);\n });\n\n setup.emit('thread.created', { threadId: 7 });\n await waitFor(() => received.length === 1);\n expect(received[0]).toEqual({ threadId: 7 });\n\n unsubscribe();\n setup.emit('thread.created', { threadId: 8 });\n await new Promise((resolve) => setTimeout(resolve, 40));\n expect(received).toHaveLength(1);\n });\n\n test('supports multiple independent subscribers on one channel', async () => {\n const a: unknown[] = [];\n const b: unknown[] = [];\n const unsubA = setup.transport.subscribe('thread.updated', (p) => a.push(p));\n const unsubB = setup.transport.subscribe('thread.updated', (p) => b.push(p));\n\n setup.emit('thread.updated', { threadId: 1 });\n await waitFor(() => a.length === 1 && b.length === 1);\n expect(a[0]).toEqual({ threadId: 1 });\n expect(b[0]).toEqual({ threadId: 1 });\n\n unsubA();\n unsubB();\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA8D;AAcvD,MAAM,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BxC,eAAe,QAAQ,WAA0B,YAAY,KAAqB;AAChF,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,CAAC,UAAU,GAAG;AACnB,QAAI,KAAK,IAAI,IAAI,UAAU;AACzB,YAAM,IAAI,MAAM,mBAAmB;AAAA,IACrC;AACA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAAA,EACvD;AACF;AAEO,SAAS,0BACd,MACA,aACM;AACN,8BAAS,8BAA8B,IAAI,IAAI,MAAM;AACnD,QAAI;AAEJ,kCAAW,YAAY;AACrB,cAAQ,MAAM,YAAY;AAAA,IAC5B,CAAC;AAED,iCAAU,YAAY;AACpB,YAAM,MAAM,QAAQ;AAAA,IACtB,CAAC;AAED,4BAAK,kCAAkC,YAAY;AACjD,YAAM,SAAS,MAAM,MAAM,UAAU,OAAO,YAAY;AACxD,gCAAO,MAAM,EAAE,QAAQ,EAAE,UAAU,6BAA6B,CAAC;AAAA,IACnE,CAAC;AAED,4BAAK,sCAAsC,YAAY;AACrD,YAAM,SAAS,MAAM,MAAM,UAAU,OAAO,aAAa,EAAE,SAAS,KAAK,CAAC;AAC1E,gCAAO,MAAM,EAAE,cAAc,EAAE,OAAO,WAAW,CAAC;AAAA,IACpD,CAAC;AAED,4BAAK,uEAAuE,YAAY;AACtF,gBAAM;AAAA,QACJ,MAAM,UAAU,OAAO,aAAa,EAAE,SAAS,MAAM,OAAO,KAAK,CAAC;AAAA,MACpE,EAAE,QAAQ,QAAQ,aAAa;AAAA,IACjC,CAAC;AAED,4BAAK,4DAA4D,YAAY;AAC3E,gBAAM,sBAAO,MAAM,UAAU,OAAO,gBAAgB,CAAC,EAAE,QAAQ;AAAA,QAC7D;AAAA,MACF;AAAA,IACF,CAAC;AAED,4BAAK,oEAAoE,YAAY;AACnF,YAAM,WAAsB,CAAC;AAC7B,YAAM,cAAc,MAAM,UAAU,UAAU,kBAAkB,CAAC,YAAY;AAC3E,iBAAS,KAAK,OAAO;AAAA,MACvB,CAAC;AAED,YAAM,KAAK,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAC5C,YAAM,QAAQ,MAAM,SAAS,WAAW,CAAC;AACzC,gCAAO,SAAS,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAE3C,kBAAY;AACZ,YAAM,KAAK,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAC5C,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACtD,gCAAO,QAAQ,EAAE,aAAa,CAAC;AAAA,IACjC,CAAC;AAED,4BAAK,4DAA4D,YAAY;AAC3E,YAAM,IAAe,CAAC;AACtB,YAAM,IAAe,CAAC;AACtB,YAAM,SAAS,MAAM,UAAU,UAAU,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAC3E,YAAM,SAAS,MAAM,UAAU,UAAU,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE3E,YAAM,KAAK,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAC5C,YAAM,QAAQ,MAAM,EAAE,WAAW,KAAK,EAAE,WAAW,CAAC;AACpD,gCAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AACpC,gCAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAEpC,aAAO;AACP,aAAO;AAAA,IACT,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
@@ -0,0 +1,23 @@
1
+ import { WoodRpcTransport } from './wood_rpc_transport.cjs';
2
+
3
+ /**
4
+ * The serialization/transport contract shared by every `WoodRpcTransport`.
5
+ *
6
+ * This module is intentionally NOT a `*.test.ts` file: it defines a reusable
7
+ * suite that each transport's own test file runs, so `InProcessRouterTransport`
8
+ * and `LoopbackRemoteTransport` are held to byte-identical behavior. Only the
9
+ * loopback JSON edge-case rejections differ, and those live in the loopback
10
+ * test file.
11
+ */
12
+ /** Operations used by the contract suite. `chat.send` locks `additionalProperties: false`. */
13
+ declare const CONTRACT_OPERATIONS_YAML = "\noperations:\n test.hello:\n controller: test\n action: hello\n chat.send:\n controller: chat\n action: sendMessage\n input:\n type: object\n additionalProperties: false\n required: [message]\n properties:\n message:\n type: string\n";
14
+ interface TransportContractSetup {
15
+ transport: WoodRpcTransport;
16
+ /** Simulate a `WindowEventService.send(channel, data)` push (prefix added internally). */
17
+ emit: (channel: string, data: unknown) => void;
18
+ destroy: () => Promise<void> | void;
19
+ }
20
+ type TransportContractFactory = () => Promise<TransportContractSetup> | TransportContractSetup;
21
+ declare function runTransportContractSuite(name: string, createSetup: TransportContractFactory): void;
22
+
23
+ export { CONTRACT_OPERATIONS_YAML, type TransportContractFactory, type TransportContractSetup, runTransportContractSuite };
@@ -0,0 +1,23 @@
1
+ import { WoodRpcTransport } from './wood_rpc_transport.js';
2
+
3
+ /**
4
+ * The serialization/transport contract shared by every `WoodRpcTransport`.
5
+ *
6
+ * This module is intentionally NOT a `*.test.ts` file: it defines a reusable
7
+ * suite that each transport's own test file runs, so `InProcessRouterTransport`
8
+ * and `LoopbackRemoteTransport` are held to byte-identical behavior. Only the
9
+ * loopback JSON edge-case rejections differ, and those live in the loopback
10
+ * test file.
11
+ */
12
+ /** Operations used by the contract suite. `chat.send` locks `additionalProperties: false`. */
13
+ declare const CONTRACT_OPERATIONS_YAML = "\noperations:\n test.hello:\n controller: test\n action: hello\n chat.send:\n controller: chat\n action: sendMessage\n input:\n type: object\n additionalProperties: false\n required: [message]\n properties:\n message:\n type: string\n";
14
+ interface TransportContractSetup {
15
+ transport: WoodRpcTransport;
16
+ /** Simulate a `WindowEventService.send(channel, data)` push (prefix added internally). */
17
+ emit: (channel: string, data: unknown) => void;
18
+ destroy: () => Promise<void> | void;
19
+ }
20
+ type TransportContractFactory = () => Promise<TransportContractSetup> | TransportContractSetup;
21
+ declare function runTransportContractSuite(name: string, createSetup: TransportContractFactory): void;
22
+
23
+ export { CONTRACT_OPERATIONS_YAML, type TransportContractFactory, type TransportContractSetup, runTransportContractSuite };
@@ -0,0 +1,85 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ const CONTRACT_OPERATIONS_YAML = `
3
+ operations:
4
+ test.hello:
5
+ controller: test
6
+ action: hello
7
+ chat.send:
8
+ controller: chat
9
+ action: sendMessage
10
+ input:
11
+ type: object
12
+ additionalProperties: false
13
+ required: [message]
14
+ properties:
15
+ message:
16
+ type: string
17
+ `;
18
+ async function waitFor(predicate, timeoutMs = 1e3) {
19
+ const deadline = Date.now() + timeoutMs;
20
+ while (!predicate()) {
21
+ if (Date.now() > deadline) {
22
+ throw new Error("waitFor timed out");
23
+ }
24
+ await new Promise((resolve) => setTimeout(resolve, 5));
25
+ }
26
+ }
27
+ function runTransportContractSuite(name, createSetup) {
28
+ describe(`WoodRpcTransport contract: ${name}`, () => {
29
+ let setup;
30
+ beforeEach(async () => {
31
+ setup = await createSetup();
32
+ });
33
+ afterEach(async () => {
34
+ await setup.destroy();
35
+ });
36
+ test("invokes an inputless operation", async () => {
37
+ const result = await setup.transport.invoke("test.hello");
38
+ expect(result).toEqual({ greeting: "Hello from test controller" });
39
+ });
40
+ test("invokes an input-bearing operation", async () => {
41
+ const result = await setup.transport.invoke("chat.send", { message: "Hi" });
42
+ expect(result).toMatchObject({ reply: "Echo: Hi" });
43
+ });
44
+ test("rejects an unknown/additional property via router schema validation", async () => {
45
+ await expect(
46
+ setup.transport.invoke("chat.send", { message: "Hi", bogus: true })
47
+ ).rejects.toThrow(/validation/i);
48
+ });
49
+ test("propagates controller/router errors as rejected promises", async () => {
50
+ await expect(setup.transport.invoke("does.not.exist")).rejects.toThrow(
51
+ /unknown operation/i
52
+ );
53
+ });
54
+ test("delivers push events to a subscriber and stops after unsubscribe", async () => {
55
+ const received = [];
56
+ const unsubscribe = setup.transport.subscribe("thread.created", (payload) => {
57
+ received.push(payload);
58
+ });
59
+ setup.emit("thread.created", { threadId: 7 });
60
+ await waitFor(() => received.length === 1);
61
+ expect(received[0]).toEqual({ threadId: 7 });
62
+ unsubscribe();
63
+ setup.emit("thread.created", { threadId: 8 });
64
+ await new Promise((resolve) => setTimeout(resolve, 40));
65
+ expect(received).toHaveLength(1);
66
+ });
67
+ test("supports multiple independent subscribers on one channel", async () => {
68
+ const a = [];
69
+ const b = [];
70
+ const unsubA = setup.transport.subscribe("thread.updated", (p) => a.push(p));
71
+ const unsubB = setup.transport.subscribe("thread.updated", (p) => b.push(p));
72
+ setup.emit("thread.updated", { threadId: 1 });
73
+ await waitFor(() => a.length === 1 && b.length === 1);
74
+ expect(a[0]).toEqual({ threadId: 1 });
75
+ expect(b[0]).toEqual({ threadId: 1 });
76
+ unsubA();
77
+ unsubB();
78
+ });
79
+ });
80
+ }
81
+ export {
82
+ CONTRACT_OPERATIONS_YAML,
83
+ runTransportContractSuite
84
+ };
85
+ //# sourceMappingURL=transport_contract_suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transport/transport_contract_suite.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, test } from 'vitest';\nimport type { WoodRpcTransport } from './wood_rpc_transport.js';\n\n/**\n * The serialization/transport contract shared by every `WoodRpcTransport`.\n *\n * This module is intentionally NOT a `*.test.ts` file: it defines a reusable\n * suite that each transport's own test file runs, so `InProcessRouterTransport`\n * and `LoopbackRemoteTransport` are held to byte-identical behavior. Only the\n * loopback JSON edge-case rejections differ, and those live in the loopback\n * test file.\n */\n\n/** Operations used by the contract suite. `chat.send` locks `additionalProperties: false`. */\nexport const CONTRACT_OPERATIONS_YAML = `\noperations:\n test.hello:\n controller: test\n action: hello\n chat.send:\n controller: chat\n action: sendMessage\n input:\n type: object\n additionalProperties: false\n required: [message]\n properties:\n message:\n type: string\n`;\n\nexport interface TransportContractSetup {\n transport: WoodRpcTransport;\n /** Simulate a `WindowEventService.send(channel, data)` push (prefix added internally). */\n emit: (channel: string, data: unknown) => void;\n destroy: () => Promise<void> | void;\n}\n\nexport type TransportContractFactory = () =>\n | Promise<TransportContractSetup>\n | TransportContractSetup;\n\nasync function waitFor(predicate: () => boolean, timeoutMs = 1000): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (!predicate()) {\n if (Date.now() > deadline) {\n throw new Error('waitFor timed out');\n }\n await new Promise((resolve) => setTimeout(resolve, 5));\n }\n}\n\nexport function runTransportContractSuite(\n name: string,\n createSetup: TransportContractFactory,\n): void {\n describe(`WoodRpcTransport contract: ${name}`, () => {\n let setup: TransportContractSetup;\n\n beforeEach(async () => {\n setup = await createSetup();\n });\n\n afterEach(async () => {\n await setup.destroy();\n });\n\n test('invokes an inputless operation', async () => {\n const result = await setup.transport.invoke('test.hello');\n expect(result).toEqual({ greeting: 'Hello from test controller' });\n });\n\n test('invokes an input-bearing operation', async () => {\n const result = await setup.transport.invoke('chat.send', { message: 'Hi' });\n expect(result).toMatchObject({ reply: 'Echo: Hi' });\n });\n\n test('rejects an unknown/additional property via router schema validation', async () => {\n await expect(\n setup.transport.invoke('chat.send', { message: 'Hi', bogus: true }),\n ).rejects.toThrow(/validation/i);\n });\n\n test('propagates controller/router errors as rejected promises', async () => {\n await expect(setup.transport.invoke('does.not.exist')).rejects.toThrow(\n /unknown operation/i,\n );\n });\n\n test('delivers push events to a subscriber and stops after unsubscribe', async () => {\n const received: unknown[] = [];\n const unsubscribe = setup.transport.subscribe('thread.created', (payload) => {\n received.push(payload);\n });\n\n setup.emit('thread.created', { threadId: 7 });\n await waitFor(() => received.length === 1);\n expect(received[0]).toEqual({ threadId: 7 });\n\n unsubscribe();\n setup.emit('thread.created', { threadId: 8 });\n await new Promise((resolve) => setTimeout(resolve, 40));\n expect(received).toHaveLength(1);\n });\n\n test('supports multiple independent subscribers on one channel', async () => {\n const a: unknown[] = [];\n const b: unknown[] = [];\n const unsubA = setup.transport.subscribe('thread.updated', (p) => a.push(p));\n const unsubB = setup.transport.subscribe('thread.updated', (p) => b.push(p));\n\n setup.emit('thread.updated', { threadId: 1 });\n await waitFor(() => a.length === 1 && b.length === 1);\n expect(a[0]).toEqual({ threadId: 1 });\n expect(b[0]).toEqual({ threadId: 1 });\n\n unsubA();\n unsubB();\n });\n });\n}\n"],"mappings":"AAAA,SAAS,WAAW,YAAY,UAAU,QAAQ,YAAY;AAcvD,MAAM,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BxC,eAAe,QAAQ,WAA0B,YAAY,KAAqB;AAChF,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,CAAC,UAAU,GAAG;AACnB,QAAI,KAAK,IAAI,IAAI,UAAU;AACzB,YAAM,IAAI,MAAM,mBAAmB;AAAA,IACrC;AACA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAAA,EACvD;AACF;AAEO,SAAS,0BACd,MACA,aACM;AACN,WAAS,8BAA8B,IAAI,IAAI,MAAM;AACnD,QAAI;AAEJ,eAAW,YAAY;AACrB,cAAQ,MAAM,YAAY;AAAA,IAC5B,CAAC;AAED,cAAU,YAAY;AACpB,YAAM,MAAM,QAAQ;AAAA,IACtB,CAAC;AAED,SAAK,kCAAkC,YAAY;AACjD,YAAM,SAAS,MAAM,MAAM,UAAU,OAAO,YAAY;AACxD,aAAO,MAAM,EAAE,QAAQ,EAAE,UAAU,6BAA6B,CAAC;AAAA,IACnE,CAAC;AAED,SAAK,sCAAsC,YAAY;AACrD,YAAM,SAAS,MAAM,MAAM,UAAU,OAAO,aAAa,EAAE,SAAS,KAAK,CAAC;AAC1E,aAAO,MAAM,EAAE,cAAc,EAAE,OAAO,WAAW,CAAC;AAAA,IACpD,CAAC;AAED,SAAK,uEAAuE,YAAY;AACtF,YAAM;AAAA,QACJ,MAAM,UAAU,OAAO,aAAa,EAAE,SAAS,MAAM,OAAO,KAAK,CAAC;AAAA,MACpE,EAAE,QAAQ,QAAQ,aAAa;AAAA,IACjC,CAAC;AAED,SAAK,4DAA4D,YAAY;AAC3E,YAAM,OAAO,MAAM,UAAU,OAAO,gBAAgB,CAAC,EAAE,QAAQ;AAAA,QAC7D;AAAA,MACF;AAAA,IACF,CAAC;AAED,SAAK,oEAAoE,YAAY;AACnF,YAAM,WAAsB,CAAC;AAC7B,YAAM,cAAc,MAAM,UAAU,UAAU,kBAAkB,CAAC,YAAY;AAC3E,iBAAS,KAAK,OAAO;AAAA,MACvB,CAAC;AAED,YAAM,KAAK,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAC5C,YAAM,QAAQ,MAAM,SAAS,WAAW,CAAC;AACzC,aAAO,SAAS,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAE3C,kBAAY;AACZ,YAAM,KAAK,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAC5C,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACtD,aAAO,QAAQ,EAAE,aAAa,CAAC;AAAA,IACjC,CAAC;AAED,SAAK,4DAA4D,YAAY;AAC3E,YAAM,IAAe,CAAC;AACtB,YAAM,IAAe,CAAC;AACtB,YAAM,SAAS,MAAM,UAAU,UAAU,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAC3E,YAAM,SAAS,MAAM,UAAU,UAAU,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAE3E,YAAM,KAAK,kBAAkB,EAAE,UAAU,EAAE,CAAC;AAC5C,YAAM,QAAQ,MAAM,EAAE,WAAW,KAAK,EAAE,WAAW,CAAC;AACpD,aAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AACpC,aAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAEpC,aAAO;AACP,aAAO;AAAA,IACT,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+ var wood_rpc_transport_exports = {};
16
+ module.exports = __toCommonJS(wood_rpc_transport_exports);
17
+ //# sourceMappingURL=wood_rpc_transport.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/transport/wood_rpc_transport.ts"],"sourcesContent":["/**\n * The single seam every test-mode RPC path is built on.\n *\n * Production uses the Electron preload bridge (`ipcRenderer.invoke` +\n * `ipcRenderer.on('__event:...')`). Tests use one of the in-process or loopback\n * implementations. The generated bridge client is built over whichever transport\n * is supplied, so application code never knows which one is active.\n *\n * IMPORTANT (fast-tier isolation): this module and `InProcessRouterTransport`\n * must never import `electron` or `playwright`. Keep this file dependency-free.\n */\nexport interface WoodRpcTransport {\n /**\n * Dispatch a request/response call to a channel. `payload` is optional so\n * inputless operations can be called as `invoke(channel)`.\n */\n invoke(channel: string, payload?: unknown): Promise<unknown>;\n\n /**\n * Subscribe to a main-process push channel (the `__event:`-prefixed events\n * emitted by `WindowEventService`). Returns an unsubscribe function.\n */\n subscribe(channel: string, listener: (payload: unknown) => void): () => void;\n}\n\n/** One channel's input/output contract. The generated channel map is a record of these. */\nexport interface WoodChannelContract {\n input: unknown;\n output: unknown;\n}\n\n/** Shape a generated channel map conforms to: channel name -> { input, output }. */\nexport type WoodChannelMapLike = Record<string, WoodChannelContract>;\n\n/** Extract the input type for a channel in a generated map. */\nexport type WoodChannelInput<\n M extends WoodChannelMapLike,\n C extends keyof M,\n> = M[C]['input'];\n\n/** Extract the output type for a channel in a generated map. */\nexport type WoodChannelOutput<\n M extends WoodChannelMapLike,\n C extends keyof M,\n> = M[C]['output'];\n\n/**\n * A `call()` typed from a generated channel map. Harness `api.call` is this,\n * parameterized on the app's generated `WoodChannelMap`:\n *\n * const call: TypedCall<WoodChannelMap> = ...;\n * await call('messages.send', { threadId, text }); // fully typed\n */\nexport type TypedCall<M extends WoodChannelMapLike> = <C extends keyof M & string>(\n channel: C,\n ...payload: M[C]['input'] extends void ? [] : [M[C]['input']]\n) => Promise<M[C]['output']>;\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}