@fuzdev/fuz_app 0.64.0 → 0.66.0

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 (223) hide show
  1. package/dist/actions/CLAUDE.md +510 -946
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.js +1 -1
  7. package/dist/actions/cancel.d.ts +2 -2
  8. package/dist/actions/cancel.js +3 -3
  9. package/dist/actions/connection_closer.d.ts +1 -4
  10. package/dist/actions/connection_closer.d.ts.map +1 -1
  11. package/dist/actions/connection_closer.js +1 -4
  12. package/dist/actions/register_action_ws.d.ts +2 -2
  13. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  14. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  15. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  16. package/dist/actions/transports_ws_auth_guard.js +1 -2
  17. package/dist/auth/CLAUDE.md +570 -1871
  18. package/dist/auth/account_schema.d.ts +1 -1
  19. package/dist/auth/account_schema.d.ts.map +1 -1
  20. package/dist/auth/api_token_queries.js +1 -1
  21. package/dist/auth/audit_log_ddl.d.ts +1 -1
  22. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  23. package/dist/auth/audit_log_ddl.js +1 -1
  24. package/dist/auth/audit_log_schema.js +2 -2
  25. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  26. package/dist/auth/bootstrap_account.js +1 -5
  27. package/dist/auth/bootstrap_routes.d.ts +7 -1
  28. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  29. package/dist/auth/bootstrap_routes.js +15 -11
  30. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  31. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  32. package/dist/auth/daemon_token_middleware.js +24 -15
  33. package/dist/auth/invite_queries.d.ts +17 -7
  34. package/dist/auth/invite_queries.d.ts.map +1 -1
  35. package/dist/auth/invite_queries.js +19 -8
  36. package/dist/auth/keyring.d.ts +6 -6
  37. package/dist/auth/keyring.js +8 -8
  38. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  39. package/dist/auth/role_grant_offer_actions.js +4 -2
  40. package/dist/auth/signup_routes.d.ts +47 -1
  41. package/dist/auth/signup_routes.d.ts.map +1 -1
  42. package/dist/auth/signup_routes.js +103 -52
  43. package/dist/db/create_db.d.ts.map +1 -1
  44. package/dist/db/create_db.js +13 -0
  45. package/dist/dev/setup.d.ts +2 -2
  46. package/dist/dev/setup.js +3 -3
  47. package/dist/env/resolve.d.ts +44 -7
  48. package/dist/env/resolve.d.ts.map +1 -1
  49. package/dist/env/resolve.js +94 -27
  50. package/dist/http/CLAUDE.md +243 -522
  51. package/dist/http/error_schemas.d.ts +0 -4
  52. package/dist/http/error_schemas.d.ts.map +1 -1
  53. package/dist/http/error_schemas.js +0 -4
  54. package/dist/http/ip_canonical.d.ts +5 -4
  55. package/dist/http/ip_canonical.d.ts.map +1 -1
  56. package/dist/http/ip_canonical.js +8 -4
  57. package/dist/http/jsonrpc.d.ts +23 -7
  58. package/dist/http/jsonrpc.d.ts.map +1 -1
  59. package/dist/http/jsonrpc.js +19 -3
  60. package/dist/http/origin.d.ts +1 -1
  61. package/dist/http/origin.js +1 -1
  62. package/dist/http/surface.d.ts +9 -2
  63. package/dist/http/surface.d.ts.map +1 -1
  64. package/dist/runtime/mock.d.ts +1 -1
  65. package/dist/runtime/mock.js +2 -2
  66. package/dist/server/app_server.d.ts +41 -10
  67. package/dist/server/app_server.d.ts.map +1 -1
  68. package/dist/server/app_server.js +10 -4
  69. package/dist/server/env.d.ts +7 -7
  70. package/dist/server/env.d.ts.map +1 -1
  71. package/dist/server/env.js +14 -14
  72. package/dist/server/static.d.ts +4 -4
  73. package/dist/server/static.js +7 -7
  74. package/dist/testing/CLAUDE.md +740 -418
  75. package/dist/testing/admin_integration.d.ts +18 -23
  76. package/dist/testing/admin_integration.d.ts.map +1 -1
  77. package/dist/testing/admin_integration.js +230 -216
  78. package/dist/testing/app_server.d.ts +141 -39
  79. package/dist/testing/app_server.d.ts.map +1 -1
  80. package/dist/testing/app_server.js +157 -44
  81. package/dist/testing/audit_completeness.d.ts +25 -22
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +198 -159
  84. package/dist/testing/bootstrap_success.d.ts +28 -0
  85. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  86. package/dist/testing/bootstrap_success.js +144 -0
  87. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  88. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/backend_config.js +1 -0
  90. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  91. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  93. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  94. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  95. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  96. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  97. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  98. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  99. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  100. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  102. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  103. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  104. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  105. package/dist/testing/cross_backend/capabilities.d.ts +65 -0
  106. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/capabilities.js +47 -0
  108. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  109. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  110. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  111. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  112. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  113. package/dist/testing/cross_backend/default_secrets.js +39 -0
  114. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  115. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  116. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  117. package/dist/testing/cross_backend/setup.d.ts +451 -0
  118. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  119. package/dist/testing/cross_backend/setup.js +581 -0
  120. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  121. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  122. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  123. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  124. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  125. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  129. package/dist/testing/cross_backend/standard.d.ts +96 -0
  130. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/standard.js +49 -0
  132. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  133. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  135. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  136. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  137. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  138. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  139. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  140. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  141. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  142. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  144. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  145. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  147. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  148. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  149. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  150. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  151. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  152. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  153. package/dist/testing/data_exposure.d.ts +11 -14
  154. package/dist/testing/data_exposure.d.ts.map +1 -1
  155. package/dist/testing/data_exposure.js +123 -146
  156. package/dist/testing/db_entities.d.ts +22 -1
  157. package/dist/testing/db_entities.d.ts.map +1 -1
  158. package/dist/testing/db_entities.js +24 -1
  159. package/dist/testing/integration.d.ts +56 -21
  160. package/dist/testing/integration.d.ts.map +1 -1
  161. package/dist/testing/integration.js +294 -319
  162. package/dist/testing/integration_helpers.d.ts +16 -6
  163. package/dist/testing/integration_helpers.d.ts.map +1 -1
  164. package/dist/testing/integration_helpers.js +7 -7
  165. package/dist/testing/mock_fs.d.ts.map +1 -1
  166. package/dist/testing/mock_fs.js +0 -2
  167. package/dist/testing/rate_limiting.d.ts.map +1 -1
  168. package/dist/testing/rate_limiting.js +9 -0
  169. package/dist/testing/role_grant_helpers.d.ts +31 -0
  170. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  171. package/dist/testing/role_grant_helpers.js +46 -0
  172. package/dist/testing/round_trip.d.ts +20 -16
  173. package/dist/testing/round_trip.d.ts.map +1 -1
  174. package/dist/testing/round_trip.js +61 -86
  175. package/dist/testing/rpc_helpers.d.ts +10 -4
  176. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  177. package/dist/testing/rpc_helpers.js +1 -1
  178. package/dist/testing/rpc_round_trip.d.ts +24 -21
  179. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  180. package/dist/testing/rpc_round_trip.js +87 -104
  181. package/dist/testing/schema_introspect.d.ts +106 -0
  182. package/dist/testing/schema_introspect.d.ts.map +1 -0
  183. package/dist/testing/schema_introspect.js +123 -0
  184. package/dist/testing/schema_parity.d.ts +144 -0
  185. package/dist/testing/schema_parity.d.ts.map +1 -0
  186. package/dist/testing/schema_parity.js +233 -0
  187. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  188. package/dist/testing/sse_round_trip.js +1 -68
  189. package/dist/testing/standard.d.ts +56 -25
  190. package/dist/testing/standard.d.ts.map +1 -1
  191. package/dist/testing/standard.js +62 -5
  192. package/dist/testing/stubs.d.ts +21 -6
  193. package/dist/testing/stubs.d.ts.map +1 -1
  194. package/dist/testing/stubs.js +33 -23
  195. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  196. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  197. package/dist/testing/testing_rate_limiter.js +74 -0
  198. package/dist/testing/transports/bootstrap.d.ts +52 -0
  199. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  200. package/dist/testing/transports/bootstrap.js +70 -0
  201. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  202. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  203. package/dist/testing/transports/fetch_transport.js +74 -0
  204. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  205. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  206. package/dist/testing/transports/sse_frame_reader.js +84 -0
  207. package/dist/testing/transports/sse_transport.d.ts +54 -0
  208. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  209. package/dist/testing/transports/sse_transport.js +51 -0
  210. package/dist/testing/transports/ws_client.d.ts +108 -0
  211. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  212. package/dist/testing/transports/ws_client.js +56 -0
  213. package/dist/testing/transports/ws_transport.d.ts +43 -0
  214. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  215. package/dist/testing/transports/ws_transport.js +169 -0
  216. package/dist/testing/ws_round_trip.d.ts +21 -103
  217. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  218. package/dist/testing/ws_round_trip.js +42 -40
  219. package/dist/ui/CLAUDE.md +5 -3
  220. package/dist/ui/MenuLink.svelte +16 -16
  221. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  222. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  223. package/package.json +10 -4
