@fuzdev/fuz_app 0.27.0 → 0.29.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.
@@ -7,28 +7,29 @@
7
7
  * @module
8
8
  */
9
9
  import { JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
10
- import { Transports, type TransportName } from './transports.js';
10
+ import { Transports, type TransportName, type TransportSendOptions } from './transports.js';
11
11
  import type { ActionEventEnvironment } from './action_event_types.js';
12
- export interface ActionPeerSendOptions {
12
+ /**
13
+ * Per-call options for `ActionPeer.send`. Extends `TransportSendOptions`
14
+ * with `transport_name` for per-call transport selection. The peer-wide
15
+ * default for any field lives on `ActionPeerOptions.default_send_options` —
16
+ * set `queue: true` there once for client-authoritative peers and override
17
+ * per-call for exceptions (e.g. high-frequency position sync where stale
18
+ * replays are wrong).
19
+ */
20
+ export interface ActionPeerSendOptions extends TransportSendOptions {
13
21
  transport_name?: TransportName;
14
- /**
15
- * Per-call `AbortSignal`. Forwarded into the chosen transport's `send`,
16
- * which bottoms out at `FrontendWebsocketClient.request({signal})` for WS
17
- * (sends the shared `cancel` notification on abort) and at
18
- * `fetch({signal})` for HTTP. Backend transport ignores it.
19
- */
20
- signal?: AbortSignal;
21
22
  }
22
23
  export interface ActionPeerOptions {
23
24
  environment: ActionEventEnvironment;
24
25
  transports?: Transports;
25
- default_send_options?: Partial<ActionPeerSendOptions>;
26
+ default_send_options?: Omit<ActionPeerSendOptions, 'signal'>;
26
27
  }
27
28
  export declare class ActionPeer {
28
29
  #private;
29
30
  readonly environment: ActionEventEnvironment;
30
31
  readonly transports: Transports;
31
- default_send_options: ActionPeerSendOptions;
32
+ default_send_options: Omit<ActionPeerSendOptions, 'signal'>;
32
33
  constructor(options: ActionPeerOptions);
33
34
  send(message: JsonrpcRequest, options?: ActionPeerSendOptions): Promise<JsonrpcResponseOrError>;
34
35
  send(message: JsonrpcNotification, options?: ActionPeerSendOptions): Promise<JsonrpcErrorResponse | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"action_peer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_peer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEN,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,EAAC,UAAU,EAAE,KAAK,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,MAAM,WAAW,qBAAqB;IACrC,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,sBAAsB,CAAC;IAGpC,UAAU,CAAC,EAAE,UAAU,CAAC;IAGxB,oBAAoB,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACtD;AAED,qBAAa,UAAU;;IACtB,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAMhC,oBAAoB,EAAE,qBAAqB,CAAC;gBAEhC,OAAO,EAAE,iBAAiB;IAOhC,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA2CjC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC;CAyIjF"}
1
+ {"version":3,"file":"action_peer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_peer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEN,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,EAAC,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAC1F,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IAClE,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,sBAAsB,CAAC;IAGpC,UAAU,CAAC,EAAE,UAAU,CAAC;IAKxB,oBAAoB,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;CAC7D;AAED,qBAAa,UAAU;;IACtB,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAMhC,oBAAoB,EAAE,IAAI,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;gBAEhD,OAAO,EAAE,iBAAiB;IAOhC,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA8CjC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC;CAyIjF"}
@@ -33,7 +33,10 @@ export class ActionPeer {
33
33
  }
34
34
  const message_type = is_jsonrpc_request(message) ? 'request' : 'notification';
35
35
  this.environment.log?.debug(`[peer] send ${message_type}:`, message.method, `via ${transport.transport_name}`);
36
- const result = await transport.send(message, { signal: options?.signal });
36
+ const result = await transport.send(message, {
37
+ signal: options?.signal,
38
+ queue: options?.queue ?? this.default_send_options.queue,
39
+ });
37
40
  if (result && 'error' in result) {
38
41
  this.environment.log?.error(`[peer] send ${message_type} failed:`, message.method, result.error.message);
39
42
  }
@@ -1,5 +1,11 @@
1
1
  /**
2
- * WebSocket JSON-RPC dispatch — the canonical WS transport binding.
2
+ * WebSocket JSON-RPC dispatch — the low-level WS transport binding.
3
+ *
4
+ * Most consumers should mount WS endpoints via `register_ws_endpoint`
5
+ * (`./register_ws_endpoint.js`), which wraps this function with the standard
6
+ * upgrade stack (origin check + auth + optional role). This module stays
7
+ * exported as the lower-level entry point for tests that drive the
8
+ * dispatcher directly via `create_ws_test_harness`.
3
9
  *
4
10
  * Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
5
11
  * consumer supplies action specs + a handler map, the dispatcher parses the
@@ -15,9 +21,10 @@
15
21
  * ## Auth expectations
16
22
  *
17
23
  * The consumer is responsible for rejecting unauthenticated upgrades *before*
18
- * routing to this handler (fuz_app's `require_auth` middleware). Inside the
19
- * dispatcher, `get_request_context(c)` is treated as guaranteed non-null and
20
- * per-action auth is enforced on each message.
24
+ * routing to this handler (fuz_app's `require_auth` middleware, or
25
+ * `register_ws_endpoint` which wires it for you). Inside the dispatcher,
26
+ * `get_request_context(c)` is treated as guaranteed non-null and per-action
27
+ * auth is enforced on each message.
21
28
  *
22
29
  * @module
23
30
  */
@@ -1 +1 @@
1
- {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAgB1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AAErC,OAAO,EAAC,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAG7F,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAE9F,YAAY,EAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,EAAC,CAAC;AAE1D,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,kBAAkB;IACvE,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;OAMG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC;;;;;OAKG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,SAAS,kBAAkB,EACjE,SAAS,uBAAuB,CAAC,IAAI,CAAC,KACpC,sBA8WF,CAAC"}
1
+ {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAgB1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AAErC,OAAO,EAAC,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAG7F,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAE9F,YAAY,EAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,EAAC,CAAC;AAE1D,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,kBAAkB;IACvE,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;OAMG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC;;;;;OAKG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,SAAS,kBAAkB,EACjE,SAAS,uBAAuB,CAAC,IAAI,CAAC,KACpC,sBA8WF,CAAC"}
@@ -1,5 +1,11 @@
1
1
  /**
2
- * WebSocket JSON-RPC dispatch — the canonical WS transport binding.
2
+ * WebSocket JSON-RPC dispatch — the low-level WS transport binding.
3
+ *
4
+ * Most consumers should mount WS endpoints via `register_ws_endpoint`
5
+ * (`./register_ws_endpoint.js`), which wraps this function with the standard
6
+ * upgrade stack (origin check + auth + optional role). This module stays
7
+ * exported as the lower-level entry point for tests that drive the
8
+ * dispatcher directly via `create_ws_test_harness`.
3
9
  *
4
10
  * Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
5
11
  * consumer supplies action specs + a handler map, the dispatcher parses the
@@ -15,9 +21,10 @@
15
21
  * ## Auth expectations
16
22
  *
17
23
  * The consumer is responsible for rejecting unauthenticated upgrades *before*
18
- * routing to this handler (fuz_app's `require_auth` middleware). Inside the
19
- * dispatcher, `get_request_context(c)` is treated as guaranteed non-null and
20
- * per-action auth is enforced on each message.
24
+ * routing to this handler (fuz_app's `require_auth` middleware, or
25
+ * `register_ws_endpoint` which wires it for you). Inside the dispatcher,
26
+ * `get_request_context(c)` is treated as guaranteed non-null and per-action
27
+ * auth is enforced on each message.
21
28
  *
22
29
  * @module
23
30
  */
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Composed WebSocket endpoint registration — the idiomatic consumer entry
3
+ * point for mounting a fuz_app WS endpoint.
4
+ *
5
+ * Wraps the standard upgrade stack every consumer writes by hand:
6
+ *
7
+ * 1. `verify_request_source(allowed_origins)` — reject disallowed origins
8
+ * before the upgrade handshake runs.
9
+ * 2. `require_auth` — reject unauthenticated upgrades.
10
+ * 3. Optional `require_role(required_role)` — for endpoints gated to a
11
+ * specific role.
12
+ *
13
+ * Then delegates to {@link register_action_ws} for per-message JSON-RPC
14
+ * dispatch.
15
+ *
16
+ * @module
17
+ */
18
+ import type { RoleName } from '../auth/role_schema.js';
19
+ import { type RegisterActionWsOptions, type RegisterActionWsResult } from './register_action_ws.js';
20
+ import type { BaseHandlerContext } from './action_types.js';
21
+ /** Options for {@link register_ws_endpoint}. */
22
+ export interface RegisterWsEndpointOptions<TCtx extends BaseHandlerContext> extends RegisterActionWsOptions<TCtx> {
23
+ /**
24
+ * Origin allowlist regexes — typically parsed from the `ALLOWED_ORIGINS`
25
+ * env var via `parse_allowed_origins`. Passed straight to
26
+ * `verify_request_source`.
27
+ */
28
+ allowed_origins: Array<RegExp>;
29
+ /**
30
+ * Role required to upgrade. Omit for any authenticated account (`require_auth`
31
+ * alone); set to e.g. `ROLE_ADMIN` to gate the endpoint behind a role. The
32
+ * per-action `auth` in each spec still applies at dispatch time — this is
33
+ * a coarse upgrade-time gate.
34
+ */
35
+ required_role?: RoleName;
36
+ }
37
+ /**
38
+ * Mount a WebSocket endpoint with the standard upgrade stack (origin check
39
+ * + auth + optional role) and JSON-RPC dispatch.
40
+ *
41
+ * Returns the {@link BackendWebsocketTransport} (supplied or freshly
42
+ * created), same as {@link register_action_ws} — retain it to wire
43
+ * `create_ws_auth_guard` on `on_audit_event` or to broadcast.
44
+ */
45
+ export declare const register_ws_endpoint: <TCtx extends BaseHandlerContext>(options: RegisterWsEndpointOptions<TCtx>) => RegisterActionWsResult;
46
+ //# sourceMappingURL=register_ws_endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAE1D,gDAAgD;AAChD,MAAM,WAAW,yBAAyB,CACzC,IAAI,SAAS,kBAAkB,CAC9B,SAAQ,uBAAuB,CAAC,IAAI,CAAC;IACtC;;;;OAIG;IACH,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,IAAI,SAAS,kBAAkB,EACnE,SAAS,yBAAyB,CAAC,IAAI,CAAC,KACtC,sBAUF,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Composed WebSocket endpoint registration — the idiomatic consumer entry
3
+ * point for mounting a fuz_app WS endpoint.
4
+ *
5
+ * Wraps the standard upgrade stack every consumer writes by hand:
6
+ *
7
+ * 1. `verify_request_source(allowed_origins)` — reject disallowed origins
8
+ * before the upgrade handshake runs.
9
+ * 2. `require_auth` — reject unauthenticated upgrades.
10
+ * 3. Optional `require_role(required_role)` — for endpoints gated to a
11
+ * specific role.
12
+ *
13
+ * Then delegates to {@link register_action_ws} for per-message JSON-RPC
14
+ * dispatch.
15
+ *
16
+ * @module
17
+ */
18
+ import { Logger } from '@fuzdev/fuz_util/log.js';
19
+ import { require_auth, require_role } from '../auth/request_context.js';
20
+ import { verify_request_source } from '../http/origin.js';
21
+ import { register_action_ws, } from './register_action_ws.js';
22
+ /**
23
+ * Mount a WebSocket endpoint with the standard upgrade stack (origin check
24
+ * + auth + optional role) and JSON-RPC dispatch.
25
+ *
26
+ * Returns the {@link BackendWebsocketTransport} (supplied or freshly
27
+ * created), same as {@link register_action_ws} — retain it to wire
28
+ * `create_ws_auth_guard` on `on_audit_event` or to broadcast.
29
+ */
30
+ export const register_ws_endpoint = (options) => {
31
+ const { app, path, allowed_origins, required_role, log = new Logger('[ws]'), ...rest } = options;
32
+ app.use(path, verify_request_source(allowed_origins));
33
+ app.use(path, require_auth);
34
+ if (required_role !== undefined) {
35
+ app.use(path, require_role(required_role));
36
+ }
37
+ return register_action_ws({ app, path, log, ...rest });
38
+ };
@@ -10,7 +10,7 @@
10
10
  * @module
11
11
  */
12
12
  import type { ActionEventEnvironment } from './action_event_types.js';
13
- import type { ActionPeer } from './action_peer.js';
13
+ import type { ActionPeer, ActionPeerSendOptions } from './action_peer.js';
14
14
  import type { ActionEventDataUnion } from './action_event_data.js';
15
15
  import type { TransportName } from './transports.js';
16
16
  /**
@@ -58,13 +58,11 @@ export interface CreateRpcClientOptions {
58
58
  */
59
59
  export declare const create_rpc_client: (options: CreateRpcClientOptions) => Record<string, (...args: Array<any>) => any>;
60
60
  /**
61
- * Per-call options accepted by every typed Proxy method. `signal` lets the
62
- * caller cancel an in-flight request (sends the shared `cancel` notification
63
- * on the WS path, aborts `fetch` on HTTP). `transport_name` overrides the
64
- * per-method `transport_for_method` selector for this call.
61
+ * Per-call options accepted by every typed Proxy method. Same shape as
62
+ * `ActionPeerSendOptions` the client threads these through unchanged
63
+ * to the underlying peer. `transport_name` overrides the per-method
64
+ * `transport_for_method` selector for this call.
65
65
  */
66
- export interface RpcClientCallOptions {
67
- signal?: AbortSignal;
68
- transport_name?: TransportName;
66
+ export interface RpcClientCallOptions extends ActionPeerSendOptions {
69
67
  }
70
68
  //# sourceMappingURL=rpc_client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAGnD;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAC;AAM/E,8EAA8E;AAC9E,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,oBAAoB,CAAA;KAAC,KAC5E;QACA,sBAAsB,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;KAC5C,GACD,SAAS,CAAC;CACb;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,sBAAsB,CAAC;IACpC,kEAAkE;IAClE,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,sBAAsB,KAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAgB7C,CAAC;AA2DF;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B"}
1
+ {"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAGnD;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAC;AAM/E,8EAA8E;AAC9E,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,oBAAoB,CAAA;KAAC,KAC5E;QACA,sBAAsB,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;KAC5C,GACD,SAAS,CAAC;CACb;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,sBAAsB,CAAC;IACpC,kEAAkE;IAClE,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,sBAAsB,KAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAgB7C,CAAC;AA2DF;;;;;GAKG;AACH,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;CAAG"}
@@ -127,6 +127,7 @@ const create_request_response_method = (peer, environment, spec, actions, transp
127
127
  const response = await peer.send(event.data.request, {
128
128
  transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
129
129
  signal: options?.signal,
130
+ queue: options?.queue,
130
131
  });
131
132
  event.transition('receive_response');
132
133
  // TODO @api shouldn't this happen in the peer like the other method calls?
@@ -155,6 +156,7 @@ const create_remote_notification_method = (peer, environment, spec, actions, tra
155
156
  const send_result = await peer.send(event.data.notification, {
156
157
  transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
157
158
  signal: options?.signal,
159
+ queue: options?.queue,
158
160
  });
159
161
  // Check if notification failed to send
160
162
  if (send_result !== null) {
@@ -24,6 +24,7 @@
24
24
  * @module
25
25
  */
26
26
  import type { Logger } from '@fuzdev/fuz_util/log.js';
27
+ import type { AsyncStatus } from '@fuzdev/fuz_util/async.js';
27
28
  import { type JsonrpcRequestId } from '../http/jsonrpc.js';
28
29
  import type { WebsocketConnection } from './transports_ws.js';
29
30
  /** Default WebSocket close code (normal closure). */
@@ -91,11 +92,11 @@ export interface FrontendWebsocketClientOptions {
91
92
  */
92
93
  reconnect?: boolean | FrontendWebsocketReconnectOptions | null;
93
94
  /**
94
- * Activity-aware heartbeat. `true` or omit for defaults; `false` disables
95
+ * Activity-aware heartbeat. `true`/`null`/omit for defaults; `false` disables
95
96
  * the timer entirely (only do this if the server side is also running
96
97
  * without heartbeat); pass an object to tune `interval` / `receive_timeout`.
97
98
  */
98
- heartbeat?: boolean | FrontendWebsocketHeartbeatOptions;
99
+ heartbeat?: boolean | FrontendWebsocketHeartbeatOptions | null;
99
100
  /**
100
101
  * Durable queue for {@link FrontendWebsocketClient.request}. `true` or omit
101
102
  * for defaults; `false` disables buffering (requests while disconnected
@@ -163,6 +164,33 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
163
164
  * does not synthesize a reconnect — wait for the next close.
164
165
  */
165
166
  set_reconnect(reconnect?: boolean | FrontendWebsocketReconnectOptions | null): void;
167
+ /**
168
+ * Swap the heartbeat policy in place. Accepts the same shape as the
169
+ * constructor's `heartbeat` option: `false` disables the timer, `true` or
170
+ * `null`/omitted restores the defaults, or a config object customizes
171
+ * specific fields (missing fields fall back to defaults, not "keep
172
+ * current" — each call defines the whole policy atomically, same as the
173
+ * constructor and {@link set_reconnect}).
174
+ *
175
+ * When connected, the live timer is restarted immediately so the new
176
+ * `interval` / `receive_timeout` take effect without a reconnect; when
177
+ * disconnected, just stashes the policy for the next open.
178
+ */
179
+ set_heartbeat(heartbeat?: boolean | FrontendWebsocketHeartbeatOptions | null): void;
180
+ /**
181
+ * Cancel a scheduled reconnect without closing the client or disabling
182
+ * auto-reconnect. Transitions status from `reconnecting` → `closed` and
183
+ * resets the backoff counters — the next close still triggers a fresh
184
+ * reconnect cycle under the current policy. No-op when no reconnect is
185
+ * pending.
186
+ *
187
+ * Use this when UI state asks "stop trying for now" without the finality
188
+ * of {@link disconnect} (which also rejects pending/queued requests and
189
+ * clears heartbeat) or the policy change of `set_reconnect(false)`
190
+ * (which disables future reconnects). The queue stays intact so that
191
+ * calling {@link connect} later flushes buffered work.
192
+ */
193
+ cancel_reconnect(): void;
166
194
  get url(): string;
167
195
  /**
168
196
  * Whether the server has permanently closed the session. Once `true`, all
@@ -190,8 +218,7 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
190
218
  * id (or uses an explicit one supplied via `options.id` — used by
191
219
  * `FrontendWebsocketTransport` which delegates to this method and has its
192
220
  * 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).
221
+ * server sends a matching response.
195
222
  *
196
223
  * Callers supplying an explicit `options.id` are responsible for
197
224
  * uniqueness — the pending map is keyed by id, and a duplicate silently
@@ -206,10 +233,22 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
206
233
  * disconnect-detection slot.
207
234
  *
208
235
  * 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
236
+ * `cancel` notification ({@link CANCEL_METHOD}) so the server-side
237
+ * dispatcher can abort the matching handler's `ctx.signal`. Suppressed
238
+ * for queued-but-never-sent (server doesn't know about it) and
212
239
  * response-beat-cancel races.
240
+ *
241
+ * Rejections throw `ThrownJsonrpcError` with a specific code so
242
+ * `FrontendWebsocketTransport` can preserve the code verbatim in its
243
+ * error envelope rather than collapsing every rejection to
244
+ * `internal_error`:
245
+ * - `unauthenticated` — session revoked (entry check or close code);
246
+ * - `request_cancelled` — caller's `AbortSignal` fired;
247
+ * - `queue_overflow` — durable queue full;
248
+ * - `service_unavailable` — socket not connected / closed / torn down
249
+ * mid-flight;
250
+ * - `internal_error` — `ws.send` threw (serialization, buffer full);
251
+ * - server's wire code verbatim — JSON-RPC error frame from peer.
213
252
  */
214
253
  request<R = unknown>(method: string, params?: unknown, options?: {
215
254
  signal?: AbortSignal;
@@ -219,4 +258,17 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
219
258
  add_message_handler(handler: SocketMessageHandler): () => void;
220
259
  add_error_handler(handler: SocketErrorHandler): () => void;
221
260
  }
261
+ /**
262
+ * Project {@link SocketStatus} onto fuz_util's {@link AsyncStatus} — the
263
+ * 5-way → 4-way mapping every consumer re-derives to surface connection state
264
+ * to UI (loading indicators, retry banners). Collapses `reconnecting` into
265
+ * `failure` (UI shows "lost, retrying") and splits `closed` by `revoked` so
266
+ * a terminal session-revocation read as `failure` while a clean client-
267
+ * initiated close reads as `initial` (the "not connected, not trying" state).
268
+ *
269
+ * @param status - the socket's current {@link SocketStatus}
270
+ * @param revoked - whether the session has been permanently revoked
271
+ * (typically `FrontendWebsocketClient.revoked`)
272
+ */
273
+ export declare const socket_status_to_async_status: (status: SocketStatus, revoked: boolean) => AsyncStatus;
222
274
  //# sourceMappingURL=socket.svelte.d.ts.map
@@ -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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"}
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { BROWSER } from 'esm-env';
27
27
  import { JSONRPC_VERSION } from '../http/jsonrpc.js';
28
+ import { JSONRPC_ERROR_CODES, ThrownJsonrpcError, jsonrpc_errors } from '../http/jsonrpc_errors.js';
28
29
  import { WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, WS_CLOSE_SESSION_REVOKED } from './transports.js';
29
30
  import { CANCEL_METHOD } from './cancel.js';
30
31
  import { HEARTBEAT_METHOD } from './heartbeat.js';
@@ -111,7 +112,7 @@ export class FrontendWebsocketClient {
111
112
  this.#backoff_factor = config.factor ?? DEFAULT_BACKOFF_FACTOR;
112
113
  const heartbeat = options.heartbeat;
113
114
  this.#heartbeat_enabled = heartbeat !== false;
114
- const heartbeat_config = typeof heartbeat === 'object' ? heartbeat : {};
115
+ const heartbeat_config = typeof heartbeat === 'object' && heartbeat !== null ? heartbeat : {};
115
116
  this.#heartbeat_interval = heartbeat_config.interval ?? DEFAULT_HEARTBEAT_INTERVAL;
116
117
  this.#heartbeat_receive_timeout =
117
118
  heartbeat_config.receive_timeout ?? DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT;
@@ -152,8 +153,7 @@ export class FrontendWebsocketClient {
152
153
  if (!next_auto) {
153
154
  this.#cancel_reconnect();
154
155
  this.status = 'closed';
155
- this.reconnect_count = 0;
156
- this.current_reconnect_delay = 0;
156
+ this.#reset_reconnect_counters();
157
157
  return;
158
158
  }
159
159
  // Auto-reconnect still on: monotonically shorten the pending wait if
@@ -176,6 +176,50 @@ export class FrontendWebsocketClient {
176
176
  this.connect();
177
177
  }, new_remaining);
178
178
  }
179
+ /**
180
+ * Swap the heartbeat policy in place. Accepts the same shape as the
181
+ * constructor's `heartbeat` option: `false` disables the timer, `true` or
182
+ * `null`/omitted restores the defaults, or a config object customizes
183
+ * specific fields (missing fields fall back to defaults, not "keep
184
+ * current" — each call defines the whole policy atomically, same as the
185
+ * constructor and {@link set_reconnect}).
186
+ *
187
+ * When connected, the live timer is restarted immediately so the new
188
+ * `interval` / `receive_timeout` take effect without a reconnect; when
189
+ * disconnected, just stashes the policy for the next open.
190
+ */
191
+ set_heartbeat(heartbeat = null) {
192
+ this.#heartbeat_enabled = heartbeat !== false;
193
+ const config = typeof heartbeat === 'object' && heartbeat !== null ? heartbeat : {};
194
+ this.#heartbeat_interval = config.interval ?? DEFAULT_HEARTBEAT_INTERVAL;
195
+ this.#heartbeat_receive_timeout = config.receive_timeout ?? DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT;
196
+ if (this.connected) {
197
+ this.#start_heartbeat();
198
+ }
199
+ else {
200
+ this.#cancel_heartbeat();
201
+ }
202
+ }
203
+ /**
204
+ * Cancel a scheduled reconnect without closing the client or disabling
205
+ * auto-reconnect. Transitions status from `reconnecting` → `closed` and
206
+ * resets the backoff counters — the next close still triggers a fresh
207
+ * reconnect cycle under the current policy. No-op when no reconnect is
208
+ * pending.
209
+ *
210
+ * Use this when UI state asks "stop trying for now" without the finality
211
+ * of {@link disconnect} (which also rejects pending/queued requests and
212
+ * clears heartbeat) or the policy change of `set_reconnect(false)`
213
+ * (which disables future reconnects). The queue stays intact so that
214
+ * calling {@link connect} later flushes buffered work.
215
+ */
216
+ cancel_reconnect() {
217
+ if (this.#reconnect_timeout === null)
218
+ return;
219
+ this.#cancel_reconnect();
220
+ this.status = 'closed';
221
+ this.#reset_reconnect_counters();
222
+ }
179
223
  get url() {
180
224
  return this.#url;
181
225
  }
@@ -229,9 +273,8 @@ export class FrontendWebsocketClient {
229
273
  this.#cancel_heartbeat();
230
274
  this.#teardown(code);
231
275
  this.status = 'closed';
232
- this.reconnect_count = 0;
233
- this.current_reconnect_delay = 0;
234
- this.#reject_all('client disconnected');
276
+ this.#reset_reconnect_counters();
277
+ this.#reject_all('client disconnected', jsonrpc_errors.service_unavailable);
235
278
  }
236
279
  /** Explicit-resource-management hook — supports `using client = new FrontendWebsocketClient(url)`. */
237
280
  [Symbol.dispose]() {
@@ -257,8 +300,7 @@ export class FrontendWebsocketClient {
257
300
  * id (or uses an explicit one supplied via `options.id` — used by
258
301
  * `FrontendWebsocketTransport` which delegates to this method and has its
259
302
  * 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).
303
+ * server sends a matching response.
262
304
  *
263
305
  * Callers supplying an explicit `options.id` are responsible for
264
306
  * uniqueness — the pending map is keyed by id, and a duplicate silently
@@ -273,17 +315,29 @@ export class FrontendWebsocketClient {
273
315
  * disconnect-detection slot.
274
316
  *
275
317
  * 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
318
+ * `cancel` notification ({@link CANCEL_METHOD}) so the server-side
319
+ * dispatcher can abort the matching handler's `ctx.signal`. Suppressed
320
+ * for queued-but-never-sent (server doesn't know about it) and
279
321
  * response-beat-cancel races.
322
+ *
323
+ * Rejections throw `ThrownJsonrpcError` with a specific code so
324
+ * `FrontendWebsocketTransport` can preserve the code verbatim in its
325
+ * error envelope rather than collapsing every rejection to
326
+ * `internal_error`:
327
+ * - `unauthenticated` — session revoked (entry check or close code);
328
+ * - `request_cancelled` — caller's `AbortSignal` fired;
329
+ * - `queue_overflow` — durable queue full;
330
+ * - `service_unavailable` — socket not connected / closed / torn down
331
+ * mid-flight;
332
+ * - `internal_error` — `ws.send` threw (serialization, buffer full);
333
+ * - server's wire code verbatim — JSON-RPC error frame from peer.
280
334
  */
281
335
  request(method, params = {}, options = {}) {
282
336
  return new Promise((resolve, reject) => {
283
337
  const resolve_typed = resolve;
284
338
  const reject_typed = reject;
285
339
  if (this.#revoked) {
286
- reject_typed(new Error('[socket] session revoked'));
340
+ reject_typed(jsonrpc_errors.unauthenticated('[socket] session revoked'));
287
341
  return;
288
342
  }
289
343
  const { signal = null } = options;
@@ -336,7 +390,7 @@ export class FrontendWebsocketClient {
336
390
  return;
337
391
  }
338
392
  this.#detach_signal(pending);
339
- reject_typed(new Error(`[socket] send failed for ${method}`));
393
+ reject_typed(jsonrpc_errors.internal_error(`[socket] send failed for ${method}`));
340
394
  return;
341
395
  }
342
396
  if (should_queue) {
@@ -344,7 +398,7 @@ export class FrontendWebsocketClient {
344
398
  return;
345
399
  }
346
400
  this.#detach_signal(pending);
347
- reject_typed(new Error(`[socket] not connected (method=${method})`));
401
+ reject_typed(jsonrpc_errors.service_unavailable(`[socket] not connected (method=${method})`));
348
402
  });
349
403
  }
350
404
  add_message_handler(handler) {
@@ -356,7 +410,7 @@ export class FrontendWebsocketClient {
356
410
  return () => this.#error_handlers.delete(handler);
357
411
  }
358
412
  #build_abort_error(method) {
359
- return new Error(`[socket] request aborted (method=${method})`);
413
+ return jsonrpc_errors.request_cancelled(`[socket] request aborted (method=${method})`);
360
414
  }
361
415
  /**
362
416
  * Fire-and-forget cancel notification to the server. The dispatcher
@@ -379,7 +433,7 @@ export class FrontendWebsocketClient {
379
433
  #enqueue(queued) {
380
434
  if (this.#queue.length >= this.#queue_max_size) {
381
435
  this.#detach_signal(queued);
382
- queued.reject(new Error(`[socket] request queue overflow (method=${queued.method}, max=${this.#queue_max_size})`));
436
+ queued.reject(jsonrpc_errors.queue_overflow(`[socket] request queue overflow (method=${queued.method}, max=${this.#queue_max_size})`));
383
437
  return;
384
438
  }
385
439
  this.#queue.push(queued);
@@ -412,25 +466,25 @@ export class FrontendWebsocketClient {
412
466
  }
413
467
  else {
414
468
  this.#detach_signal(q);
415
- q.reject(new Error(`[socket] queued request send failed (method=${q.method})`));
469
+ q.reject(jsonrpc_errors.internal_error(`[socket] queued request send failed (method=${q.method})`));
416
470
  }
417
471
  }
418
472
  }
419
- #reject_all(reason) {
473
+ #reject_all(reason, make_error) {
420
474
  const pending = this.#pending;
421
475
  this.#pending = new Map();
422
476
  for (const [id, p] of pending) {
423
477
  this.#detach_signal(p);
424
- p.reject(new Error(`[socket] ${reason} (method=${p.method}, id=${id})`));
478
+ p.reject(make_error(`[socket] ${reason} (method=${p.method}, id=${id})`));
425
479
  }
426
480
  const queued = this.#queue;
427
481
  this.#queue = [];
428
482
  for (const q of queued) {
429
483
  this.#detach_signal(q);
430
- q.reject(new Error(`[socket] ${reason} (method=${q.method})`));
484
+ q.reject(make_error(`[socket] ${reason} (method=${q.method})`));
431
485
  }
432
486
  }
433
- #reject_pending_only(reason) {
487
+ #reject_pending_only(reason, make_error) {
434
488
  // Socket closed but auto-reconnect will try again — pending requests were
435
489
  // in flight on the old socket so we can't correlate them after reopen;
436
490
  // queued requests haven't been sent yet and stay buffered for the flush.
@@ -438,7 +492,7 @@ export class FrontendWebsocketClient {
438
492
  this.#pending = new Map();
439
493
  for (const [id, p] of pending) {
440
494
  this.#detach_signal(p);
441
- p.reject(new Error(`[socket] ${reason} (method=${p.method}, id=${id})`));
495
+ p.reject(make_error(`[socket] ${reason} (method=${p.method}, id=${id})`));
442
496
  }
443
497
  }
444
498
  #start_heartbeat() {
@@ -505,7 +559,7 @@ export class FrontendWebsocketClient {
505
559
  // record it here so the client-initiated close is still observable,
506
560
  // and reject any pending requests that can never resolve now.
507
561
  this.#record_close(close_code, '');
508
- this.#reject_pending_only(`socket torn down (code ${close_code})`);
562
+ this.#reject_pending_only(`socket torn down (code ${close_code})`, jsonrpc_errors.service_unavailable);
509
563
  }
510
564
  this.ws = null;
511
565
  }
@@ -535,10 +589,14 @@ export class FrontendWebsocketClient {
535
589
  }
536
590
  this.#reconnect_scheduled_at = null;
537
591
  }
538
- #handle_open = (_event) => {
539
- this.status = 'connected';
592
+ /** Reset the reactive reconnect counters — the pair always travels together. */
593
+ #reset_reconnect_counters() {
540
594
  this.reconnect_count = 0;
541
595
  this.current_reconnect_delay = 0;
596
+ }
597
+ #handle_open = (_event) => {
598
+ this.status = 'connected';
599
+ this.#reset_reconnect_counters();
542
600
  this.last_connect_time = Date.now();
543
601
  this.#cancel_reconnect();
544
602
  this.#start_heartbeat();
@@ -556,14 +614,13 @@ export class FrontendWebsocketClient {
556
614
  this.#revoked = true;
557
615
  this.status = 'closed';
558
616
  this.#cancel_reconnect();
559
- this.reconnect_count = 0;
560
- this.current_reconnect_delay = 0;
561
- this.#reject_all('session revoked');
617
+ this.#reset_reconnect_counters();
618
+ this.#reject_all('session revoked', jsonrpc_errors.unauthenticated);
562
619
  return;
563
620
  }
564
621
  // Pending in-flight requests can't be correlated post-reconnect; reject
565
622
  // them. Queue stays so the flush on reopen replays unsent work.
566
- this.#reject_pending_only(`connection closed (code ${event.code})`);
623
+ this.#reject_pending_only(`connection closed (code ${event.code})`, jsonrpc_errors.service_unavailable);
567
624
  // Let `#schedule_reconnect` set `status: 'reconnecting'` directly to avoid
568
625
  // a transient `'closed'` flicker; only set `'closed'` when reconnect is off.
569
626
  if (this.#auto_reconnect) {
@@ -571,7 +628,7 @@ export class FrontendWebsocketClient {
571
628
  }
572
629
  else {
573
630
  this.status = 'closed';
574
- this.#reject_all('connection closed, auto-reconnect disabled');
631
+ this.#reject_all('connection closed, auto-reconnect disabled', jsonrpc_errors.service_unavailable);
575
632
  }
576
633
  };
577
634
  #handle_error = (event) => {
@@ -611,7 +668,16 @@ export class FrontendWebsocketClient {
611
668
  this.#detach_signal(pending);
612
669
  if ('error' in json && json.error) {
613
670
  const err = json.error;
614
- pending.reject(new Error(`[rpc ${pending.method} #${id}] ${err.code ?? '?'} ${err.message ?? 'unknown error'}`));
671
+ // Preserve the server's wire code verbatim so the transport's
672
+ // catch block re-emits the same code in its envelope. Fall
673
+ // back to `internal_error` only when the frame is malformed.
674
+ const wire_code = typeof err.code === 'number'
675
+ ? err.code
676
+ : JSONRPC_ERROR_CODES.internal_error;
677
+ const wire_message = typeof err.message === 'string' && err.message.length > 0
678
+ ? err.message
679
+ : 'unknown error';
680
+ pending.reject(new ThrownJsonrpcError(wire_code, wire_message, err.data));
615
681
  }
616
682
  else {
617
683
  pending.resolve(json.result);
@@ -630,3 +696,29 @@ export class FrontendWebsocketClient {
630
696
  }
631
697
  };
632
698
  }
699
+ /**
700
+ * Project {@link SocketStatus} onto fuz_util's {@link AsyncStatus} — the
701
+ * 5-way → 4-way mapping every consumer re-derives to surface connection state
702
+ * to UI (loading indicators, retry banners). Collapses `reconnecting` into
703
+ * `failure` (UI shows "lost, retrying") and splits `closed` by `revoked` so
704
+ * a terminal session-revocation read as `failure` while a clean client-
705
+ * initiated close reads as `initial` (the "not connected, not trying" state).
706
+ *
707
+ * @param status - the socket's current {@link SocketStatus}
708
+ * @param revoked - whether the session has been permanently revoked
709
+ * (typically `FrontendWebsocketClient.revoked`)
710
+ */
711
+ export const socket_status_to_async_status = (status, revoked) => {
712
+ switch (status) {
713
+ case 'initial':
714
+ return 'initial';
715
+ case 'connecting':
716
+ return 'pending';
717
+ case 'connected':
718
+ return 'success';
719
+ case 'reconnecting':
720
+ return 'failure';
721
+ case 'closed':
722
+ return revoked ? 'failure' : 'initial';
723
+ }
724
+ };
@@ -18,14 +18,31 @@ export declare const TransportName: z.ZodString;
18
18
  export type TransportName = z.infer<typeof TransportName>;
19
19
  /**
20
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.
21
+ * extensible — adding a field is non-breaking. Source of truth for the
22
+ * shared option shape; `ActionPeerSendOptions` and `RpcClientCallOptions`
23
+ * extend it.
26
24
  */
27
25
  export interface TransportSendOptions {
26
+ /**
27
+ * Per-call cancellation. Bottoms out at
28
+ * `FrontendWebsocketClient.request({signal})` on the WS path (sends the
29
+ * shared `cancel` notification on abort) and at `fetch({signal})` on
30
+ * HTTP. Backend transport has no per-call abort surface to honor.
31
+ */
28
32
  signal?: AbortSignal;
33
+ /**
34
+ * Per-call durable-queue opt-in. Names the **client-authoritative vs
35
+ * server-authoritative** distinction — server-authoritative consumers
36
+ * (e.g. zzz completion calls) fail fast with `service_unavailable` when
37
+ * the transport is down; client-authoritative consumers (games,
38
+ * real-time apps) buffer and replay on reconnect because the user
39
+ * already committed to the action at click time. Honored only by
40
+ * `FrontendWebsocketTransport` on the `request_response` path (default
41
+ * `false`). HTTP and backend transports ignore it; WS notifications
42
+ * also ignore it and always fail-fast when disconnected (fire-and-forget
43
+ * `connection.send` has no queue semantic).
44
+ */
45
+ queue?: boolean;
29
46
  }
30
47
  export interface Transport {
31
48
  transport_name: TransportName;
@@ -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;;;;;;;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"}
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;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;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"}
@@ -1 +1 @@
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
+ {"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;IA4DvC,QAAQ,IAAI,OAAO;IAInB,OAAO,IAAI,IAAI;CAUf"}
@@ -42,11 +42,19 @@ export class FrontendWebsocketTransport {
42
42
  });
43
43
  }
44
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
- if (!this.is_ready()) {
45
+ // Notifications fail-fast when disconnected regardless of `queue`
46
+ // `connection.send()` is fire-and-forget with no queue semantic, so
47
+ // silently dropping would masquerade as success at the rpc_client
48
+ // layer (caller would see `{ok: true}` for a lost message).
49
+ //
50
+ // Requests have no such gate here: `connection.request()` throws
51
+ // `ThrownJsonrpcError` with the right code (`service_unavailable`
52
+ // when not connected, `queue_overflow` when the durable queue is
53
+ // full, `request_cancelled` on abort, server's wire code for peer
54
+ // error frames), and the catch block below preserves that code
55
+ // verbatim in the error envelope. Queuing is routed via `queue`.
56
+ const queue = options?.queue ?? false;
57
+ if (is_jsonrpc_notification(message) && !this.is_ready()) {
50
58
  return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.service_unavailable('WebSocket not connected'));
51
59
  }
52
60
  if (is_jsonrpc_request(message)) {
@@ -54,7 +62,7 @@ export class FrontendWebsocketTransport {
54
62
  const result = await this.#connection.request(message.method, message.params, {
55
63
  id: message.id,
56
64
  signal: options?.signal,
57
- queue: false,
65
+ queue,
58
66
  });
59
67
  return create_jsonrpc_response(message.id, to_jsonrpc_result(result));
60
68
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides error types, named constructors, and HTTP status mapping
5
5
  * for the throw/catch error pattern used by `apply_route_specs`.
6
- * Core error codes (5 standard + 8 general application). Domain-specific
6
+ * Core error codes (5 standard + 10 general application). Domain-specific
7
7
  * codes stay in consumers — add by casting `as JsonrpcErrorCode`.
8
8
  *
9
9
  * `JsonrpcErrorCode` and `JsonrpcErrorObject` types are Zod-inferred
@@ -20,9 +20,9 @@ import { type JsonrpcErrorCode, type JsonrpcErrorObject } from './jsonrpc.js';
20
20
  /** Default message for unknown errors. */
21
21
  export declare const UNKNOWN_ERROR_MESSAGE = "unknown error";
22
22
  /** Names of standard and general application JSON-RPC error codes. */
23
- export type JsonrpcErrorName = 'parse_error' | 'invalid_request' | 'method_not_found' | 'invalid_params' | 'internal_error' | 'unauthenticated' | 'forbidden' | 'not_found' | 'conflict' | 'validation_error' | 'rate_limited' | 'service_unavailable' | 'timeout';
23
+ export type JsonrpcErrorName = 'parse_error' | 'invalid_request' | 'method_not_found' | 'invalid_params' | 'internal_error' | 'unauthenticated' | 'forbidden' | 'not_found' | 'conflict' | 'validation_error' | 'rate_limited' | 'service_unavailable' | 'timeout' | 'queue_overflow' | 'request_cancelled';
24
24
  /**
25
- * Standard JSON-RPC error codes (5) plus general application codes (8).
25
+ * Standard JSON-RPC error codes (5) plus general application codes (10).
26
26
  *
27
27
  * Extensible — consumers add domain-specific codes to their own objects
28
28
  * by casting `as JsonrpcErrorCode`. Application codes use the -32000 to
@@ -55,6 +55,18 @@ export declare const JSONRPC_ERROR_CODES: {
55
55
  readonly rate_limited: JsonrpcErrorCode;
56
56
  readonly service_unavailable: JsonrpcErrorCode;
57
57
  readonly timeout: JsonrpcErrorCode;
58
+ /**
59
+ * Client-side backpressure — an outbound buffer (e.g. `FrontendWebsocketClient`'s
60
+ * disconnected request queue) refused a new request because it was full.
61
+ * Distinct from `rate_limited`, which signals a server-side policy.
62
+ */
63
+ readonly queue_overflow: JsonrpcErrorCode;
64
+ /**
65
+ * Caller-initiated cancellation (e.g. `AbortSignal` fired). Cooperative,
66
+ * not a failure — the request did not complete because the caller asked
67
+ * for it to stop.
68
+ */
69
+ readonly request_cancelled: JsonrpcErrorCode;
58
70
  };
59
71
  /**
60
72
  * Named constructors for `JsonrpcErrorObject` values.
@@ -77,6 +89,8 @@ export declare const jsonrpc_error_messages: {
77
89
  readonly rate_limited: (message?: string, data?: unknown) => JsonrpcErrorObject;
78
90
  readonly service_unavailable: (message?: string, data?: unknown) => JsonrpcErrorObject;
79
91
  readonly timeout: (message?: string, data?: unknown) => JsonrpcErrorObject;
92
+ readonly queue_overflow: (message?: string, data?: unknown) => JsonrpcErrorObject;
93
+ readonly request_cancelled: (message?: string, data?: unknown) => JsonrpcErrorObject;
80
94
  };
81
95
  /**
82
96
  * Error class carrying a JSON-RPC error code — thrown by handlers,
@@ -108,6 +122,8 @@ export declare const jsonrpc_errors: {
108
122
  readonly rate_limited: (message?: string | undefined, data?: unknown) => ThrownJsonrpcError;
109
123
  readonly service_unavailable: (message?: string | undefined, data?: unknown) => ThrownJsonrpcError;
110
124
  readonly timeout: (message?: string | undefined, data?: unknown) => ThrownJsonrpcError;
125
+ readonly queue_overflow: (message?: string | undefined, data?: unknown) => ThrownJsonrpcError;
126
+ readonly request_cancelled: (message?: string | undefined, data?: unknown) => ThrownJsonrpcError;
111
127
  };
112
128
  /**
113
129
  * Maps JSON-RPC error codes to HTTP status codes.
@@ -1 +1 @@
1
- {"version":3,"file":"jsonrpc_errors.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/jsonrpc_errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAMN,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,MAAM,cAAc,CAAC;AAEtB,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GACzB,aAAa,GACb,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,WAAW,GACX,WAAW,GACX,UAAU,GACV,kBAAkB,GAClB,cAAc,GACd,qBAAqB,GACrB,SAAS,CAAC;AAEb;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;0BAEK,gBAAgB;8BACR,gBAAgB;+BACd,gBAAgB;6BACpB,gBAAgB;6BAChB,gBAAgB;IAG1D;;;;OAIG;8BACwB,gBAAgB;IAC3C;;;OAGG;wBACkB,gBAAgB;wBAChB,gBAAgB;uBACjB,gBAAgB;IACpC;;;OAGG;+BACyB,gBAAgB;2BACpB,gBAAgB;kCACT,gBAAgB;sBAC5B,gBAAgB;CAC2B,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;kCACb,OAAO,KAAG,kBAAkB;sCAMxB,OAAO,KAAG,kBAAkB;yCAMzB,MAAM,SAAS,OAAO,KAAG,kBAAkB;wCAM5C,MAAM,SAAS,OAAO,KAAG,kBAAkB;wCAO5D,MAAM,SACR,OAAO,KACZ,kBAAkB;yCAMM,MAAM,SAA6B,OAAO,KAAG,kBAAkB;mCAMrE,MAAM,SAAuB,OAAO,KAAG,kBAAkB;oCAMvD,MAAM,SAAS,OAAO,KAAG,kBAAkB;kCAM9C,MAAM,SAAsB,OAAO,KAAG,kBAAkB;0CAMhD,MAAM,SAA8B,OAAO,KAAG,kBAAkB;sCAMpE,MAAM,SAA0B,OAAO,KAAG,kBAAkB;6CAO1E,MAAM,SACR,OAAO,KACZ,kBAAkB;iCAMF,MAAM,SAAqB,OAAO,KAAG,kBAAkB;CAKe,CAAC;AAE3F;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,OAAO,CAAC;gBAEH,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,YAAY;CAK3F;AAWD;;;;GAIG;AACH,eAAO,MAAM,cAAc;8CAXQ,kBAAkB;kDAAlB,kBAAkB;gFAAlB,kBAAkB;+EAAlB,kBAAkB;+EAAlB,kBAAkB;gFAAlB,kBAAkB;0EAAlB,kBAAkB;2EAAlB,kBAAkB;yEAAlB,kBAAkB;iFAAlB,kBAAkB;6EAAlB,kBAAkB;oFAAlB,kBAAkB;wEAAlB,kBAAkB;CAyBqC,CAAC;AAI3F;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAcpE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAMzC,CAAC;AAEvC;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,gBAAgB,KAAG,MAClB,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,GAAI,QAAQ,MAAM,KAAG,gBACa,CAAC"}
1
+ {"version":3,"file":"jsonrpc_errors.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/jsonrpc_errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAMN,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,MAAM,cAAc,CAAC;AAEtB,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAErD,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GACzB,aAAa,GACb,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,WAAW,GACX,WAAW,GACX,UAAU,GACV,kBAAkB,GAClB,cAAc,GACd,qBAAqB,GACrB,SAAS,GACT,gBAAgB,GAChB,mBAAmB,CAAC;AAEvB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;0BAEK,gBAAgB;8BACR,gBAAgB;+BACd,gBAAgB;6BACpB,gBAAgB;6BAChB,gBAAgB;IAG1D;;;;OAIG;8BACwB,gBAAgB;IAC3C;;;OAGG;wBACkB,gBAAgB;wBAChB,gBAAgB;uBACjB,gBAAgB;IACpC;;;OAGG;+BACyB,gBAAgB;2BACpB,gBAAgB;kCACT,gBAAgB;sBAC5B,gBAAgB;IACnC;;;;OAIG;6BACuB,gBAAgB;IAC1C;;;;OAIG;gCAC0B,gBAAgB;CACiB,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;kCACb,OAAO,KAAG,kBAAkB;sCAMxB,OAAO,KAAG,kBAAkB;yCAMzB,MAAM,SAAS,OAAO,KAAG,kBAAkB;wCAM5C,MAAM,SAAS,OAAO,KAAG,kBAAkB;wCAO5D,MAAM,SACR,OAAO,KACZ,kBAAkB;yCAMM,MAAM,SAA6B,OAAO,KAAG,kBAAkB;mCAMrE,MAAM,SAAuB,OAAO,KAAG,kBAAkB;oCAMvD,MAAM,SAAS,OAAO,KAAG,kBAAkB;kCAM9C,MAAM,SAAsB,OAAO,KAAG,kBAAkB;0CAMhD,MAAM,SAA8B,OAAO,KAAG,kBAAkB;sCAMpE,MAAM,SAA0B,OAAO,KAAG,kBAAkB;6CAO1E,MAAM,SACR,OAAO,KACZ,kBAAkB;iCAMF,MAAM,SAAqB,OAAO,KAAG,kBAAkB;wCAMhD,MAAM,SAA4B,OAAO,KAAG,kBAAkB;2CAO9E,MAAM,SACR,OAAO,KACZ,kBAAkB;CAKoE,CAAC;AAE3F;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,OAAO,CAAC;gBAEH,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,YAAY;CAK3F;AAWD;;;;GAIG;AACH,eAAO,MAAM,cAAc;8CAXQ,kBAAkB;kDAAlB,kBAAkB;gFAAlB,kBAAkB;+EAAlB,kBAAkB;+EAAlB,kBAAkB;gFAAlB,kBAAkB;0EAAlB,kBAAkB;2EAAlB,kBAAkB;yEAAlB,kBAAkB;iFAAlB,kBAAkB;6EAAlB,kBAAkB;oFAAlB,kBAAkB;wEAAlB,kBAAkB;+EAAlB,kBAAkB;kFAAlB,kBAAkB;CA2BqC,CAAC;AAI3F;;;;;GAKG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkBpE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAMzC,CAAC;AAEvC;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,gBAAgB,KAAG,MAClB,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,GAAI,QAAQ,MAAM,KAAG,gBACa,CAAC"}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides error types, named constructors, and HTTP status mapping
5
5
  * for the throw/catch error pattern used by `apply_route_specs`.
6
- * Core error codes (5 standard + 8 general application). Domain-specific
6
+ * Core error codes (5 standard + 10 general application). Domain-specific
7
7
  * codes stay in consumers — add by casting `as JsonrpcErrorCode`.
8
8
  *
9
9
  * `JsonrpcErrorCode` and `JsonrpcErrorObject` types are Zod-inferred
@@ -20,7 +20,7 @@ import { JSONRPC_PARSE_ERROR, JSONRPC_INVALID_REQUEST, JSONRPC_METHOD_NOT_FOUND,
20
20
  /** Default message for unknown errors. */
21
21
  export const UNKNOWN_ERROR_MESSAGE = 'unknown error';
22
22
  /**
23
- * Standard JSON-RPC error codes (5) plus general application codes (8).
23
+ * Standard JSON-RPC error codes (5) plus general application codes (10).
24
24
  *
25
25
  * Extensible — consumers add domain-specific codes to their own objects
26
26
  * by casting `as JsonrpcErrorCode`. Application codes use the -32000 to
@@ -55,6 +55,18 @@ export const JSONRPC_ERROR_CODES = {
55
55
  rate_limited: -32006,
56
56
  service_unavailable: -32007,
57
57
  timeout: -32008,
58
+ /**
59
+ * Client-side backpressure — an outbound buffer (e.g. `FrontendWebsocketClient`'s
60
+ * disconnected request queue) refused a new request because it was full.
61
+ * Distinct from `rate_limited`, which signals a server-side policy.
62
+ */
63
+ queue_overflow: -32009,
64
+ /**
65
+ * Caller-initiated cancellation (e.g. `AbortSignal` fired). Cooperative,
66
+ * not a failure — the request did not complete because the caller asked
67
+ * for it to stop.
68
+ */
69
+ request_cancelled: -32010,
58
70
  };
59
71
  /**
60
72
  * Named constructors for `JsonrpcErrorObject` values.
@@ -129,6 +141,16 @@ export const jsonrpc_error_messages = {
129
141
  message,
130
142
  data,
131
143
  }),
144
+ queue_overflow: (message = 'queue overflow', data) => ({
145
+ code: JSONRPC_ERROR_CODES.queue_overflow,
146
+ message,
147
+ data,
148
+ }),
149
+ request_cancelled: (message = 'request cancelled', data) => ({
150
+ code: JSONRPC_ERROR_CODES.request_cancelled,
151
+ message,
152
+ data,
153
+ }),
132
154
  };
133
155
  /**
134
156
  * Error class carrying a JSON-RPC error code — thrown by handlers,
@@ -168,6 +190,8 @@ export const jsonrpc_errors = {
168
190
  rate_limited: create_error_thrower(jsonrpc_error_messages.rate_limited),
169
191
  service_unavailable: create_error_thrower(jsonrpc_error_messages.service_unavailable),
170
192
  timeout: create_error_thrower(jsonrpc_error_messages.timeout),
193
+ queue_overflow: create_error_thrower(jsonrpc_error_messages.queue_overflow),
194
+ request_cancelled: create_error_thrower(jsonrpc_error_messages.request_cancelled),
171
195
  };
172
196
  // --- HTTP status mapping ---
173
197
  /**
@@ -187,9 +211,13 @@ export const JSONRPC_ERROR_CODE_TO_HTTP_STATUS = {
187
211
  [-32003]: 404, // not_found
188
212
  [-32004]: 409, // conflict
189
213
  [-32005]: 422, // validation_error
214
+ // queue_overflow shares 429 with rate_limited — listed first so reverse
215
+ // map wins with rate_limited (server-side) rather than client-side overflow.
216
+ [-32009]: 429, // queue_overflow (client-side backpressure)
190
217
  [-32006]: 429, // rate_limited
191
218
  [-32007]: 503, // service_unavailable
192
219
  [-32008]: 504, // timeout
220
+ [-32010]: 499, // request_cancelled (nginx "client closed request")
193
221
  };
194
222
  /**
195
223
  * Maps HTTP status codes to JSON-RPC error codes (reverse mapping).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -45,9 +45,9 @@
45
45
  "@fuzdev/blake3_wasm": "^0.1.0",
46
46
  "@fuzdev/fuz_code": "^0.45.1",
47
47
  "@fuzdev/fuz_css": "^0.58.0",
48
- "@fuzdev/fuz_ui": "^0.191.2",
49
- "@fuzdev/fuz_util": "^0.56.0",
50
- "@fuzdev/gro": "^0.197.3",
48
+ "@fuzdev/fuz_ui": "^0.191.4",
49
+ "@fuzdev/fuz_util": "^0.57.0",
50
+ "@fuzdev/gro": "^0.198.0",
51
51
  "@jridgewell/trace-mapping": "^0.3.31",
52
52
  "@node-rs/argon2": "^2.0.2",
53
53
  "@ryanatkn/eslint-config": "^0.11.0",