@fuzdev/fuz_app 0.24.0 → 0.26.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 (56) hide show
  1. package/dist/actions/action_codegen.d.ts +25 -0
  2. package/dist/actions/action_codegen.d.ts.map +1 -1
  3. package/dist/actions/action_codegen.js +39 -0
  4. package/dist/actions/action_peer.d.ts +7 -0
  5. package/dist/actions/action_peer.d.ts.map +1 -1
  6. package/dist/actions/action_peer.js +1 -1
  7. package/dist/actions/action_types.d.ts +16 -1
  8. package/dist/actions/action_types.d.ts.map +1 -1
  9. package/dist/actions/cancel.d.ts +78 -0
  10. package/dist/actions/cancel.d.ts.map +1 -0
  11. package/dist/actions/cancel.js +79 -0
  12. package/dist/actions/register_action_ws.d.ts.map +1 -1
  13. package/dist/actions/register_action_ws.js +62 -22
  14. package/dist/actions/rpc_client.d.ts +10 -0
  15. package/dist/actions/rpc_client.d.ts.map +1 -1
  16. package/dist/actions/rpc_client.js +22 -7
  17. package/dist/actions/socket.svelte.d.ts +22 -10
  18. package/dist/actions/socket.svelte.d.ts.map +1 -1
  19. package/dist/actions/socket.svelte.js +46 -12
  20. package/dist/actions/transports.d.ts +14 -3
  21. package/dist/actions/transports.d.ts.map +1 -1
  22. package/dist/actions/transports_http.d.ts +3 -3
  23. package/dist/actions/transports_http.d.ts.map +1 -1
  24. package/dist/actions/transports_http.js +4 -3
  25. package/dist/actions/transports_ws.d.ts +33 -6
  26. package/dist/actions/transports_ws.d.ts.map +1 -1
  27. package/dist/actions/transports_ws.js +43 -46
  28. package/dist/actions/transports_ws_backend.d.ts +12 -3
  29. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  30. package/dist/actions/transports_ws_backend.js +12 -1
  31. package/dist/auth/bearer_auth.js +0 -1
  32. package/dist/auth/keyring.d.ts.map +1 -1
  33. package/dist/auth/keyring.js +0 -2
  34. package/dist/auth/migrations.js +4 -4
  35. package/dist/db/migrate.d.ts +12 -2
  36. package/dist/db/migrate.d.ts.map +1 -1
  37. package/dist/db/migrate.js +25 -16
  38. package/dist/db/status.d.ts.map +1 -1
  39. package/dist/db/status.js +0 -2
  40. package/dist/dev/setup.d.ts +34 -0
  41. package/dist/dev/setup.d.ts.map +1 -1
  42. package/dist/dev/setup.js +50 -2
  43. package/dist/http/db_routes.d.ts.map +1 -1
  44. package/dist/http/db_routes.js +0 -1
  45. package/dist/testing/admin_integration.d.ts.map +1 -1
  46. package/dist/testing/admin_integration.js +0 -3
  47. package/dist/testing/app_server.js +1 -1
  48. package/dist/testing/data_exposure.js +6 -8
  49. package/dist/testing/db.js +1 -1
  50. package/dist/testing/integration.js +0 -1
  51. package/dist/testing/rate_limiting.d.ts.map +1 -1
  52. package/dist/testing/rate_limiting.js +0 -6
  53. package/dist/testing/rpc_round_trip.js +4 -4
  54. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  55. package/dist/testing/sse_round_trip.js +1 -2
  56. package/package.json +2 -2
@@ -24,6 +24,7 @@
24
24
  * @module
25
25
  */
26
26
  import type { Logger } from '@fuzdev/fuz_util/log.js';
27
+ import { type JsonrpcRequestId } from '../http/jsonrpc.js';
27
28
  import type { WebsocketConnection } from './transports_ws.js';
28
29
  /** Default WebSocket close code (normal closure). */
29
30
  export declare const DEFAULT_CLOSE_CODE = 1000;
@@ -186,23 +187,34 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
186
187
  send(data: object): boolean;
