@fuzdev/fuz_app 0.113.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.
Files changed (72) hide show
  1. package/dist/actions/CLAUDE.md +46 -20
  2. package/dist/actions/action_dispatcher.d.ts +1 -1
  3. package/dist/actions/action_dispatcher.d.ts.map +1 -1
  4. package/dist/actions/action_dispatcher.js +22 -16
  5. package/dist/actions/action_event.d.ts +18 -2
  6. package/dist/actions/action_event.d.ts.map +1 -1
  7. package/dist/actions/action_event.js +63 -19
  8. package/dist/actions/action_event_types.d.ts +22 -0
  9. package/dist/actions/action_event_types.d.ts.map +1 -1
  10. package/dist/actions/action_event_types.js +19 -0
  11. package/dist/actions/compile_action_registry.d.ts +4 -2
  12. package/dist/actions/compile_action_registry.d.ts.map +1 -1
  13. package/dist/actions/compile_action_registry.js +8 -3
  14. package/dist/actions/frontend_rpc_client.d.ts +6 -0
  15. package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
  16. package/dist/actions/frontend_rpc_client.js +2 -1
  17. package/dist/actions/heartbeat.d.ts +8 -4
  18. package/dist/actions/heartbeat.d.ts.map +1 -1
  19. package/dist/actions/heartbeat.js +8 -4
  20. package/dist/actions/peer_ping.d.ts.map +1 -1
  21. package/dist/actions/peer_ping.js +3 -1
  22. package/dist/actions/perform_action.d.ts +1 -1
  23. package/dist/actions/perform_action.d.ts.map +1 -1
  24. package/dist/actions/perform_action.js +6 -4
  25. package/dist/actions/register_action_ws.d.ts +13 -0
  26. package/dist/actions/register_action_ws.d.ts.map +1 -1
  27. package/dist/actions/register_action_ws.js +23 -2
  28. package/dist/actions/rpc_client.d.ts +1 -1
  29. package/dist/actions/rpc_client.d.ts.map +1 -1
  30. package/dist/actions/rpc_client.js +2 -1
  31. package/dist/actions/socket.svelte.d.ts +53 -10
  32. package/dist/actions/socket.svelte.d.ts.map +1 -1
  33. package/dist/actions/socket.svelte.js +84 -23
  34. package/dist/actions/transports.d.ts +24 -0
  35. package/dist/actions/transports.d.ts.map +1 -1
  36. package/dist/actions/transports.js +30 -0
  37. package/dist/actions/transports_http.d.ts +11 -4
  38. package/dist/actions/transports_http.d.ts.map +1 -1
  39. package/dist/actions/transports_http.js +63 -18
  40. package/dist/actions/transports_ws.d.ts +28 -3
  41. package/dist/actions/transports_ws.d.ts.map +1 -1
  42. package/dist/actions/transports_ws.js +33 -4
  43. package/dist/actions/ws_endpoint_spec.d.ts +7 -0
  44. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  45. package/dist/http/CLAUDE.md +7 -3
  46. package/dist/http/jsonrpc_helpers.d.ts +0 -5
  47. package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
  48. package/dist/http/jsonrpc_helpers.js +0 -13
  49. package/dist/http/schema_helpers.d.ts +56 -0
  50. package/dist/http/schema_helpers.d.ts.map +1 -1
  51. package/dist/http/schema_helpers.js +91 -0
  52. package/dist/server/app_server.d.ts.map +1 -1
  53. package/dist/server/app_server.js +1 -0
  54. package/dist/testing/CLAUDE.md +17 -12
  55. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  56. package/dist/testing/cross_backend/peer_ping_ws.js +18 -0
  57. package/dist/testing/cross_backend/ws_round_trip.d.ts +11 -3
  58. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  59. package/dist/testing/cross_backend/ws_round_trip.js +74 -12
  60. package/dist/testing/transports/ws_client.d.ts +6 -0
  61. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  62. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  63. package/dist/testing/transports/ws_transport.js +6 -1
  64. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  65. package/dist/testing/ws_round_trip.js +5 -0
  66. package/dist/ui/form_state.svelte.d.ts +9 -4
  67. package/dist/ui/form_state.svelte.d.ts.map +1 -1
  68. package/dist/ui/form_state.svelte.js +28 -8
  69. package/package.json +2 -2
  70. package/dist/actions/request_tracker.svelte.d.ts +0 -77
  71. package/dist/actions/request_tracker.svelte.d.ts.map +0 -1
  72. package/dist/actions/request_tracker.svelte.js +0 -169
