@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.
- package/dist/actions/action_peer.d.ts +12 -11
- package/dist/actions/action_peer.d.ts.map +1 -1
- package/dist/actions/action_peer.js +4 -1
- package/dist/actions/register_action_ws.d.ts +11 -4
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +11 -4
- package/dist/actions/register_ws_endpoint.d.ts +46 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -0
- package/dist/actions/register_ws_endpoint.js +38 -0
- package/dist/actions/rpc_client.d.ts +6 -8
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +2 -0
- package/dist/actions/socket.svelte.d.ts +59 -7
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +123 -31
- package/dist/actions/transports.d.ts +22 -5
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +14 -6
- package/dist/http/jsonrpc_errors.d.ts +19 -3
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +30 -2
- package/package.json +4 -4
|
@@ -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
|
-
|
|
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?:
|
|
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;
|
|
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, {
|
|
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
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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;
|
|
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
|
|
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
|
|
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 (
|
|
210
|
-
* can abort the matching handler's `ctx.signal`. Suppressed
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;
|
|
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
|
|
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
|
|
233
|
-
this
|
|
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
|
|
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 (
|
|
277
|
-
* can abort the matching handler's `ctx.signal`. Suppressed
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
539
|
-
|
|
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
|
|
560
|
-
this
|
|
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
|
-
|
|
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.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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;
|
|
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
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
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
|
|
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 +
|
|
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 (
|
|
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;
|
|
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 +
|
|
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 (
|
|
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.
|
|
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.
|
|
49
|
-
"@fuzdev/fuz_util": "^0.
|
|
50
|
-
"@fuzdev/gro": "^0.
|
|
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",
|