@@ -0,0 +1,84 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * SSE frame reader over a `ReadableStreamDefaultReader<Uint8Array>`.
4
+ *
5
+ * Transport-agnostic core shared by the in-process SSE route suite
6
+ * (`testing/sse_round_trip.ts`, reading a Hono `Response.body`) and the
7
+ * cross-process `transports/sse_transport.ts` (reading a streaming `fetch`
8
+ * body): `\n\n`-delimited framing, a per-read timeout (so vitest can't hang
9
+ * on a stalled stream), and `wait_for_close` for server-initiated close
10
+ * detection (the auth-guard revocation seam).
11
+ *
12
+ * @module
13
+ */
14
+ /** Default per-read / wait-for-close timeout. */
15
+ export const SSE_FRAME_READ_TIMEOUT_MS = 2000;
16
+ /**
17
+ * Wrap a byte-stream reader in `\n\n`-delimited SSE frame parsing.
18
+ *
19
+ * Preserves bytes past a frame terminator in an internal buffer for the next
20
+ * `read_frame`. `read_frame` and `wait_for_close` both race each underlying
21
+ * read against `timeout_ms` so a misbehaving stream surfaces as a failure
22
+ * rather than a vitest hang.
23
+ */
24
+ export const create_sse_frame_reader = (reader, default_timeout_ms = SSE_FRAME_READ_TIMEOUT_MS) => {
25
+ const decoder = new TextDecoder();
26
+ let buffer = '';
27
+ let closed = false;
28
+ // Race a single read against a timeout — vitest would otherwise hang on a
29
+ // stalled stream. Returns false when the stream ended.
30
+ const pump_once = async (timeout_ms) => {
31
+ const timeout = new Promise((resolve) => {
32
+ setTimeout(() => resolve({ timed_out: true }), timeout_ms);
33
+ });
34
+ const result = (await Promise.race([reader.read(), timeout]));
35
+ if ('timed_out' in result) {
36
+ throw new Error(`SSE read timed out after ${timeout_ms}ms`);
37
+ }
38
+ if (result.done) {
39
+ closed = true;
40
+ return false;
41
+ }
42
+ buffer += decoder.decode(result.value, { stream: true });
43
+ return true;
44
+ };
45
+ const read_frame = async (timeout_ms = default_timeout_ms) => {
46
+ // SSE frames end with a blank line — the canonical terminator is `\n\n`.
47
+ for (;;) {
48
+ const idx = buffer.indexOf('\n\n');
49
+ if (idx >= 0) {
50
+ const frame = buffer.slice(0, idx);
51
+ buffer = buffer.slice(idx + 2);
52
+ return frame;
53
+ }
54
+ const cont = await pump_once(timeout_ms);
55
+ if (!cont)
56
+ throw new Error('SSE stream ended before a frame was received');
57
+ }
58
+ };
59
+ const wait_for_close = async (timeout_ms = default_timeout_ms) => {
60
+ const deadline = Date.now() + timeout_ms;
61
+ for (;;) {
62
+ if (closed)
63
+ return true;
64
+ const remaining = deadline - Date.now();
65
+ if (remaining <= 0)
66
+ return false;
67
+ try {
68
+ await pump_once(Math.min(remaining, timeout_ms));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ };
75
+ const cancel = async () => {
76
+ try {
77
+ await reader.cancel();
78
+ }
79
+ catch {
80
+ // already closed
81
+ }
82
+ };
83
+ return { read_frame, wait_for_close, cancel };
84
+ };
@@ -0,0 +1,54 @@
1
+ import '../assert_dev_env.js';
2
+ /** Construction options for `create_sse_transport`. */
3
+ export interface SseTransportOptions {
4
+ /** Base URL the binary is reachable at — e.g. `http://localhost:1178`. */
5
+ readonly base_url: string;
6
+ /** SSE endpoint path on the binary (e.g. `/api/admin/audit/stream`). */
7
+ readonly sse_path: string;
8
+ /**
9
+ * Session cookie values (full `Set-Cookie` strings as
10
+ * `FetchTransport.cookies()` returns them) threaded onto the request
11
+ * `Cookie` header. Without these the stream is anonymous and the
12
+ * connect is refused (the audit stream requires an admin session).
13
+ */
14
+ readonly cookies: ReadonlyArray<string>;
15
+ /**
16
+ * `Origin` header for the request. Backends running with
17
+ * `ALLOWED_ORIGINS=http://localhost:*` accept `http://localhost:<port>`.
18
+ * Defaults to `base_url` — acceptable because cross-process tests always
19
+ * run against `localhost`.
20
+ */
21
+ readonly origin?: string;
22
+ /** Default per-read / wait-for-close timeout. Falls back to 2000ms. */
23
+ readonly default_timeout_ms?: number;
24
+ }
25
+ /** A cross-process SSE client: read frames, await server close, cancel. */
26
+ export interface SseTransport {
27
+ /**
28
+ * Read one complete SSE frame (up to the next `\n\n`), without the
29
+ * trailing terminator. Throws if the per-read timeout elapses or the
30
+ * stream ends before a frame arrives.
31
+ */
32
+ read_frame: (timeout_ms?: number) => Promise<string>;
33
+ /**
34
+ * Drain until the server closes the stream. Resolves `true` if the
35
+ * stream closes within `timeout_ms`, `false` on timeout. The signal for
36
+ * an auth-guard revocation dropping a live stream — mirrors
37
+ * `WsClient.wait_for_close`.
38
+ */
39
+ wait_for_close: (timeout_ms?: number) => Promise<boolean>;
40
+ /** Cancel the reader (client-initiated close). Safe to call when already closed. */
41
+ close: () => Promise<void>;
42
+ }
43
+ /**
44
+ * Open a real-HTTP SSE stream pinned to `options.base_url` + `sse_path`.
45
+ *
46
+ * Resolves once the response headers arrive and the body is a
47
+ * `text/event-stream`; rejects if the connect is refused (non-2xx status,
48
+ * wrong content type, missing body) so the test surfaces the real cause
49
+ * rather than hanging.
50
+ *
51
+ * @throws Error if the connect fails (status, content type, or no body).
52
+ */
53
+ export declare const create_sse_transport: (options: SseTransportOptions) => Promise<SseTransport>;
54
+ //# sourceMappingURL=sse_transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/sse_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqB9B,uDAAuD;AACvD,MAAM,WAAW,mBAAmB;IACnC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC5B;;;;OAIG;IACH,UAAU,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD;;;;;OAKG;IACH,cAAc,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,oFAAoF;IACpF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAU,SAAS,mBAAmB,KAAG,OAAO,CAAC,YAAY,CA2B7F,CAAC"}
@@ -0,0 +1,51 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-process Server-Sent Events transport.
4
+ *
5
+ * Opens a real streaming `fetch` against a spawned test binary's SSE
6
+ * endpoint, threading the session cookie captured by the sibling
7
+ * `FetchTransport` so the stream authenticates as the same account, then
8
+ * delegates frame parsing to the shared `create_sse_frame_reader`. Uses only
9
+ * built-in streaming `fetch` + `TextDecoder` — no extra dep.
10
+ *
11
+ * Mirrors how `ws_transport.ts` is the cross-process counterpart to the
12
+ * in-process WS harness; the in-process SSE route suite
13
+ * (`../sse_round_trip.ts`) shares the same `create_sse_frame_reader` over a
14
+ * Hono `Response.body`.
15
+ *
16
+ * @module
17
+ */
18
+ import { create_sse_frame_reader } from './sse_frame_reader.js';
19
+ /**
20
+ * Open a real-HTTP SSE stream pinned to `options.base_url` + `sse_path`.
21
+ *
22
+ * Resolves once the response headers arrive and the body is a
23
+ * `text/event-stream`; rejects if the connect is refused (non-2xx status,
24
+ * wrong content type, missing body) so the test surfaces the real cause
25
+ * rather than hanging.
26
+ *
27
+ * @throws Error if the connect fails (status, content type, or no body).
28
+ */
29
+ export const create_sse_transport = async (options) => {
30
+ const { base_url, sse_path, cookies, origin, default_timeout_ms } = options;
31
+ const url = `${base_url}${sse_path}`;
32
+ const headers = {
33
+ Accept: 'text/event-stream',
34
+ Origin: origin ?? base_url,
35
+ };
36
+ if (cookies.length > 0)
37
+ headers.Cookie = cookies.join('; ');
38
+ const res = await fetch(url, { method: 'GET', headers });
39
+ if (!res.ok) {
40
+ throw new Error(`SSE connect to ${url} failed: status ${res.status}`);
41
+ }
42
+ const content_type = res.headers.get('Content-Type');
43
+ if (!content_type?.includes('text/event-stream')) {
44
+ throw new Error(`SSE connect to ${url}: unexpected Content-Type ${content_type}`);
45
+ }
46
+ if (!res.body) {
47
+ throw new Error(`SSE connect to ${url}: response has no body`);
48
+ }
49
+ const { read_frame, wait_for_close, cancel } = create_sse_frame_reader(res.body.getReader(), default_timeout_ms);
50
+ return { read_frame, wait_for_close, close: cancel };
51
+ };
@@ -0,0 +1,108 @@
1
+ import '../assert_dev_env.js';
2
+ import { JSONRPC_VERSION } from '../../http/jsonrpc.js';
3
+ export interface JsonrpcNotificationFrame<P = unknown> {
4
+ jsonrpc: typeof JSONRPC_VERSION;
5
+ method: string;
6
+ params: P;
7
+ }
8
+ export interface JsonrpcSuccessResponseFrame<R = unknown> {
9
+ jsonrpc: typeof JSONRPC_VERSION;
10
+ id: number | string;
11
+ result: R;
12
+ }
13
+ export interface JsonrpcErrorResponseFrame<D = unknown> {
14
+ jsonrpc: typeof JSONRPC_VERSION;
15
+ id: number | string;
16
+ error: {
17
+ code: number;
18
+ message: string;
19
+ data?: D;
20
+ };
21
+ }
22
+ /** Predicate matching a JSON-RPC notification with the given method name. */
23
+ export declare const is_notification: (method: string) => (msg: unknown) => boolean;
24
+ /**
25
+ * Type-guard combinator: match a notification whose typed `params` satisfies
26
+ * `match`. Collapses the common test pattern of casting `msg` to
27
+ * `JsonrpcNotificationFrame<P>` in every predicate body.
28
+ *
29
+ * ```ts
30
+ * const match_roster_for = (id: Uuid) =>
31
+ * is_notification_with<RosterChangedParams>(
32
+ * WORLD_METHODS.roster_changed,
33
+ * (params) => params.character_id === id && !params.removed,
34
+ * );
35
+ * const roster = await client.wait_for(match_roster_for(char_id));
36
+ * ```
37
+ */
38
+ export declare const is_notification_with: <P>(method: string, match: (params: P) => boolean) => (msg: unknown) => msg is JsonrpcNotificationFrame<P>;
39
+ /** Predicate matching a JSON-RPC response frame (success or error) for the given request id. */
40
+ export declare const is_response_for: (id: number | string) => (msg: unknown) => boolean;
41
+ /**
42
+ * Default wait-for timeout shared across in-process + cross-process
43
+ * impls. Tunable per-call via the `timeout_ms` parameter.
44
+ */
45
+ export declare const WS_CLIENT_DEFAULT_TIMEOUT_MS = 1000;
46
+ /** A test WS client: send requests, inspect / await incoming messages. */
47
+ export interface WsClient {
48
+ /**
49
+ * Send a JSON-RPC message (request or notification) to the server.
50
+ *
51
+ * @throws Error if called after `close()` resolves — every impl
52
+ * rejects sends on a closed socket so post-close test bugs surface
53
+ * immediately rather than silently dropping.
54
+ */
55
+ send: (message: unknown) => Promise<void>;
56
+ /**
57
+ * Send a JSON-RPC request and await its response. Resolves with the
58
+ * `result`; throws with a useful message (code, text, and any `data`
59
+ * payload) on an error frame — without this, asserting on
60
+ * `result.foo` for a failed request throws
61
+ * `Cannot read property 'foo' of undefined`, hiding the real cause.
62
+ * Use `send` + `wait_for(is_response_for(id))` directly when the test
63
+ * needs to assert on the error frame itself.
64
+ *
65
+ * @throws Error if the server returns a JSON-RPC error frame for `id`,
66
+ * or if `wait_for` times out before a matching response arrives.
67
+ */
68
+ request: <R = unknown>(id: number | string, method: string, params: unknown, timeout_ms?: number) => Promise<R>;
69
+ /**
70
+ * Close the connection. Returns a promise that resolves once the
71
+ * transport's own cleanup (and any `on_socket_close` for the
72
+ * in-process driver) has completed — tests that assert on post-close
73
+ * state should await.
74
+ */
75
+ close: (code?: number, reason?: string) => Promise<void>;
76
+ /**
77
+ * Wait for the server to close the connection. Resolves `true` if the
78
+ * socket closed within `timeout_ms`, `false` on timeout. The signal for
79
+ * server-initiated close — used by close-on-revoke tests that fire a
80
+ * revocation over a side channel and assert the live socket drops.
81
+ *
82
+ * Resolves `true` immediately when the socket is already closed.
83
+ * Distinct from `close()` (client-initiated): this awaits a close the
84
+ * test did not request. Mirrors `wait_for_close` on the SSE frame reader
85
+ * in `../sse_round_trip.ts`.
86
+ */
87
+ wait_for_close: (timeout_ms?: number) => Promise<boolean>;
88
+ /** Every message the server has sent, in arrival order. */
89
+ readonly messages: ReadonlyArray<unknown>;
90
+ /**
91
+ * Wait until a message satisfies `predicate`. Matches are checked
92
+ * against already-received messages first, then new arrivals until
93
+ * the timeout (defaults to `WS_CLIENT_DEFAULT_TIMEOUT_MS`).
94
+ *
95
+ * When `predicate` is a type guard (e.g. `is_notification_with<P>`),
96
+ * the result is narrowed automatically and callers don't need to
97
+ * spell `<JsonrpcNotificationFrame<P>>` on the call site.
98
+ *
99
+ * @throws Error if `timeout_ms` elapses before a matching message
100
+ * arrives — the pending waiter is dropped from the internal list so
101
+ * later messages don't keep iterating it.
102
+ */
103
+ wait_for: {
104
+ <T>(predicate: (msg: unknown) => msg is T, timeout_ms?: number): Promise<T>;
105
+ <T = unknown>(predicate: (msg: unknown) => boolean, timeout_ms?: number): Promise<T>;
106
+ };
107
+ }
108
+ //# sourceMappingURL=ws_client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_client.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_client.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2B9B,OAAO,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAatD,MAAM,WAAW,wBAAwB,CAAC,CAAC,GAAG,OAAO;IACpD,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,CAAC;CACV;AAED,MAAM,WAAW,2BAA2B,CAAC,CAAC,GAAG,OAAO;IACvD,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC;CACV;AAED,MAAM,WAAW,yBAAyB,CAAC,CAAC,GAAG,OAAO;IACrD,OAAO,EAAE,OAAO,eAAe,CAAC;IAChC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,CAAA;KAAC,CAAC;CACjD;AAQD,6EAA6E;AAC7E,eAAO,MAAM,eAAe,GAC1B,QAAQ,MAAM,MACd,KAAK,OAAO,KAAG,OACsC,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAC/B,CAAC,EAAE,QAAQ,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,MAChD,KAAK,OAAO,KAAG,GAAG,IAAI,wBAAwB,CAAC,CAAC,CAGE,CAAC;AAErD,gGAAgG;AAChG,eAAO,MAAM,eAAe,GAC1B,IAAI,MAAM,GAAG,MAAM,MACnB,KAAK,OAAO,KAAG,OAC8D,CAAC;AAMhF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACxB;;;;;;OAMG;IACH,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C;;;;;;;;;;;OAWG;IACH,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,EACpB,EAAE,EAAE,MAAM,GAAG,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,UAAU,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB;;;;;OAKG;IACH,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD;;;;;;;;;;OAUG;IACH,cAAc,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAC1C;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE;QACT,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,GAAG,IAAI,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAE5E,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;KACrF,CAAC;CACF"}
@@ -0,0 +1,56 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Shared WebSocket client surface for cross-backend tests.
4
+ *
5
+ * `WsClient` is the interface every in-process or cross-process WS test
6
+ * driver implements — `send` / `request` / `close` / `messages` /
7
+ * `wait_for`. Two impls today:
8
+ *
9
+ * - **In-process** — `create_ws_test_harness` in `../ws_round_trip.ts`.
10
+ * Drives `register_action_ws` against a fake Hono upgrade so the
11
+ * dispatcher's full path runs without the wire upgrade.
12
+ * - **Cross-process** — `create_ws_transport` in `./ws_transport.ts`.
13
+ * Wraps the native `WebSocket` upgrade against a real running binary,
14
+ * threading the session cookie captured by `FetchTransport`.
15
+ *
16
+ * Wire-frame types + predicates also live here so both impls (and every
17
+ * shared assertion helper) reference one source.
18
+ *
19
+ * @module
20
+ */
21
+ import { is_jsonrpc_error_response, is_jsonrpc_notification, is_jsonrpc_response, } from '../../http/jsonrpc_helpers.js';
22
+ import { JSONRPC_VERSION } from '../../http/jsonrpc.js';
23
+ // ---------------------------------------------------------------------
24
+ // Predicates — compose with `WsClient.wait_for` and
25
+ // `messages.filter(...)`. Both in-process and cross-process tests use
26
+ // the same names against the same shapes.
27
+ // ---------------------------------------------------------------------
28
+ /** Predicate matching a JSON-RPC notification with the given method name. */
29
+ export const is_notification = (method) => (msg) => is_jsonrpc_notification(msg) && msg.method === method;
30
+ /**
31
+ * Type-guard combinator: match a notification whose typed `params` satisfies
32
+ * `match`. Collapses the common test pattern of casting `msg` to
33
+ * `JsonrpcNotificationFrame<P>` in every predicate body.
34
+ *
35
+ * ```ts
36
+ * const match_roster_for = (id: Uuid) =>
37
+ * is_notification_with<RosterChangedParams>(
38
+ * WORLD_METHODS.roster_changed,
39
+ * (params) => params.character_id === id && !params.removed,
40
+ * );
41
+ * const roster = await client.wait_for(match_roster_for(char_id));
42
+ * ```
43
+ */
44
+ export const is_notification_with = (method, match) => (msg) => is_jsonrpc_notification(msg) &&
45
+ msg.method === method &&
46
+ match(msg.params);
47
+ /** Predicate matching a JSON-RPC response frame (success or error) for the given request id. */
48
+ export const is_response_for = (id) => (msg) => (is_jsonrpc_response(msg) || is_jsonrpc_error_response(msg)) && msg.id === id;
49
+ // ---------------------------------------------------------------------
50
+ // WsClient — the test-driver surface every impl implements.
51
+ // ---------------------------------------------------------------------
52
+ /**
53
+ * Default wait-for timeout shared across in-process + cross-process
54
+ * impls. Tunable per-call via the `timeout_ms` parameter.
55
+ */
56
+ export const WS_CLIENT_DEFAULT_TIMEOUT_MS = 1000;
@@ -0,0 +1,43 @@
1
+ import '../assert_dev_env.js';
2
+ import { type WsClient } from './ws_client.js';
3
+ /** Construction options for `create_ws_transport`. */
4
+ export interface WsTransportOptions {
5
+ /** Base URL the binary is reachable at — e.g. `http://localhost:8788`. Converted to `ws://` for the upgrade. */
6
+ readonly base_url: string;
7
+ /** WebSocket endpoint path on the binary (e.g. `/api/ws`). */
8
+ readonly ws_path: string;
9
+ /**
10
+ * Session cookie values (full `Set-Cookie` strings as
11
+ * `FetchTransport.cookies()` returns them) threaded onto the upgrade
12
+ * `Cookie` header. Without these the upgrade is anonymous and
13
+ * per-action auth fails on the first message.
14
+ */
15
+ readonly cookies: ReadonlyArray<string>;
16
+ /**
17
+ * Origin header for the upgrade. Backends running with
18
+ * `ALLOWED_ORIGINS=http://localhost:*` accept `http://localhost:<port>`.
19
+ * Defaults to `base_url` — acceptable because cross-process tests
20
+ * always run against `localhost`.
21
+ */
22
+ readonly origin?: string;
23
+ /**
24
+ * Optional per-call default for `wait_for` timeouts. Falls back to
25
+ * `WS_CLIENT_DEFAULT_TIMEOUT_MS` if omitted.
26
+ */
27
+ readonly default_timeout_ms?: number;
28
+ }
29
+ /**
30
+ * Build a real-upgrade WS client pinned to `options.base_url` + `ws_path`.
31
+ *
32
+ * Resolves once the upgrade succeeds and the socket is in `OPEN` state;
33
+ * rejects if the upgrade is refused (401, allowlist rejection, network
34
+ * failure). Incoming messages are JSON-parsed and pushed onto the
35
+ * `messages` array; `wait_for` checks already-received messages first
36
+ * before waiting for new arrivals.
37
+ *
38
+ * @throws Error if the upgrade fails (status, network) — the rejection
39
+ * message carries the underlying error so the test surfaces the real
40
+ * cause rather than hanging.
41
+ */
42
+ export declare const create_ws_transport: (options: WsTransportOptions) => Promise<WsClient>;
43
+ //# sourceMappingURL=ws_transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAKN,KAAK,QAAQ,EACb,MAAM,gBAAgB,CAAC;AAGxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAsJvF,CAAC"}
@@ -0,0 +1,169 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-process WebSocket transport.
4
+ *
5
+ * Implements the shared `WsClient` interface (see `ws_client.ts`) over a
6
+ * real `WebSocket` upgrade against a spawned test binary. The cookie
7
+ * captured by the sibling `FetchTransport` on bootstrap is threaded onto
8
+ * the upgrade request so the WS session authenticates as the same
9
+ * account.
10
+ *
11
+ * Uses the `ws` npm package — Node's native `WebSocket` (from undici)
12
+ * follows the WHATWG spec strictly and doesn't accept custom request
13
+ * headers on construction, so cookie-on-upgrade requires the `ws`
14
+ * package's `headers` option. fuz_app declares `ws` as an optional
15
+ * peerDependency — consumers wiring cross-process tests install it
16
+ * themselves (`npm install --save-dev ws`).
17
+ *
18
+ * @module
19
+ */
20
+ import { WebSocket } from 'ws';
21
+ import { WS_CLIENT_DEFAULT_TIMEOUT_MS, is_response_for, } from './ws_client.js';
22
+ import { create_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
23
+ /**
24
+ * Build a real-upgrade WS client pinned to `options.base_url` + `ws_path`.
25
+ *
26
+ * Resolves once the upgrade succeeds and the socket is in `OPEN` state;
27
+ * rejects if the upgrade is refused (401, allowlist rejection, network
28
+ * failure). Incoming messages are JSON-parsed and pushed onto the
29
+ * `messages` array; `wait_for` checks already-received messages first
30
+ * before waiting for new arrivals.
31
+ *
32
+ * @throws Error if the upgrade fails (status, network) — the rejection
33
+ * message carries the underlying error so the test surfaces the real
34
+ * cause rather than hanging.
35
+ */
36
+ export const create_ws_transport = async (options) => {
37
+ const { base_url, ws_path, cookies, origin, default_timeout_ms } = options;
38
+ const default_timeout = default_timeout_ms ?? WS_CLIENT_DEFAULT_TIMEOUT_MS;
39
+ const ws_url = `${base_url.replace(/^http/i, 'ws')}${ws_path}`;
40
+ const headers = {};
41
+ if (cookies.length > 0)
42
+ headers.Cookie = cookies.join('; ');
43
+ const socket = new WebSocket(ws_url, {
44
+ headers,
45
+ origin: origin ?? base_url,
46
+ });
47
+ const received = [];
48
+ const waiters = [];
49
+ let close_resolvers = [];
50
+ let close_error = null;
51
+ socket.on('message', (data) => {
52
+ // `ws` may deliver Buffer / ArrayBuffer / Buffer[]; normalize to string.
53
+ const text = Array.isArray(data)
54
+ ? Buffer.concat(data).toString('utf-8')
55
+ : data instanceof ArrayBuffer
56
+ ? Buffer.from(data).toString('utf-8')
57
+ : data.toString('utf-8');
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(text);
61
+ }
62
+ catch {
63
+ // Non-JSON frame — store the raw string so tests that inspect
64
+ // it still see the payload.
65
+ parsed = text;
66
+ }
67
+ received.push(parsed);
68
+ for (let i = waiters.length - 1; i >= 0; i--) {
69
+ const waiter = waiters[i];
70
+ if (waiter.predicate(parsed)) {
71
+ waiter.resolve(parsed);
72
+ waiters.splice(i, 1);
73
+ }
74
+ }
75
+ });
76
+ socket.on('close', () => {
77
+ for (const resolve of close_resolvers)
78
+ resolve();
79
+ close_resolvers = [];
80
+ });
81
+ // Wait for the upgrade to complete (or fail) before resolving the
82
+ // factory promise. Suite bodies expect a connected client back.
83
+ await new Promise((resolve, reject) => {
84
+ const on_open = () => {
85
+ socket.on('error', (err) => {
86
+ // Post-open errors stash for diagnostics; close handler
87
+ // resolves the close() awaiters whether or not error fired.
88
+ close_error = err;
89
+ });
90
+ resolve();
91
+ };
92
+ const on_error = (err) => {
93
+ reject(new Error(`ws upgrade to ${ws_url} failed: ${err.message}`));
94
+ };
95
+ socket.once('open', on_open);
96
+ socket.once('error', on_error);
97
+ });
98
+ const is_closed = () => socket.readyState === WebSocket.CLOSING || socket.readyState === WebSocket.CLOSED;
99
+ const wait_for_close = (timeout_ms = default_timeout) => {
100
+ if (socket.readyState === WebSocket.CLOSED)
101
+ return Promise.resolve(true);
102
+ return new Promise((resolve) => {
103
+ const on_close = () => {
104
+ clearTimeout(timer);
105
+ resolve(true);
106
+ };
107
+ const timer = setTimeout(() => {
108
+ const i = close_resolvers.indexOf(on_close);
109
+ if (i >= 0)
110
+ close_resolvers.splice(i, 1);
111
+ resolve(false);
112
+ }, timeout_ms);
113
+ close_resolvers.push(on_close);
114
+ });
115
+ };
116
+ const wait_for_impl = (predicate, timeout_ms = default_timeout) => {
117
+ for (const msg of received) {
118
+ if (predicate(msg))
119
+ return Promise.resolve(msg);
120
+ }
121
+ return new Promise((resolve, reject) => {
122
+ const waiter = {
123
+ predicate,
124
+ resolve: (msg) => {
125
+ clearTimeout(timer);
126
+ resolve(msg);
127
+ },
128
+ };
129
+ const timer = setTimeout(() => {
130
+ const i = waiters.indexOf(waiter);
131
+ if (i >= 0)
132
+ waiters.splice(i, 1);
133
+ reject(new Error(`wait_for timed out after ${timeout_ms}ms`));
134
+ }, timeout_ms);
135
+ waiters.push(waiter);
136
+ });
137
+ };
138
+ const send_impl = async (message) => {
139
+ if (is_closed() || socket.readyState !== WebSocket.OPEN)
140
+ throw new Error('send after close');
141
+ socket.send(JSON.stringify(message));
142
+ };
143
+ return {
144
+ get messages() {
145
+ return received;
146
+ },
147
+ send: send_impl,
148
+ async request(id, method, params, timeout_ms) {
149
+ await send_impl(create_jsonrpc_request(method, params, id));
150
+ const msg = await wait_for_impl(is_response_for(id), timeout_ms);
151
+ if ('error' in msg) {
152
+ const detail = msg.error.data === undefined ? '' : ` data=${JSON.stringify(msg.error.data)}`;
153
+ throw new Error(`rpc #${id} failed: [${msg.error.code}] ${msg.error.message}${detail}`);
154
+ }
155
+ return msg.result;
156
+ },
157
+ async close(code, reason) {
158
+ if (!is_closed())
159
+ socket.close(code, reason);
160
+ if (socket.readyState !== WebSocket.CLOSED) {
161
+ await new Promise((resolve) => close_resolvers.push(resolve));
162
+ }
163
+ if (close_error)
164
+ throw close_error;
165
+ },
166
+ wait_for: wait_for_impl,
167
+ wait_for_close,
168
+ };
169
+ };