187
188
  /**
188
189
  * Promise-based JSON-RPC over the socket. Auto-assigns a monotonic request
189
- * id, tracks the pending promise, and resolves when the server sends a
190
- * matching response (or rejects on error frame, socket close, or aborted
191
- * signal).
190
+ * id (or uses an explicit one supplied via `options.id` used by
191
+ * `FrontendWebsocketTransport` which delegates to this method and has its
192
+ * own peer-minted UUID), tracks the pending promise, and resolves when the
193
+ * server sends a matching response (or rejects on error frame, socket
194
+ * close, or aborted signal).
195
+ *
196
+ * Callers supplying an explicit `options.id` are responsible for
197
+ * uniqueness — the pending map is keyed by id, and a duplicate silently
198
+ * overwrites the prior entry. Auto-minted ids are monotonic and never
199
+ * collide with themselves or with peer-minted UUIDs (the types differ:
200
+ * integer vs string).
192
201
  *
193
202
  * While the socket is disconnected, the request is buffered in a bounded
194
- * queue (default-on, {@link DEFAULT_QUEUE_MAX_SIZE}) and flushed on
195
- * reopen. Pass `{queue: false}` to reject immediately when disconnected
196
- * — used internally by the heartbeat, which must not fight the queue for
197
- * the disconnect-detection slot.
203
+ * queue (default-on, `DEFAULT_QUEUE_MAX_SIZE`) and flushed on reopen. Pass
204
+ * `{queue: false}` to reject immediately when disconnected — used
205
+ * internally by the heartbeat, which must not fight the queue for the
206
+ * disconnect-detection slot.
198
207
  *
199
- * `AbortSignal` integration today rejects the local promise; the
200
- * server-side cancel protocol (sending a `cancel` notification to abort
201
- * the in-flight handler) lands in Phase 3c as a follow-up PR.
208
+ * On `AbortSignal` fire: rejects the local promise *and* sends the shared
209
+ * `cancel` notification (`CANCEL_METHOD`) so the server-side dispatcher
210
+ * can abort the matching handler's `ctx.signal`. Suppressed for
211
+ * queued-but-never-sent (server doesn't know about it) and
212
+ * response-beat-cancel races.
202
213
  */
203
214
  request<R = unknown>(method: string, params?: unknown, options?: {
204
215
  signal?: AbortSignal;
205
216
  queue?: boolean;
217
+ id?: JsonrpcRequestId;
206
218
  }): Promise<R>;
207
219
  add_message_handler(handler: SocketMessageHandler): () => void;
208
220
  add_error_handler(handler: SocketErrorHandler): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,7 @@
26
26
  import { BROWSER } from 'esm-env';
27
27
  import { JSONRPC_VERSION } from '../http/jsonrpc.js';
28
28
  import { WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, WS_CLOSE_SESSION_REVOKED } from './transports.js';
29
+ import { CANCEL_METHOD } from './cancel.js';
29
30
  import { HEARTBEAT_METHOD } from './heartbeat.js';
30
31
  /** Default WebSocket close code (normal closure). */
31
32
  export const DEFAULT_CLOSE_CODE = 1000;
@@ -253,19 +254,29 @@ export class FrontendWebsocketClient {
253
254
  }
254
255
  /**
255
256
  * Promise-based JSON-RPC over the socket. Auto-assigns a monotonic request
256
- * id, tracks the pending promise, and resolves when the server sends a
257
- * matching response (or rejects on error frame, socket close, or aborted
258
- * signal).
257
+ * id (or uses an explicit one supplied via `options.id` used by
258
+ * `FrontendWebsocketTransport` which delegates to this method and has its
259
+ * own peer-minted UUID), tracks the pending promise, and resolves when the
260
+ * server sends a matching response (or rejects on error frame, socket
261
+ * close, or aborted signal).
262
+ *
263
+ * Callers supplying an explicit `options.id` are responsible for
264
+ * uniqueness — the pending map is keyed by id, and a duplicate silently
265
+ * overwrites the prior entry. Auto-minted ids are monotonic and never
266
+ * collide with themselves or with peer-minted UUIDs (the types differ:
267
+ * integer vs string).
259
268
  *
260
269
  * While the socket is disconnected, the request is buffered in a bounded
261
- * queue (default-on, {@link DEFAULT_QUEUE_MAX_SIZE}) and flushed on
262
- * reopen. Pass `{queue: false}` to reject immediately when disconnected
263
- * — used internally by the heartbeat, which must not fight the queue for
264
- * the disconnect-detection slot.
270
+ * queue (default-on, `DEFAULT_QUEUE_MAX_SIZE`) and flushed on reopen. Pass
271
+ * `{queue: false}` to reject immediately when disconnected — used
272
+ * internally by the heartbeat, which must not fight the queue for the
273
+ * disconnect-detection slot.
265
274
  *
266
- * `AbortSignal` integration today rejects the local promise; the
267
- * server-side cancel protocol (sending a `cancel` notification to abort
268
- * the in-flight handler) lands in Phase 3c as a follow-up PR.
275
+ * On `AbortSignal` fire: rejects the local promise *and* sends the shared
276
+ * `cancel` notification (`CANCEL_METHOD`) so the server-side dispatcher
277
+ * can abort the matching handler's `ctx.signal`. Suppressed for
278
+ * queued-but-never-sent (server doesn't know about it) and
279
+ * response-beat-cancel races.
269
280
  */
