@fuzdev/fuz_app 0.112.0 → 0.114.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/CLAUDE.md +46 -20
- package/dist/actions/action_dispatcher.d.ts +1 -1
- package/dist/actions/action_dispatcher.d.ts.map +1 -1
- package/dist/actions/action_dispatcher.js +22 -16
- package/dist/actions/action_event.d.ts +18 -2
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +63 -19
- package/dist/actions/action_event_types.d.ts +22 -0
- package/dist/actions/action_event_types.d.ts.map +1 -1
- package/dist/actions/action_event_types.js +19 -0
- package/dist/actions/compile_action_registry.d.ts +4 -2
- package/dist/actions/compile_action_registry.d.ts.map +1 -1
- package/dist/actions/compile_action_registry.js +8 -3
- package/dist/actions/frontend_rpc_client.d.ts +6 -0
- package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
- package/dist/actions/frontend_rpc_client.js +2 -1
- package/dist/actions/heartbeat.d.ts +8 -4
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +8 -4
- package/dist/actions/peer_ping.d.ts.map +1 -1
- package/dist/actions/peer_ping.js +3 -1
- package/dist/actions/perform_action.d.ts +1 -1
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +6 -4
- package/dist/actions/register_action_ws.d.ts +13 -0
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +23 -2
- package/dist/actions/rpc_client.d.ts +1 -1
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +2 -1
- package/dist/actions/socket.svelte.d.ts +53 -10
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +84 -23
- package/dist/actions/transports.d.ts +24 -0
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -0
- package/dist/actions/transports_http.d.ts +11 -4
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_http.js +63 -18
- package/dist/actions/transports_ws.d.ts +28 -3
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +33 -4
- package/dist/actions/ws_endpoint_spec.d.ts +7 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +6 -1
- package/dist/auth/audit_log_route_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_route_schema.js +12 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -1
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +2 -1
- package/dist/db/fact_disk_storage.js +1 -1
- package/dist/http/CLAUDE.md +10 -6
- package/dist/http/auth_shape.d.ts +19 -6
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +19 -6
- package/dist/http/jsonrpc_helpers.d.ts +0 -5
- package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.js +0 -13
- package/dist/http/schema_helpers.d.ts +56 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +91 -0
- package/dist/http/surface_query.d.ts +29 -4
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +30 -5
- package/dist/realtime/sse_auth_guard.d.ts +9 -3
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +9 -3
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -0
- package/dist/testing/CLAUDE.md +18 -13
- package/dist/testing/attack_surface.d.ts +5 -2
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +45 -17
- package/dist/testing/auth_apps.d.ts +25 -0
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +34 -5
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +18 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +3 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +36 -7
- package/dist/testing/cross_backend/ws_round_trip.d.ts +11 -3
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +74 -12
- package/dist/testing/surface_invariants.d.ts +7 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +11 -5
- package/dist/testing/transports/ws_client.d.ts +6 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +6 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +5 -0
- package/dist/ui/AdminAccounts.svelte +6 -4
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -3
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +10 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminRoleGrantHistory.svelte +7 -7
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSettings.svelte +1 -1
- package/dist/ui/RoleGrantOfferInbox.svelte +4 -1
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -1
- package/dist/ui/SurfaceExplorer.svelte +33 -17
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/form_state.svelte.d.ts +9 -4
- package/dist/ui/form_state.svelte.d.ts.map +1 -1
- package/dist/ui/form_state.svelte.js +28 -8
- package/package.json +10 -10
- package/dist/actions/request_tracker.svelte.d.ts +0 -77
- package/dist/actions/request_tracker.svelte.d.ts.map +0 -1
- package/dist/actions/request_tracker.svelte.js +0 -169
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* `attempted` state (set on submit attempt). Errors show after a field is blurred or
|
|
6
6
|
* after a submit attempt, avoiding premature validation while the user is still typing.
|
|
7
7
|
*
|
|
8
|
-
* The `FormState.form` attachment also handles Enter key
|
|
9
|
-
*
|
|
8
|
+
* The `FormState.form` attachment also handles the Enter key: in an input
|
|
9
|
+
* followed by another input it moves focus to that input (past any button
|
|
10
|
+
* between), and in the last input it submits the form (`requestSubmit`) —
|
|
11
|
+
* so Enter in a login form's password field logs in, even when the form's
|
|
12
|
+
* button is `type="button"`, as `PendingButton` is. Hidden inputs don't count, and an Enter that commits
|
|
13
|
+
* IME composition is left alone.
|
|
10
14
|
*
|
|
11
15
|
* All trackable inputs must have a `name` attribute — an error is thrown in dev
|
|
12
16
|
* if an input without `name` loses focus.
|
|
@@ -43,7 +47,8 @@
|
|
|
43
47
|
import { DEV } from 'esm-env';
|
|
44
48
|
import { on } from 'svelte/events';
|
|
45
49
|
import { SvelteSet } from 'svelte/reactivity';
|
|
46
|
-
|
|
50
|
+
/** Inputs Enter moves between — disabled and hidden ones are skipped. */
|
|
51
|
+
const ENTER_INPUT_SELECTOR = 'input:not(:disabled):not([type=hidden])';
|
|
47
52
|
const FORM_INPUT_SELECTOR = 'input, textarea, select';
|
|
48
53
|
export class FormState {
|
|
49
54
|
#touched = new SvelteSet();
|
|
@@ -56,8 +61,9 @@ export class FormState {
|
|
|
56
61
|
return this.#attempted;
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
|
-
* Creates a form attachment that handles Enter key
|
|
60
|
-
*
|
|
64
|
+
* Creates a form attachment that handles the Enter key (advance focus to
|
|
65
|
+
* the next input, or submit from the last one) and tracks field touched
|
|
66
|
+
* state via delegated `focusout`.
|
|
61
67
|
*
|
|
62
68
|
* Fields are identified by their `name` attribute.
|
|
63
69
|
*
|
|
@@ -73,14 +79,28 @@ export class FormState {
|
|
|
73
79
|
const keydown_cleanup = on(form, 'keydown', (e) => {
|
|
74
80
|
if (e.key !== 'Enter')
|
|
75
81
|
return;
|
|
82
|
+
// an IME composing text commits it with Enter — not a submit; Safari
|
|
83
|
+
// reports that Enter with `isComposing` false but `keyCode` 229 — the
|
|
84
|
+
// deprecated field is the only signal there
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
86
|
+
if (e.isComposing || e.keyCode === 229)
|
|
87
|
+
return;
|
|
76
88
|
if (!(e.target instanceof HTMLInputElement))
|
|
77
89
|
return;
|
|
78
|
-
const
|
|
79
|
-
const index =
|
|
90
|
+
const inputs = Array.from(form.querySelectorAll(ENTER_INPUT_SELECTOR));
|
|
91
|
+
const index = inputs.indexOf(e.target);
|
|
80
92
|
if (index < 0)
|
|
81
93
|
return;
|
|
82
94
|
e.preventDefault();
|
|
83
|
-
|
|
95
|
+
// advance to the next input, past any button between (a show-password
|
|
96
|
+
// toggle, say); the last input submits instead
|
|
97
|
+
const next_input = inputs[index + 1];
|
|
98
|
+
if (next_input) {
|
|
99
|
+
next_input.focus();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
form.requestSubmit();
|
|
103
|
+
}
|
|
84
104
|
});
|
|
85
105
|
const focusout_cleanup = on(form, 'focusout', (e) => {
|
|
86
106
|
const target = e.target;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.114.0",
|
|
4
4
|
"description": "fullstack app library",
|
|
5
5
|
"glyph": "🗝",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@electric-sql/pglite": ">=0.4",
|
|
35
|
-
"@fuzdev/
|
|
35
|
+
"@fuzdev/blake3-wasm": ">=0.2.0",
|
|
36
36
|
"@fuzdev/fuz_util": ">=0.68.0",
|
|
37
37
|
"@hono/node-server": ">=1",
|
|
38
38
|
"@hono/node-ws": ">=1",
|
|
@@ -73,12 +73,12 @@
|
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@electric-sql/pglite": "^0.4.5",
|
|
76
|
-
"@fuzdev/
|
|
77
|
-
"@fuzdev/fuz_code": "^0.
|
|
78
|
-
"@fuzdev/fuz_css": "^0.
|
|
79
|
-
"@fuzdev/fuz_ui": "^0.
|
|
80
|
-
"@fuzdev/fuz_util": "^0.
|
|
81
|
-
"@fuzdev/gro": "0.
|
|
76
|
+
"@fuzdev/blake3-wasm": "^0.2.0",
|
|
77
|
+
"@fuzdev/fuz_code": "^0.48.0",
|
|
78
|
+
"@fuzdev/fuz_css": "^0.64.0",
|
|
79
|
+
"@fuzdev/fuz_ui": "^0.208.0",
|
|
80
|
+
"@fuzdev/fuz_util": "^0.69.0",
|
|
81
|
+
"@fuzdev/gro": "^0.212.0",
|
|
82
82
|
"@fuzdev/mdz": "^0.3.0",
|
|
83
83
|
"@hono/node-server": "^1.19.14",
|
|
84
84
|
"@hono/node-ws": "^1.3.1",
|
|
@@ -104,13 +104,13 @@
|
|
|
104
104
|
"prettier-plugin-svelte": "^4.1.1",
|
|
105
105
|
"svelte": "^5.56.4",
|
|
106
106
|
"svelte-check": "^4.7.1",
|
|
107
|
-
"svelte-docinfo": "^0.
|
|
107
|
+
"svelte-docinfo": "^0.7.0",
|
|
108
108
|
"svelte2tsx": "^0.7.52",
|
|
109
109
|
"tslib": "^2.8.1",
|
|
110
110
|
"typescript": "^5.9.3",
|
|
111
111
|
"typescript-eslint": "^8.57.0",
|
|
112
112
|
"vite": "^7.3.1",
|
|
113
|
-
"vitest": "^
|
|
113
|
+
"vitest": "^5.0.0",
|
|
114
114
|
"ws": "^8.20.1",
|
|
115
115
|
"zimmerframe": "^1.1.4",
|
|
116
116
|
"zod": "^4.3.6"
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request tracker — manages pending JSON-RPC requests with timeouts.
|
|
3
|
-
*
|
|
4
|
-
* Uses SvelteMap for reactive pending request tracking.
|
|
5
|
-
*
|
|
6
|
-
* @module
|
|
7
|
-
*/
|
|
8
|
-
import { type Deferred, type AsyncStatus } from '@fuzdev/fuz_util/async.ts';
|
|
9
|
-
import { SvelteMap } from 'svelte/reactivity';
|
|
10
|
-
import { type JsonrpcErrorResponse, type JsonrpcRequestId, type JsonrpcResponseOrError } from '../http/jsonrpc.js';
|
|
11
|
-
/** ISO datetime string for request creation timestamps. */
|
|
12
|
-
type Datetime = string & {
|
|
13
|
-
readonly __brand: 'Datetime';
|
|
14
|
-
};
|
|
15
|
-
export declare class RequestTrackerItem {
|
|
16
|
-
readonly id: JsonrpcRequestId;
|
|
17
|
-
readonly deferred: Deferred<JsonrpcResponseOrError>;
|
|
18
|
-
readonly created: Datetime;
|
|
19
|
-
status: AsyncStatus;
|
|
20
|
-
timeout: NodeJS.Timeout | undefined;
|
|
21
|
-
constructor(id: JsonrpcRequestId, deferred: Deferred<JsonrpcResponseOrError>, created: Datetime, status: AsyncStatus, timeout: NodeJS.Timeout | undefined);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Reactive pending-request store with per-request timeouts. Used by transports
|
|
25
|
-
* that don't delegate request/response correlation to a `WebsocketRpcConnection`.
|
|
26
|
-
*/
|
|
27
|
-
export declare class RequestTracker {
|
|
28
|
-
readonly pending_requests: SvelteMap<JsonrpcRequestId, RequestTrackerItem>;
|
|
29
|
-
readonly request_timeout_ms: number;
|
|
30
|
-
constructor(request_timeout_ms?: number);
|
|
31
|
-
/**
|
|
32
|
-
* Track a new request keyed by `id`.
|
|
33
|
-
*
|
|
34
|
-
* @returns deferred resolved on response, or rejected via the timeout
|
|
35
|
-
* @mutates this - inserts a `RequestTrackerItem` into `pending_requests`
|
|
36
|
-
* and arms a timeout that auto-rejects after `request_timeout_ms`;
|
|
37
|
-
* clears any prior timeout for the same id
|
|
38
|
-
*/
|
|
39
|
-
track_request(id: JsonrpcRequestId): Deferred<JsonrpcResponseOrError>;
|
|
40
|
-
/**
|
|
41
|
-
* Resolve a pending request with its response.
|
|
42
|
-
*
|
|
43
|
-
* @mutates this - clears the timeout, marks status `'success'`,
|
|
44
|
-
* resolves the deferred, and removes the entry from `pending_requests`
|
|
45
|
-
*/
|
|
46
|
-
resolve_request(id: JsonrpcRequestId, response: JsonrpcResponseOrError): void;
|
|
47
|
-
/**
|
|
48
|
-
* Reject a pending request with `error_message`.
|
|
49
|
-
*
|
|
50
|
-
* @mutates this - clears the timeout, marks status `'failure'`,
|
|
51
|
-
* rejects the deferred with a `ThrownJsonrpcError`, and removes the
|
|
52
|
-
* entry from `pending_requests`
|
|
53
|
-
*/
|
|
54
|
-
reject_request(id: JsonrpcRequestId, error_message: JsonrpcErrorResponse): void;
|
|
55
|
-
/**
|
|
56
|
-
* Handles an incoming JSON-RPC message. Resolves or rejects the associated request.
|
|
57
|
-
* Ignores notifications and unknown/invalid messages.
|
|
58
|
-
*/
|
|
59
|
-
handle_message(message: any): void;
|
|
60
|
-
/**
|
|
61
|
-
* Cancel a pending request without rejecting its deferred — just
|
|
62
|
-
* cleanup. The caller's promise stays unsettled; pair with an external
|
|
63
|
-
* resolution if needed.
|
|
64
|
-
*
|
|
65
|
-
* @mutates this - clears the timeout and removes the entry from `pending_requests`
|
|
66
|
-
*/
|
|
67
|
-
cancel_request(id: JsonrpcRequestId): void;
|
|
68
|
-
/**
|
|
69
|
-
* Cancel all pending requests.
|
|
70
|
-
* @param reason - optional reason to include in rejection
|
|
71
|
-
* @mutates this - clears every timeout, marks each status `'failure'`,
|
|
72
|
-
* rejects each deferred with `internal_error`, and empties `pending_requests`
|
|
73
|
-
*/
|
|
74
|
-
cancel_all_requests(reason?: string): void;
|
|
75
|
-
}
|
|
76
|
-
export {};
|
|
77
|
-
//# sourceMappingURL=request_tracker.svelte.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"request_tracker.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/request_tracker.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAmB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,EAEN,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,MAAM,oBAAoB,CAAC;AAG5B,2DAA2D;AAC3D,KAAK,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAM1D,qBAAa,kBAAkB;IAC9B,QAAQ,CAAC,EAAE,EAAE,gBAAgB,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IACpD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,MAAM,EAAE,WAAW,CAAiB;IACpC,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAgB;gBAGlD,EAAE,EAAE,gBAAgB,EACpB,QAAQ,EAAE,QAAQ,CAAC,sBAAsB,CAAC,EAC1C,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS;CAQpC;AAED;;;GAGG;AACH,qBAAa,cAAc;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,SAAS,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAmB;IAC7F,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;gBAExB,kBAAkB,SAAU;IAIxC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,EAAE,gBAAgB,GAAG,QAAQ,CAAC,sBAAsB,CAAC;IA6BrE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,sBAAsB,GAAG,IAAI;IAkB7E;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,EAAE,gBAAgB,EAAE,aAAa,EAAE,oBAAoB,GAAG,IAAI;IAuB/E;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;IAiBlC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI;IAe1C;;;;;OAKG;IACH,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAiB1C"}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request tracker — manages pending JSON-RPC requests with timeouts.
|
|
3
|
-
*
|
|
4
|
-
* Uses SvelteMap for reactive pending request tracking.
|
|
5
|
-
*
|
|
6
|
-
* @module
|
|
7
|
-
*/
|
|
8
|
-
import { create_deferred } from '@fuzdev/fuz_util/async.ts';
|
|
9
|
-
import { SvelteMap } from 'svelte/reactivity';
|
|
10
|
-
import { JSONRPC_INTERNAL_ERROR } from '../http/jsonrpc.js';
|
|
11
|
-
import { ThrownJsonrpcError, JSONRPC_ERROR_CODES } from '../http/jsonrpc_errors.js';
|
|
12
|
-
const get_datetime_now = () => new Date().toISOString();
|
|
13
|
-
// TODO what if this uses a tracker id param that's an opaque UUID but can be used for action association?
|
|
14
|
-
// TODO name, like `TrackedRequest`? or is this implicit namespacing and generic name preferred
|
|
15
|
-
export class RequestTrackerItem {
|
|
16
|
-
id;
|
|
17
|
-
deferred;
|
|
18
|
-
created;
|
|
19
|
-
status = $state.raw();
|
|
20
|
-
timeout = $state.raw();
|
|
21
|
-
constructor(id, deferred, created, status, timeout) {
|
|
22
|
-
this.id = id;
|
|
23
|
-
this.deferred = deferred;
|
|
24
|
-
this.created = created;
|
|
25
|
-
this.status = status;
|
|
26
|
-
this.timeout = timeout;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Reactive pending-request store with per-request timeouts. Used by transports
|
|
31
|
-
* that don't delegate request/response correlation to a `WebsocketRpcConnection`.
|
|
32
|
-
*/
|
|
33
|
-
export class RequestTracker {
|
|
34
|
-
pending_requests = new SvelteMap();
|
|
35
|
-
request_timeout_ms;
|
|
36
|
-
constructor(request_timeout_ms = 120_000) {
|
|
37
|
-
this.request_timeout_ms = request_timeout_ms;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Track a new request keyed by `id`.
|
|
41
|
-
*
|
|
42
|
-
* @returns deferred resolved on response, or rejected via the timeout
|
|
43
|
-
* @mutates this - inserts a `RequestTrackerItem` into `pending_requests`
|
|
44
|
-
* and arms a timeout that auto-rejects after `request_timeout_ms`;
|
|
45
|
-
* clears any prior timeout for the same id
|
|
46
|
-
*/
|
|
47
|
-
track_request(id) {
|
|
48
|
-
const deferred = create_deferred();
|
|
49
|
-
const created = get_datetime_now();
|
|
50
|
-
// If we're tracking a request with the same id, clean up the previous one first
|
|
51
|
-
const existing_request = this.pending_requests.get(id);
|
|
52
|
-
if (existing_request?.timeout) {
|
|
53
|
-
clearTimeout(existing_request.timeout);
|
|
54
|
-
}
|
|
55
|
-
// Set up a timeout to automatically reject the request after a delay
|
|
56
|
-
const timeout = setTimeout(() => {
|
|
57
|
-
// Create a proper timeout error message
|
|
58
|
-
this.reject_request(id, {
|
|
59
|
-
jsonrpc: '2.0',
|
|
60
|
-
id,
|
|
61
|
-
error: { code: JSONRPC_INTERNAL_ERROR, message: `request timed out: ${id}` }
|
|
62
|
-
});
|
|
63
|
-
}, this.request_timeout_ms);
|
|
64
|
-
// Store the request tracker using the new class
|
|
65
|
-
this.pending_requests.set(id, new RequestTrackerItem(id, deferred, created, 'pending', timeout));
|
|
66
|
-
return deferred;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Resolve a pending request with its response.
|
|
70
|
-
*
|
|
71
|
-
* @mutates this - clears the timeout, marks status `'success'`,
|
|
72
|
-
* resolves the deferred, and removes the entry from `pending_requests`
|
|
73
|
-
*/
|
|
74
|
-
resolve_request(id, response) {
|
|
75
|
-
const request = this.pending_requests.get(id);
|
|
76
|
-
if (!request) {
|
|
77
|
-
console.warn(`received response for unknown request: ${id}`);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
// Clear the timeout and resolve the promise
|
|
81
|
-
if (request.timeout) {
|
|
82
|
-
clearTimeout(request.timeout);
|
|
83
|
-
request.timeout = undefined;
|
|
84
|
-
}
|
|
85
|
-
request.status = 'success';
|
|
86
|
-
request.deferred.resolve(response);
|
|
87
|
-
this.pending_requests.delete(id);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Reject a pending request with `error_message`.
|
|
91
|
-
*
|
|
92
|
-
* @mutates this - clears the timeout, marks status `'failure'`,
|
|
93
|
-
* rejects the deferred with a `ThrownJsonrpcError`, and removes the
|
|
94
|
-
* entry from `pending_requests`
|
|
95
|
-
*/
|
|
96
|
-
reject_request(id, error_message) {
|
|
97
|
-
const request = this.pending_requests.get(id);
|
|
98
|
-
if (!request) {
|
|
99
|
-
console.warn(`received error for unknown request: ${id}`);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// Clear the timeout and reject the promise
|
|
103
|
-
if (request.timeout) {
|
|
104
|
-
clearTimeout(request.timeout);
|
|
105
|
-
request.timeout = undefined;
|
|
106
|
-
}
|
|
107
|
-
request.status = 'failure';
|
|
108
|
-
const error = new ThrownJsonrpcError(error_message.error.code, error_message.error.message, error_message.error.data);
|
|
109
|
-
request.deferred.reject(error);
|
|
110
|
-
this.pending_requests.delete(id);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Handles an incoming JSON-RPC message. Resolves or rejects the associated request.
|
|
114
|
-
* Ignores notifications and unknown/invalid messages.
|
|
115
|
-
*/
|
|
116
|
-
handle_message(message) {
|
|
117
|
-
if (!message)
|
|
118
|
-
return; // ignore invalid values
|
|
119
|
-
const { id } = message;
|
|
120
|
-
// TODO maybe log a warning/error?
|
|
121
|
-
if (id == null)
|
|
122
|
-
return; // ignore notifications and errors without ids
|
|
123
|
-
// JSON-RPC responses require both an `id` and either a `result` or `error` field, but not both
|
|
124
|
-
if ('result' in message) {
|
|
125
|
-
this.resolve_request(id, message);
|
|
126
|
-
}
|
|
127
|
-
else if ('error' in message) {
|
|
128
|
-
this.reject_request(id, message);
|
|
129
|
-
}
|
|
130
|
-
// ignore other messages
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Cancel a pending request without rejecting its deferred — just
|
|
134
|
-
* cleanup. The caller's promise stays unsettled; pair with an external
|
|
135
|
-
* resolution if needed.
|
|
136
|
-
*
|
|
137
|
-
* @mutates this - clears the timeout and removes the entry from `pending_requests`
|
|
138
|
-
*/
|
|
139
|
-
cancel_request(id) {
|
|
140
|
-
const request = this.pending_requests.get(id);
|
|
141
|
-
if (!request) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (request.timeout) {
|
|
145
|
-
clearTimeout(request.timeout);
|
|
146
|
-
request.timeout = undefined;
|
|
147
|
-
}
|
|
148
|
-
// We don't reject the promise here, just clean up the tracking
|
|
149
|
-
this.pending_requests.delete(id);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Cancel all pending requests.
|
|
153
|
-
* @param reason - optional reason to include in rejection
|
|
154
|
-
* @mutates this - clears every timeout, marks each status `'failure'`,
|
|
155
|
-
* rejects each deferred with `internal_error`, and empties `pending_requests`
|
|
156
|
-
*/
|
|
157
|
-
cancel_all_requests(reason) {
|
|
158
|
-
for (const [id, request] of this.pending_requests.entries()) {
|
|
159
|
-
if (request.timeout) {
|
|
160
|
-
clearTimeout(request.timeout);
|
|
161
|
-
request.timeout = undefined;
|
|
162
|
-
}
|
|
163
|
-
request.status = 'failure';
|
|
164
|
-
request.deferred.reject(new ThrownJsonrpcError(JSONRPC_ERROR_CODES.internal_error, // TODO canceled error?
|
|
165
|
-
reason || 'request cancelled'));
|
|
166
|
-
this.pending_requests.delete(id);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|