@@ -223,6 +223,7 @@ export const create_ws_test_harness = (options) => {
223
223
  // test closed the socket.
224
224
  const close_waiters = [];
225
225
  let is_closed = false;
226
+ let close_code = null;
226
227
  // Captured in `ws.close` below; `client.close(...)` returns it so
227
228
  // tests can await async `on_socket_close` cleanup.
228
229
  let close_pending = null;
@@ -240,6 +241,7 @@ export const create_ws_test_harness = (options) => {
240
241
  if (is_closed)
241
242
  return;
242
243
  is_closed = true;
244
+ close_code = code ?? 1000;
243
245
  for (const resolve of close_waiters.splice(0))
244
246
  resolve();
245
247
  const close_event = new Event('close');
@@ -326,6 +328,9 @@ export const create_ws_test_harness = (options) => {
326
328
  if (close_pending)
327
329
  await close_pending;
328
330
  },
331
+ get close_code() {
332
+ return close_code;
333
+ },
329
334
  wait_for: wait_for_impl,
330
335
  wait_for_close: (timeout_ms = WS_CLIENT_DEFAULT_TIMEOUT_MS) => {
331
336
  if (is_closed)
@@ -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 advancing
9
- * between focusable elements.
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.
@@ -48,8 +52,9 @@ export declare class FormState {
48
52
  */
49
53
  get attempted(): boolean;
50
54
  /**
51
- * Creates a form attachment that handles Enter key advancing between
52
- * focusable elements and tracks field touched state via delegated `focusout`.
55
+ * Creates a form attachment that handles the Enter key (advance focus to
56
+ * the next input, or submit from the last one) and tracks field touched
57
+ * state via delegated `focusout`.
53
58
  *
54
59
  * Fields are identified by their `name` attribute.
55
60
  *
@@ -1 +1 @@
1
- {"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AASrD,qBAAa,SAAS;;IAKrB;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;;;;OAQG;IACH,IAAI,IAAI,UAAU,CAAC,eAAe,CAAC;IAuCnC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIlC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI5B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAiB1B;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAIf;;;;OAIG;IACH,KAAK,IAAI,IAAI;CAIb"}
1
+ {"version":3,"file":"form_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/form_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAUrD,qBAAa,SAAS;;IAKrB;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;;;;;;;OASG;IACH,IAAI,IAAI,UAAU,CAAC,eAAe,CAAC;IAmDnC;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIlC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI5B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAiB1B;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAIf;;;;OAIG;IACH,KAAK,IAAI,IAAI;CAIb"}
@@ -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 advancing
9
- * between focusable elements.
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
- const FOCUSABLE_SELECTOR = 'input:not(:disabled), button:not(:disabled)';
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 advancing between
60
- * focusable elements and tracks field touched state via delegated `focusout`.
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 elements = Array.from(form.querySelectorAll(FOCUSABLE_SELECTOR));
79
- const index = elements.indexOf(e.target);
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
- elements[(index + 1) % elements.length].focus();
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.113.0",
3
+ "version": "0.114.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -75,7 +75,7 @@
75
75
  "@electric-sql/pglite": "^0.4.5",
76
76
  "@fuzdev/blake3-wasm": "^0.2.0",
77
77
  "@fuzdev/fuz_code": "^0.48.0",
78
- "@fuzdev/fuz_css": "^0.63.3",
78
+ "@fuzdev/fuz_css": "^0.64.0",
79
79
  "@fuzdev/fuz_ui": "^0.208.0",
80
80
  "@fuzdev/fuz_util": "^0.69.0",
81
81
  "@fuzdev/gro": "^0.212.0",
@@ -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
- }