270
281
  request(method, params = {}, options = {}) {
271
282
  return new Promise((resolve, reject) => {
@@ -280,7 +291,7 @@ export class FrontendWebsocketClient {
280
291
  reject_typed(this.#build_abort_error(method));
281
292
  return;
282
293
  }
283
- const id = ++this.#next_request_id;
294
+ const id = options.id ?? ++this.#next_request_id;
284
295
  const frame = { jsonrpc: JSONRPC_VERSION, id, method, params };
285
296
  // Bind the signal listener up-front so `#detach_signal` can find it by
286
297
  // reference regardless of which settlement path runs (inline send,
@@ -290,11 +301,21 @@ export class FrontendWebsocketClient {
290
301
  ? () => {
291
302
  if (!pending)
292
303
  return;
293
- this.#pending.delete(id);
304
+ // `Map.delete` returns true iff the entry existed — which
305
+ // is our signal that the frame was actually written to
306
+ // the socket (pending-only tracks in-flight). If it was
307
+ // only queued (never sent), the server doesn't know
308
+ // about it and doesn't need a cancel. If the response
309
+ // beat the abort, `#handle_message` already deleted the
310
+ // entry and detached this listener, so this closure
311
+ // never runs in that race.
312
+ const was_in_flight = this.#pending.delete(id);
294
313
  this.#drop_queued(id);
295
314
  this.#detach_signal(pending);
296
315
  pending = null;
297
316
  reject_typed(this.#build_abort_error(method));
317
+ if (was_in_flight)
318
+ this.#send_cancel(id);
298
319
  }
299
320
  : null;
300
321
  if (signal && signal_handler)
@@ -337,6 +358,19 @@ export class FrontendWebsocketClient {
337
358
  #build_abort_error(method) {
338
359
  return new Error(`[socket] request aborted (method=${method})`);
339
360
  }
361
+ /**
362
+ * Fire-and-forget cancel notification to the server. The dispatcher
363
+ * looks up the matching pending handler's per-request `AbortController`
364
+ * and aborts it; unknown ids no-op. Drops silently when disconnected —
365
+ * the server's own socket-close path will abort any in-flight handlers.
366
+ */
367
+ #send_cancel(request_id) {
368
+ this.send({
369
+ jsonrpc: JSONRPC_VERSION,
370
+ method: CANCEL_METHOD,
371
+ params: { request_id },
372
+ });
373
+ }
340
374
  #detach_signal(pending) {
341
375
  if (pending.signal && pending.signal_handler) {
342
376
  pending.signal.removeEventListener('abort', pending.signal_handler);
@@ -16,11 +16,22 @@ export declare const WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002;
16
16
  export declare const WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003;
17
17
  export declare const TransportName: z.ZodString;
18
18
  export type TransportName = z.infer<typeof TransportName>;
19
+ /**
20
+ * Per-call options accepted by every transport's `send`. Optional and
21
+ * extensible — adding a field is non-breaking. Today: an `AbortSignal`
22
+ * for cancellation that bottoms out at `FrontendWebsocketClient.request`
23
+ * (which sends the shared `cancel` notification on abort) and at
24
+ * `fetch({signal})` for HTTP. Backend transport receives the option but
25
+ * has no per-call abort surface to honor.
26
+ */
27
+ export interface TransportSendOptions {
28
+ signal?: AbortSignal;
29
+ }
19
30
  export interface Transport {
20
31
  transport_name: TransportName;
21
- send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
22
- send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
23
- send(message: JsonrpcMessageFromClientToServer): Promise<JsonrpcMessageFromServerToClient | null>;
32
+ send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
33
+ send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
34
+ send(message: JsonrpcMessageFromClientToServer, options?: TransportSendOptions): Promise<JsonrpcMessageFromServerToClient | null>;
24
35
  is_ready: () => boolean;
25
36
  dispose?: () => void;
26
37
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,mDAAmD;AACnD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,sEAAsE;AACtE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AAKtD,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACzE,IAAI,CAAC,OAAO,EAAE,gCAAgC,GAAG,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IAClG,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;OAEG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAS9C,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;OAKG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CAqDtE"}
1
+ {"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,mDAAmD;AACnD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,sEAAsE;AACtE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AAKtD,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/F,IAAI,CACH,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACxC,IAAI,CACH,OAAO,EAAE,gCAAgC,EACzC,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IACpD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;OAEG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAS9C,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;OAKG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CAqDtE"}
@@ -4,13 +4,13 @@
4
4
  * @module
5
5
  */
6
6
  import type { JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
7
- import type { Transport } from './transports.js';
7
+ import type { Transport, TransportSendOptions } from './transports.js';
8
8
  export declare class FrontendHttpTransport implements Transport {
9
9
  #private;
10
10
  readonly transport_name: "frontend_http_rpc";
11
11
  constructor(url: string, headers?: Record<string, string>, has_side_effects?: (method: string) => boolean);
12
- send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
13
- send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
12
+ send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
13
+ send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
14
14
  is_ready(): boolean;
15
15
  }
16
16
  //# sourceMappingURL=transports_http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transports_http.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,KAAK,EAGX,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE/C,qBAAa,qBAAsB,YAAW,SAAS;;IACtD,QAAQ,CAAC,cAAc,EAAG,mBAAmB,CAAU;gBAOtD,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO;IAOzC,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAuE9E,QAAQ,IAAI,OAAO;CAGnB"}
1
+ {"version":3,"file":"transports_http.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,KAAK,EAGX,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,SAAS,EAAE,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAErE,qBAAa,qBAAsB,YAAW,SAAS;;IACtD,QAAQ,CAAC,cAAc,EAAG,mBAAmB,CAAU;gBAOtD,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO;IAOzC,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAyEvC,QAAQ,IAAI,OAAO;CAGnB"}
@@ -16,7 +16,8 @@ export class FrontendHttpTransport {
16
16
  this.#headers = headers ?? { 'content-type': 'application/json', accept: 'application/json' };
17
17
  this.#has_side_effects = has_side_effects;
18
18
  }
19
- async send(message) {
19
+ async send(message, options) {
20
+ const signal = options?.signal;
20
21
  try {
21
22
  let response;
22
23
  if (this.#has_side_effects && !this.#has_side_effects(message.method) && 'id' in message) {
@@ -31,6 +32,7 @@ export class FrontendHttpTransport {
31
32
  response = await fetch(`${this.#url}${separator}${search_params.toString()}`, {
32
33
  method: 'GET',
33
34
  headers: this.#headers,
35
+ signal,
34
36
  });
35
37
  }
36
38
  else {
@@ -38,8 +40,7 @@ export class FrontendHttpTransport {
38
40
  method: 'POST',
39
41
  headers: this.#headers,
40
42
  body: JSON.stringify(message),
41
- // TODO
42
- // signal: AbortSignal.timeout(REQUEST_TIMEOUT),
43
+ signal,
43
44
  });
44
45
  }
45
46
  const result = await response.json();
@@ -1,10 +1,18 @@
1
1
  /**
2
- * WebSocket transport — sends JSON-RPC messages via WebSocket with request tracking.
2
+ * Frontend WebSocket transport — thin adapter over `WebsocketRpcConnection`.
3
+ *
4
+ * Delegates request/response correlation, the durable queue, the heartbeat,
5
+ * and `AbortSignal`-driven cancel to the underlying connection (the
6
+ * canonical implementation is `FrontendWebsocketClient`). The transport's
7
+ * own job is the `Transport` contract: route inbound server-pushed
8
+ * messages into `peer.receive` and translate the connection's
9
+ * `Promise<R>`/`ThrownJsonrpcError` shape into `JsonrpcResponseOrError`
10
+ * envelopes. No parallel pending-request map.
3
11
  *
4
12
  * @module
5
13
  */
6
- import type { JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
7
- import type { Transport } from './transports.js';
14
+ import type { JsonrpcNotification, JsonrpcRequest, JsonrpcRequestId, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
15
+ import type { Transport, TransportSendOptions } from './transports.js';
8
16
  /**
9
17
  * Minimal interface for a WebSocket connection, decoupled from the concrete Socket Cell.
10
18
  */
@@ -14,12 +22,31 @@ export interface WebsocketConnection {
14
22
  add_message_handler: (handler: (event: MessageEvent) => void) => () => void;
15
23
  add_error_handler: (handler: (event: Event) => void) => () => void;
16
24
  }
25
+ /**
26
+ * RPC-capable WebSocket connection — a `WebsocketConnection` that also
27
+ * handles request/response correlation with timeout, queue,
28
+ * `AbortSignal` cancel, and explicit-id support. Required by
29
+ * `FrontendWebsocketTransport` so it can delegate the pending-map
30
+ * bookkeeping to one canonical implementation
31
+ * (`FrontendWebsocketClient`) instead of running a parallel one.
32
+ *
33
+ * Consumer wrappers around `FrontendWebsocketClient` (e.g. zzz's
34
+ * `Socket`) implement this by adding a one-line delegate to the
35
+ * underlying client's `request`.
36
+ */
37
+ export interface WebsocketRpcConnection extends WebsocketConnection {
38
+ request: (method: string, params: unknown, options?: {
39
+ signal?: AbortSignal;
40
+ queue?: boolean;
41
+ id?: JsonrpcRequestId;
42
+ }) => Promise<unknown>;
43
+ }
17
44
  export declare class FrontendWebsocketTransport implements Transport {
18
45
  #private;
19
46
  readonly transport_name: "frontend_websocket_rpc";
20
- constructor(connection: WebsocketConnection, receive: (data: unknown) => Promise<unknown>, request_timeout_ms?: number);
21
- send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
22
- send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
47
+ constructor(connection: WebsocketRpcConnection, receive: (data: unknown) => Promise<unknown>);
48
+ send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
49
+ send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
23
50
  is_ready(): boolean;
24
51
  dispose(): void;
25
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transports_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,KAAK,EAGX,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAI/C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC5E,iBAAiB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CACnE;AAED,qBAAa,0BAA2B,YAAW,SAAS;;IAC3D,QAAQ,CAAC,cAAc,EAAG,wBAAwB,CAAU;gBAS3D,UAAU,EAAE,mBAAmB,EAC/B,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5C,kBAAkB,CAAC,EAAE,MAAM;IAkCtB,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA8C9E,QAAQ,IAAI,OAAO;IAInB,OAAO,IAAI,IAAI;CAUf"}
1
+ {"version":3,"file":"transports_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH,OAAO,KAAK,EAGX,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,SAAS,EAAE,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAIrE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC5E,iBAAiB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CACnE;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IAClE,OAAO,EAAE,CACR,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,OAAO,CAAC,EAAE;QAAC,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,EAAE,CAAC,EAAE,gBAAgB,CAAA;KAAC,KACpE,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB;AAED,qBAAa,0BAA2B,YAAW,SAAS;;IAC3D,QAAQ,CAAC,cAAc,EAAG,wBAAwB,CAAU;gBAOhD,UAAU,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC;IAyBtF,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAoDvC,QAAQ,IAAI,OAAO;IAInB,OAAO,IAAI,IAAI;CAUf"}
@@ -1,82 +1,79 @@
1
1
  /**
2
- * WebSocket transport — sends JSON-RPC messages via WebSocket with request tracking.
2
+ * Frontend WebSocket transport — thin adapter over `WebsocketRpcConnection`.
3
+ *
4
+ * Delegates request/response correlation, the durable queue, the heartbeat,
5
+ * and `AbortSignal`-driven cancel to the underlying connection (the
6
+ * canonical implementation is `FrontendWebsocketClient`). The transport's
7
+ * own job is the `Transport` contract: route inbound server-pushed
8
+ * messages into `peer.receive` and translate the connection's
9
+ * `Promise<R>`/`ThrownJsonrpcError` shape into `JsonrpcResponseOrError`
10
+ * envelopes. No parallel pending-request map.
3
11
  *
4
12
  * @module
5
13
  */
6
- import { ThrownJsonrpcError, jsonrpc_error_messages, UNKNOWN_ERROR_MESSAGE, } from '../http/jsonrpc_errors.js';
7
- import { is_jsonrpc_notification, is_jsonrpc_request, is_jsonrpc_response, is_jsonrpc_error_response, to_jsonrpc_message_id, create_jsonrpc_error_response, } from '../http/jsonrpc_helpers.js';
8
- import { RequestTracker } from './request_tracker.svelte.js';
14
+ import { ThrownJsonrpcError, jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
15
+ import { is_jsonrpc_notification, is_jsonrpc_request, to_jsonrpc_message_id, to_jsonrpc_result, create_jsonrpc_response, create_jsonrpc_error_response, } from '../http/jsonrpc_helpers.js';
9
16
  export class FrontendWebsocketTransport {
10
17
  transport_name = 'frontend_websocket_rpc';
11
18
  #connection;
12
19
  #receive;
13
- #request_tracker;
14
20
  #remove_message_handler;
15
21
  #remove_error_handler;
16
- constructor(connection, receive, request_timeout_ms) {
22
+ constructor(connection, receive) {
17
23
  this.#connection = connection;
18
24
  this.#receive = receive;
19
- this.#request_tracker = new RequestTracker(request_timeout_ms);
20
- // TODO maybe we want to do this setup elsewhere, not hardcoded like this
25
+ // Inbound dispatch — only server-pushed requests/notifications need
26
+ // routing here. Responses to requests we sent are correlated by the
27
+ // connection's own `request()` pending map.
21
28
  this.#remove_message_handler = connection.add_message_handler(async (event) => {
22
29
  try {
23
30
  const data = JSON.parse(event.data);
24
- // TODO the `data.id !== null` check should be refactored, maybe we want the "Error Message Response" concept for non-null ids
25
- // Check if this is a response to one of our requests
26
- if (is_jsonrpc_response(data) || (is_jsonrpc_error_response(data) && data.id !== null)) {
27
- // This is a response to a request we sent
28
- this.#request_tracker.handle_message(data);
29
- }
30
- else if (is_jsonrpc_request(data) || is_jsonrpc_notification(data)) {
31
- // This is a new request/notification from the server
31
+ if (is_jsonrpc_request(data) || is_jsonrpc_notification(data)) {
32
32
  await this.#receive(data);
33
33
  }
34
- else {
35
- console.warn('[ws_transport] received unknown message type:', data);
36
- }
34
+ // Responses are owned by `connection.request()` — ignore here.
37
35
  }
38
36
  catch (error) {
39
37
  console.error('[ws_transport] error parsing WebSocket message:', error);
40
- // TODO maybe send the whole thing back wrapped in an error?
41
- // can't reference anything else for a response
42
38
  }
43
39
  });
44
40
  this.#remove_error_handler = connection.add_error_handler((event) => {
45
41
  console.error('[ws_transport] WebSocket error:', event);
46
42
  });
47
43
  }
48
- async send(message) {
44
+ async send(message, options) {
45
+ // Fail-fast at the transport boundary. The connection's own queue
46
+ // would buffer the request and flush on reconnect; that's the right
47
+ // default for direct `client.request()` callers but the typed Proxy
48
+ // path expects "service unavailable" semantics when the WS is down.
49
49
  if (!this.is_ready()) {
50
50
  return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.service_unavailable('WebSocket not connected'));
51
51
  }
52
- try {
53
- // If this is a JSON-RPC request with an id (not a notification), set up request tracking.
54
- if (is_jsonrpc_request(message)) {
55
- // TODO track the whole request?
56
- const deferred = this.#request_tracker.track_request(message.id);
57
- this.#connection.send(message);
58
- // Return the promise that will resolve when the response is received
59
- const result = await deferred.promise;
60
- return result;
52
+ if (is_jsonrpc_request(message)) {
53
+ try {
54
+ const result = await this.#connection.request(message.method, message.params, {
55
+ id: message.id,
56
+ signal: options?.signal,
57
+ queue: false,
58
+ });
59
+ return create_jsonrpc_response(message.id, to_jsonrpc_result(result));
61
60
  }
62
- else if (is_jsonrpc_notification(message)) {
63
- // For notifications, just send without tracking
64
- this.#connection.send(message);
65
- return null;
61
+ catch (error) {
62
+ if (error instanceof ThrownJsonrpcError) {
63
+ return create_jsonrpc_error_response(message.id, {
64
+ code: error.code,
65
+ message: error.message,
66
+ data: error.data,
67
+ });
68
+ }
69
+ return create_jsonrpc_error_response(message.id, jsonrpc_error_messages.internal_error(error instanceof Error ? error.message : String(error)));
66
70
  }
67
- // Invalid message type - return error with id if available
68
- return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.invalid_request());
69
71
  }
70
- catch (error) {
71
- if (error instanceof ThrownJsonrpcError) {
72
- return create_jsonrpc_error_response(to_jsonrpc_message_id(message), {
73
- code: error.code,
74
- message: error.message,
75
- data: error.data,
76
- });
77
- }
78
- return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.internal_error(error.message || UNKNOWN_ERROR_MESSAGE));
72
+ if (is_jsonrpc_notification(message)) {
73
+ this.#connection.send(message);
74
+ return null;
79
75
  }
76
+ return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.invalid_request());
80
77
  }
81
78
  is_ready() {
82
79
  return this.#connection.connected;
@@ -7,7 +7,7 @@
7
7
  import type { WSContext } from 'hono/ws';
8
8
  import type { JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
9
9
  import { type Uuid } from '../uuid.js';
10
- import { type Transport } from './transports.js';
10
+ import { type Transport, type TransportSendOptions } from './transports.js';
11
11
  /**
12
12
  * Auth identity attached to a single WebSocket connection.
13
13
  *
@@ -78,8 +78,8 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
78
78
  * @returns the number of sockets closed
79
79
  */
80
80
  close_sockets_for_token(api_token_id: string): number;
81
- send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
82
- send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
81
+ send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
82
+ send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
83
83
  /**
84
84
  * Broadcast to connections whose identity satisfies a predicate.
85
85
  *
@@ -92,5 +92,14 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
92
92
  */
93
93
  broadcast_filtered(message: JsonrpcMessageFromServerToClient, predicate: (identity: ConnectionIdentity) => boolean): number;
94
94
  is_ready(): boolean;
95
+ /**
96
+ * Number of currently tracked WebSocket connections.
97
+ *
98
+ * Read-only counter intended for telemetry, logging, and tests.
99
+ * Counts every entry in the connection map — including connections
100
+ * that have been closed by the peer but not yet removed by the WS
101
+ * adapter's `onClose` callback.
102
+ */
103
+ get_connection_count(): number;
95
104
  }
96
105
  //# sourceMappingURL=transports_ws_backend.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAc,KAAK,IAAI,EAAC,MAAM,YAAY,CAAC;AAClD,OAAO,EAA2B,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAIzE;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,EAAE,IAAI,CAAC;IACjB,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4BAA6B,SAAQ,SAAS;IAC9D,kBAAkB,EAAE,CACnB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,KAChD,MAAM,CAAC;CACZ;AAED,qDAAqD;AACrD,eAAO,MAAM,iCAAiC,GAC7C,WAAW,SAAS,KAClB,SAAS,IAAI,4BAEqE,CAAC;AAEtF,qBAAa,yBAA0B,YAAW,4BAA4B;;IAC7E,QAAQ,CAAC,cAAc,EAAG,uBAAuB,CAAU;IAY3D;;;;;;;;OAQG;IACH,cAAc,CACb,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,IAAI,EAChB,YAAY,GAAE,MAAM,GAAG,IAAW,GAChC,IAAI;IAQP;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IA0BtC;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIrD;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM;IAInD;;;;;;;;OAQG;IACH,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAsB/C,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA4C9E;;;;;;;;;OASG;IACH,kBAAkB,CACjB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,GAClD,MAAM;IAoBT,QAAQ,IAAI,OAAO;CAGnB"}
1
+ {"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAc,KAAK,IAAI,EAAC,MAAM,YAAY,CAAC;AAClD,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAIpG;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,EAAE,IAAI,CAAC;IACjB,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4BAA6B,SAAQ,SAAS;IAC9D,kBAAkB,EAAE,CACnB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,KAChD,MAAM,CAAC;CACZ;AAED,qDAAqD;AACrD,eAAO,MAAM,iCAAiC,GAC7C,WAAW,SAAS,KAClB,SAAS,IAAI,4BAEqE,CAAC;AAEtF,qBAAa,yBAA0B,YAAW,4BAA4B;;IAC7E,QAAQ,CAAC,cAAc,EAAG,uBAAuB,CAAU;IAY3D;;;;;;;;OAQG;IACH,cAAc,CACb,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,IAAI,EAChB,YAAY,GAAE,MAAM,GAAG,IAAW,GAChC,IAAI;IAQP;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IA0BtC;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIrD;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM;IAInD;;;;;;;;OAQG;IACH,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAsB/C,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA6CvC;;;;;;;;;OASG;IACH,kBAAkB,CACjB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,GAClD,MAAM;IAoBT,QAAQ,IAAI,OAAO;IAInB;;;;;;;OAOG;IACH,oBAAoB,IAAI,MAAM;CAG9B"}
@@ -107,7 +107,7 @@ export class BackendWebsocketTransport {
107
107
  this.#cleanup_connection(connection_id, ws);
108
108
  ws.close(WS_CLOSE_SESSION_REVOKED, 'Session revoked');
109
109
  }
110
- async send(message) {
110
+ async send(message, _options) {
111
111
  // TODO currently just broadcasts all messages to all clients, the transport abstraction is still a WIP
112
112
  if (is_jsonrpc_request(message)) {
113
113
  return create_jsonrpc_error_response(message.id,
@@ -170,4 +170,15 @@ export class BackendWebsocketTransport {
170
170
  is_ready() {
171
171
  return this.#connections.size > 0;
172
172
  }
173
+ /**
174
+ * Number of currently tracked WebSocket connections.
175
+ *
176
+ * Read-only counter intended for telemetry, logging, and tests.
177
+ * Counts every entry in the connection map — including connections
178
+ * that have been closed by the peer but not yet removed by the WS
179
+ * adapter's `onClose` callback.
180
+ */
181
+ get_connection_count() {
182
+ return this.#connections.size;
183
+ }
173
184
  }
@@ -50,7 +50,6 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
50
50
  // Case-insensitive scheme matching per RFC 7235 §2.1 — defense-in-depth:
51
51
  // without this, a `bearer` (lowercase) header silently bypasses token
52
52
  // validation and browser-context rejection instead of being processed.
53
- // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
54
53
  if (!auth_header || auth_header.slice(0, 7).toLowerCase() !== 'bearer ') {
55
54
  await next();
56
55
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"keyring.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC,CAAC;CACrF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAAO,GAAG,IAkCxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAc5E,CAAC;AA6CF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAC/B;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,GAC5B;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAC,CAAC;AAEtC;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,sBAUxE,CAAC"}
1
+ {"version":3,"file":"keyring.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC,CAAC;CACrF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAAO,GAAG,IAiCxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAc5E,CAAC;AA6CF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAC/B;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,GAC5B;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAC,CAAC;AAEtC;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,sBAUxE,CAAC"}
@@ -58,9 +58,7 @@ export const create_keyring = (env_value) => {
58
58
  },
59
59
  async verify(signed_value) {
60
60
  for (let i = 0; i < secrets.length; i++) {
61
- // eslint-disable-next-line no-await-in-loop
62
61
  const key = await get_key(i);
63
- // eslint-disable-next-line no-await-in-loop
64
62
  const result = await verify_with_crypto_key(signed_value, key);
65
63
  if (result !== false) {
66
64
  return { value: result, key_index: i };
@@ -49,23 +49,23 @@ export const AUTH_MIGRATIONS = [
49
49
  await db.query(ACTOR_INDEX);
50
50
  await db.query(PERMIT_SCHEMA);
51
51
  for (const sql of PERMIT_INDEXES) {
52
- await db.query(sql); // eslint-disable-line no-await-in-loop
52
+ await db.query(sql);
53
53
  }
54
54
  await db.query(AUTH_SESSION_SCHEMA);
55
55
  for (const sql of AUTH_SESSION_INDEXES) {
56
- await db.query(sql); // eslint-disable-line no-await-in-loop
56
+ await db.query(sql);
57
57
  }
58
58
  await db.query(API_TOKEN_SCHEMA);
59
59
  await db.query(API_TOKEN_INDEX);
60
60
  await db.query(AUDIT_LOG_SCHEMA);
61
61
  for (const sql of AUDIT_LOG_INDEXES) {
62
- await db.query(sql); // eslint-disable-line no-await-in-loop
62
+ await db.query(sql);
63
63
  }
64
64
  await db.query(BOOTSTRAP_LOCK_SCHEMA);
65
65
  await db.query(BOOTSTRAP_LOCK_SEED);
66
66
  await db.query(INVITE_SCHEMA);
67
67
  for (const sql of INVITE_INDEXES) {
68
- await db.query(sql); // eslint-disable-line no-await-in-loop
68
+ await db.query(sql);
69
69
  }
70
70
  await db.query(APP_SETTINGS_SCHEMA);
71
71
  await db.query(APP_SETTINGS_SEED);
@@ -3,7 +3,11 @@
3
3
  *
4
4
  * Migrations are functions in ordered arrays, grouped by namespace.
5
5
  * A `schema_version` table tracks progress per namespace.
6
- * Each migration runs in its own transaction.
6
+ *
7
+ * **Chain-level transactions**: All pending migrations in a namespace run in a
8
+ * single transaction. Any failure rolls back every migration in that run —
9
+ * no partial-state recovery. This rules out non-transactional DDL (e.g.,
10
+ * `CREATE INDEX CONCURRENTLY`); run those out of band.
7
11
  *
8
12
  * **Forward-only**: No down-migrations. Schema changes are additive.
9
13
  * For pre-release development, collapse migrations into a single v0.
@@ -53,9 +57,15 @@ export interface MigrationResult {
53
57
  *
54
58
  * Creates the `schema_version` tracking table if it does not exist,
55
59
  * then for each namespace: acquires an advisory lock, reads the current
56
- * version, runs pending migrations in order (each in its own transaction),
60
+ * version, runs all pending migrations in order inside a single transaction,
57
61
  * updates the stored version, and releases the lock.
58
62
  *
63
+ * **Atomicity**: The pending chain for each namespace runs in one transaction —
64
+ * any failure rolls back every migration that ran in that invocation. The
65
+ * next run starts from the previously-stored version, re-running the whole
66
+ * (fixed) chain. Namespaces are independent: a later namespace's failure
67
+ * does not roll back an earlier namespace that already committed.
68
+ *
59
69
  * **Concurrency**: Uses PostgreSQL advisory locks to serialize concurrent
60
70
  * callers on the same namespace. Safe for multi-instance deployments.
61
71
  *
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAEhC;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAC,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AA8BD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAC1B,IAAI,EAAE,EACN,YAAY,KAAK,CAAC,kBAAkB,CAAC,KACnC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CA2EhC,CAAC"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAEhC;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAC,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AA8BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,cAAc,GAC1B,IAAI,EAAE,EACN,YAAY,KAAK,CAAC,kBAAkB,CAAC,KACnC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CA0EhC,CAAC"}