@fuzdev/fuz_app 0.10.0 → 0.11.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 (63) hide show
  1. package/dist/actions/action_bridge.d.ts +8 -8
  2. package/dist/actions/action_bridge.d.ts.map +1 -1
  3. package/dist/actions/action_bridge.js +5 -5
  4. package/dist/actions/action_codegen.d.ts +18 -1
  5. package/dist/actions/action_codegen.d.ts.map +1 -1
  6. package/dist/actions/action_codegen.js +49 -6
  7. package/dist/actions/action_event.d.ts +60 -0
  8. package/dist/actions/action_event.d.ts.map +1 -0
  9. package/dist/actions/action_event.js +361 -0
  10. package/dist/actions/action_event_data.d.ts +639 -0
  11. package/dist/actions/action_event_data.d.ts.map +1 -0
  12. package/dist/actions/action_event_data.js +29 -0
  13. package/dist/actions/action_event_helpers.d.ts +73 -0
  14. package/dist/actions/action_event_helpers.d.ts.map +1 -0
  15. package/dist/actions/action_event_helpers.js +96 -0
  16. package/dist/actions/action_event_types.d.ts +31 -0
  17. package/dist/actions/action_event_types.d.ts.map +1 -0
  18. package/dist/actions/action_event_types.js +38 -0
  19. package/dist/actions/action_peer.d.ts +30 -0
  20. package/dist/actions/action_peer.d.ts.map +1 -0
  21. package/dist/actions/action_peer.js +146 -0
  22. package/dist/actions/action_rpc.d.ts.map +1 -1
  23. package/dist/actions/action_rpc.js +6 -2
  24. package/dist/actions/action_spec.d.ts +1 -1
  25. package/dist/actions/action_spec.js +1 -1
  26. package/dist/actions/request_tracker.svelte.d.ts +69 -0
  27. package/dist/actions/request_tracker.svelte.d.ts.map +1 -0
  28. package/dist/actions/request_tracker.svelte.js +161 -0
  29. package/dist/actions/rpc_client.d.ts +43 -0
  30. package/dist/actions/rpc_client.d.ts.map +1 -0
  31. package/dist/actions/rpc_client.js +151 -0
  32. package/dist/actions/transports.d.ts +47 -0
  33. package/dist/actions/transports.d.ts.map +1 -0
  34. package/dist/actions/transports.js +108 -0
  35. package/dist/actions/transports_http.d.ts +16 -0
  36. package/dist/actions/transports_http.d.ts.map +1 -0
  37. package/dist/actions/transports_http.js +81 -0
  38. package/dist/actions/transports_ws.d.ts +26 -0
  39. package/dist/actions/transports_ws.d.ts.map +1 -0
  40. package/dist/actions/transports_ws.js +94 -0
  41. package/dist/actions/transports_ws_backend.d.ts +42 -0
  42. package/dist/actions/transports_ws_backend.d.ts.map +1 -0
  43. package/dist/actions/transports_ws_backend.js +133 -0
  44. package/dist/http/jsonrpc.d.ts +22 -97
  45. package/dist/http/jsonrpc.d.ts.map +1 -1
  46. package/dist/http/jsonrpc.js +11 -24
  47. package/dist/http/jsonrpc_errors.d.ts +2 -0
  48. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  49. package/dist/http/jsonrpc_errors.js +2 -0
  50. package/dist/http/surface.d.ts +3 -3
  51. package/dist/http/surface.d.ts.map +1 -1
  52. package/dist/realtime/sse.d.ts +5 -3
  53. package/dist/realtime/sse.d.ts.map +1 -1
  54. package/dist/realtime/sse_auth_guard.d.ts +2 -2
  55. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  56. package/dist/server/app_server.d.ts +2 -2
  57. package/dist/server/app_server.d.ts.map +1 -1
  58. package/dist/testing/stubs.d.ts +2 -2
  59. package/dist/testing/stubs.d.ts.map +1 -1
  60. package/dist/uuid.d.ts +12 -0
  61. package/dist/uuid.d.ts.map +1 -0
  62. package/dist/uuid.js +9 -0
  63. package/package.json +1 -1
@@ -0,0 +1,161 @@
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.js';
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
+ /**
16
+ * Represents a pending request with its associated state.
17
+ */
18
+ export class RequestTrackerItem {
19
+ id;
20
+ deferred;
21
+ created;
22
+ status = $state.raw();
23
+ timeout = $state.raw();
24
+ constructor(id, deferred, created, status, timeout) {
25
+ this.id = id;
26
+ this.deferred = deferred;
27
+ this.created = created;
28
+ this.status = status;
29
+ this.timeout = timeout;
30
+ }
31
+ }
32
+ /**
33
+ * Tracks JSON-RPC requests and their responses to manage promises and timeouts.
34
+ * Used by transports to handle the request-response lifecycle.
35
+ */
36
+ export class RequestTracker {
37
+ pending_requests = new SvelteMap();
38
+ request_timeout_ms;
39
+ constructor(request_timeout_ms = 120_000) {
40
+ this.request_timeout_ms = request_timeout_ms;
41
+ }
42
+ /**
43
+ * Track a new request with the given id.
44
+ * @param id - the request id
45
+ * @returns a deferred promise that will be resolved when the response is received
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 the given response data.
70
+ * @param id - the request id
71
+ * @param response - the response data
72
+ */
73
+ resolve_request(id, response) {
74
+ const request = this.pending_requests.get(id);
75
+ if (!request) {
76
+ console.warn(`received response for unknown request: ${id}`);
77
+ return;
78
+ }
79
+ // Clear the timeout and resolve the promise
80
+ if (request.timeout) {
81
+ clearTimeout(request.timeout);
82
+ request.timeout = undefined;
83
+ }
84
+ request.status = 'success';
85
+ request.deferred.resolve(response);
86
+ this.pending_requests.delete(id);
87
+ }
88
+ /**
89
+ * Rejects a pending request with the given error.
90
+ * @param id - the request id
91
+ * @param error_message - the complete `JsonrpcErrorResponse` object
92
+ */
93
+ reject_request(id, error_message) {
94
+ const request = this.pending_requests.get(id);
95
+ if (!request) {
96
+ console.warn(`received error for unknown request: ${id}`);
97
+ return;
98
+ }
99
+ // Clear the timeout and reject the promise
100
+ if (request.timeout) {
101
+ clearTimeout(request.timeout);
102
+ request.timeout = undefined;
103
+ }
104
+ request.status = 'failure';
105
+ const error = new ThrownJsonrpcError(error_message.error.code, error_message.error.message, error_message.error.data);
106
+ request.deferred.reject(error);
107
+ this.pending_requests.delete(id);
108
+ }
109
+ /**
110
+ * Handles an incoming JSON-RPC message. Resolves or rejects the associated request.
111
+ * Ignores notifications and unknown/invalid messages.
112
+ */
113
+ handle_message(message) {
114
+ if (!message)
115
+ return; // ignore invalid values
116
+ const { id } = message;
117
+ // TODO maybe log a warning/error?
118
+ if (id == null)
119
+ return; // ignore notifications and errors without ids
120
+ // JSON-RPC responses require both an `id` and either a `result` or `error` field, but not both
121
+ if ('result' in message) {
122
+ this.resolve_request(id, message);
123
+ }
124
+ else if ('error' in message) {
125
+ this.reject_request(id, message);
126
+ }
127
+ // ignore other messages
128
+ }
129
+ /**
130
+ * Cancel a pending request.
131
+ * @param id - the request id
132
+ */
133
+ cancel_request(id) {
134
+ const request = this.pending_requests.get(id);
135
+ if (!request) {
136
+ return;
137
+ }
138
+ if (request.timeout) {
139
+ clearTimeout(request.timeout);
140
+ request.timeout = undefined;
141
+ }
142
+ // We don't reject the promise here, just clean up the tracking
143
+ this.pending_requests.delete(id);
144
+ }
145
+ /**
146
+ * Cancel all pending requests.
147
+ * @param reason - optional reason to include in rejection
148
+ */
149
+ cancel_all_requests(reason) {
150
+ for (const [id, request] of this.pending_requests.entries()) {
151
+ if (request.timeout) {
152
+ clearTimeout(request.timeout);
153
+ request.timeout = undefined;
154
+ }
155
+ request.status = 'failure';
156
+ request.deferred.reject(new ThrownJsonrpcError(JSONRPC_ERROR_CODES.internal_error, // TODO canceled error?
157
+ reason || 'request cancelled'));
158
+ this.pending_requests.delete(id);
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Typed RPC client — creates a Proxy-based API from action specs.
3
+ *
4
+ * Two tiers of usage:
5
+ * - **Tier 1** (simple, for tx): transport send/receive, Result return. No `environment`.
6
+ * - **Tier 2** (full, for zzz): ActionEvent lifecycle with `environment`.
7
+ *
8
+ * Consumers cast the return to their generated `ActionsApi` interface for full type safety.
9
+ *
10
+ * @module
11
+ */
12
+ import type { ActionEventEnvironment } from './action_event_types.js';
13
+ import type { ActionPeer } from './action_peer.js';
14
+ import type { ActionEventDataUnion } from './action_event_data.js';
15
+ /** Duck-typed action history — consumers pass their concrete Actions cell. */
16
+ export interface RpcClientActionHistory {
17
+ add_from_json: (json: {
18
+ method: string;
19
+ action_event_data: ActionEventDataUnion;
20
+ }) => {
21
+ listen_to_action_event: (event: any) => void;
22
+ } | undefined;
23
+ }
24
+ /** Options for `create_rpc_client`. */
25
+ export interface CreateRpcClientOptions {
26
+ peer: ActionPeer;
27
+ environment: ActionEventEnvironment;
28
+ /** Optional action history tracking (duck-typed Actions cell). */
29
+ actions?: RpcClientActionHistory;
30
+ }
31
+ /**
32
+ * Creates a Proxy-based API from action specs.
33
+ *
34
+ * Method calls are dynamically dispatched based on the action spec's kind:
35
+ * - `request_response` → send request, await response, return Result
36
+ * - `remote_notification` → send notification, return Result
37
+ * - `local_call` → execute locally (sync or async), return Result or throw
38
+ *
39
+ * @param options - client options (peer, environment, optional action history)
40
+ * @returns a Proxy that responds to any method name found in the environment's specs
41
+ */
42
+ export declare const create_rpc_client: (options: CreateRpcClientOptions) => Record<string, (...args: Array<any>) => any>;
43
+ //# sourceMappingURL=rpc_client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAMjE,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;CACjC;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"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Typed RPC client — creates a Proxy-based API from action specs.
3
+ *
4
+ * Two tiers of usage:
5
+ * - **Tier 1** (simple, for tx): transport send/receive, Result return. No `environment`.
6
+ * - **Tier 2** (full, for zzz): ActionEvent lifecycle with `environment`.
7
+ *
8
+ * Consumers cast the return to their generated `ActionsApi` interface for full type safety.
9
+ *
10
+ * @module
11
+ */
12
+ import { create_action_event } from './action_event.js';
13
+ import { is_send_request, is_notification_send, extract_action_result, } from './action_event_helpers.js';
14
+ /**
15
+ * Creates a Proxy-based API from action specs.
16
+ *
17
+ * Method calls are dynamically dispatched based on the action spec's kind:
18
+ * - `request_response` → send request, await response, return Result
19
+ * - `remote_notification` → send notification, return Result
20
+ * - `local_call` → execute locally (sync or async), return Result or throw
21
+ *
22
+ * @param options - client options (peer, environment, optional action history)
23
+ * @returns a Proxy that responds to any method name found in the environment's specs
24
+ */
25
+ export const create_rpc_client = (options) => {
26
+ const { peer, environment, actions } = options;
27
+ return new Proxy({}, {
28
+ get(_target, method) {
29
+ const spec = environment.lookup_action_spec(method);
30
+ if (!spec) {
31
+ return undefined;
32
+ }
33
+ return create_action_method(peer, environment, spec, actions);
34
+ },
35
+ has(_target, method) {
36
+ return environment.lookup_action_spec(method) !== undefined;
37
+ },
38
+ });
39
+ };
40
+ /**
41
+ * Creates a method that executes an action through its complete lifecycle.
42
+ */
43
+ const create_action_method = (peer, environment, spec, actions) => {
44
+ switch (spec.kind) {
45
+ case 'local_call':
46
+ return spec.async
47
+ ? create_async_local_call_method(environment, spec, actions)
48
+ : create_sync_local_call_method(environment, spec, actions);
49
+ case 'request_response':
50
+ return create_request_response_method(peer, environment, spec, actions);
51
+ case 'remote_notification':
52
+ return create_remote_notification_method(peer, environment, spec, actions);
53
+ }
54
+ };
55
+ /**
56
+ * Creates a synchronous local call method.
57
+ * Returns value directly - can throw on error (sync methods cannot return Result).
58
+ */
59
+ const create_sync_local_call_method = (environment, spec, actions) => {
60
+ return (input) => {
61
+ const event = create_action_event(environment, spec, input);
62
+ const action = actions?.add_from_json({
63
+ method: spec.method,
64
+ action_event_data: event.toJSON(),
65
+ });
66
+ action?.listen_to_action_event(event);
67
+ event.parse().handle_sync();
68
+ const result = extract_action_result(event);
69
+ if (result.ok) {
70
+ return result.value;
71
+ }
72
+ else {
73
+ // Sync methods must throw on error (cannot return Result synchronously)
74
+ throw new Error(`${spec.method} failed: ${result.error.message}`);
75
+ }
76
+ };
77
+ };
78
+ /**
79
+ * Creates an asynchronous local call method.
80
+ * Returns Result for type-safe error handling.
81
+ */
82
+ const create_async_local_call_method = (environment, spec, actions) => {
83
+ return async (input) => {
84
+ const event = create_action_event(environment, spec, input);
85
+ const action = actions?.add_from_json({
86
+ method: spec.method,
87
+ action_event_data: event.toJSON(),
88
+ });
89
+ action?.listen_to_action_event(event);
90
+ await event.parse().handle_async();
91
+ return extract_action_result(event);
92
+ };
93
+ };
94
+ /**
95
+ * Creates a request/response method that communicates over the network.
96
+ */
97
+ const create_request_response_method = (peer, environment, spec, actions) => {
98
+ return async (input) => {
99
+ const event = create_action_event(environment, spec, input);
100
+ const action = actions?.add_from_json({
101
+ method: spec.method,
102
+ action_event_data: event.toJSON(),
103
+ });
104
+ action?.listen_to_action_event(event);
105
+ await event.parse().handle_async();
106
+ // Check if we're in send_error phase before type narrowing
107
+ if (event.data.kind === 'request_response' && event.data.phase === 'send_error') {
108
+ await event.handle_async(); // Call send_error handler
109
+ return extract_action_result(event);
110
+ }
111
+ if (!is_send_request(event.data))
112
+ throw Error(); // TODO @many maybe make this an assertion helper?
113
+ if (event.data.step !== 'handled') {
114
+ return extract_action_result(event);
115
+ }
116
+ const response = await peer.send(event.data.request);
117
+ event.transition('receive_response');
118
+ // TODO @api shouldn't this happen in the peer like the other method calls?
119
+ event.set_response(response);
120
+ event.parse(); // May transition to receive_error
121
+ await event.handle_async();
122
+ return extract_action_result(event);
123
+ };
124
+ };
125
+ /**
126
+ * Creates a remote notification method (fire and forget).
127
+ * Returns Result<{value: void}> for consistency.
128
+ */
129
+ const create_remote_notification_method = (peer, environment, spec, actions) => {
130
+ return async (input) => {
131
+ const event = create_action_event(environment, spec, input);
132
+ const action = actions?.add_from_json({
133
+ method: spec.method,
134
+ action_event_data: event.toJSON(),
135
+ });
136
+ action?.listen_to_action_event(event);
137
+ await event.parse().handle_async();
138
+ if (!is_notification_send(event.data))
139
+ throw Error(); // TODO @many maybe make this an assertion helper?
140
+ if (event.data.step === 'handled') {
141
+ const send_result = await peer.send(event.data.notification);
142
+ // Check if notification failed to send
143
+ if (send_result !== null) {
144
+ environment.log?.error('notification send failed:', send_result.error);
145
+ return { ok: false, error: send_result.error };
146
+ }
147
+ return { ok: true, value: undefined };
148
+ }
149
+ return extract_action_result(event);
150
+ };
151
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Transport abstraction for action communication.
3
+ *
4
+ * Provides the `Transport` interface and `Transports` registry for managing
5
+ * multiple transports with fallback behavior.
6
+ *
7
+ * @module
8
+ */
9
+ import { z } from 'zod';
10
+ import type { JsonrpcMessageFromClientToServer, JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
11
+ /** WebSocket close code for session revocation. */
12
+ export declare const WS_CLOSE_SESSION_REVOKED = 4001;
13
+ export declare const TransportName: z.ZodString;
14
+ export type TransportName = z.infer<typeof TransportName>;
15
+ export interface Transport {
16
+ transport_name: TransportName;
17
+ send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
18
+ send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
19
+ send(message: JsonrpcMessageFromClientToServer): Promise<JsonrpcMessageFromServerToClient | null>;
20
+ is_ready: () => boolean;
21
+ dispose?: () => void;
22
+ }
23
+ export declare class Transports {
24
+ #private;
25
+ /**
26
+ * Whether to allow fallback to other transports if the current one is not available.
27
+ * @default true
28
+ */
29
+ allow_fallback: boolean;
30
+ /**
31
+ * Registers a transport.
32
+ */
33
+ register_transport(transport: Transport): void;
34
+ set_current_transport(transport_name: TransportName): void;
35
+ /**
36
+ * Gets either the current transport or the first ready transport
37
+ * depending on `allow_fallback`, or throws an error.
38
+ * @param transport_name - optional transport to use instead of the current
39
+ * @throws when no transport available or ready
40
+ */
41
+ get_transport(transport_name?: TransportName): Transport | null;
42
+ is_ready(): boolean | null;
43
+ get_current_transport(): Transport | null;
44
+ get_current_transport_name(): TransportName | null;
45
+ get_transport_by_name(transport_name: TransportName): Transport | null;
46
+ }
47
+ //# sourceMappingURL=transports.d.ts.map
@@ -0,0 +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;AACtB,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;AAK7C,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACzE,IAAI,CAAC,OAAO,EAAE,gCAAgC,GAAG,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IAClG,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;OAEG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAS9C,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;OAKG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CAqDtE"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Transport abstraction for action communication.
3
+ *
4
+ * Provides the `Transport` interface and `Transports` registry for managing
5
+ * multiple transports with fallback behavior.
6
+ *
7
+ * @module
8
+ */
9
+ import { z } from 'zod';
10
+ /** WebSocket close code for session revocation. */
11
+ export const WS_CLOSE_SESSION_REVOKED = 4001;
12
+ // TODO figure out the symmetry of frontend and backend transports (none/partial/full?) --
13
+ // we may also need orthogonal abstractions to clarify the transport role
14
+ export const TransportName = z.string(); // not branded for convenience, will just error at runtime, the schema is just for docs atm
15
+ export class Transports {
16
+ #current_transport = null;
17
+ #transport_by_name = new Map();
18
+ /**
19
+ * Whether to allow fallback to other transports if the current one is not available.
20
+ * @default true
21
+ */
22
+ allow_fallback = true; // TODO allow registering transports with a priority level so this can be customized
23
+ /**
24
+ * Registers a transport.
25
+ */
26
+ register_transport(transport) {
27
+ this.#transport_by_name.set(transport.transport_name, transport); // TODO maybe ensure unregistering of any previous transport?
28
+ // Set current transport if not already set
29
+ if (!this.#current_transport) {
30
+ this.#current_transport = transport;
31
+ }
32
+ }
33
+ set_current_transport(transport_name) {
34
+ const transport = this.#transport_by_name.get(transport_name);
35
+ if (!transport)
36
+ throw new Error(`transport not registered: ${transport_name}`);
37
+ this.#current_transport = transport;
38
+ }
39
+ /**
40
+ * Gets either the current transport or the first ready transport
41
+ * depending on `allow_fallback`, or throws an error.
42
+ * @param transport_name - optional transport to use instead of the current
43
+ * @throws when no transport available or ready
44
+ */
45
+ get_transport(transport_name) {
46
+ return this.allow_fallback
47
+ ? this.#get_first_ready(transport_name)
48
+ : this.#get_exact(transport_name);
49
+ }
50
+ // TODO these 4 arent used yet but seem useful? `get_transport` is the main method
51
+ is_ready() {
52
+ const transport = this.#current_transport;
53
+ if (!transport)
54
+ return null;
55
+ return transport.is_ready();
56
+ }
57
+ get_current_transport() {
58
+ return this.#current_transport ?? null;
59
+ }
60
+ get_current_transport_name() {
61
+ return this.#current_transport?.transport_name ?? null;
62
+ }
63
+ get_transport_by_name(transport_name) {
64
+ return this.#transport_by_name.get(transport_name) ?? null;
65
+ }
66
+ /**
67
+ * Gets the specified transport, defaulting to the current, or throws an error.
68
+ * @param transport_name - optional transport type to use instead of the current
69
+ * @throws when no transport available or ready
70
+ */
71
+ #get_exact(transport_name) {
72
+ const transport = transport_name
73
+ ? this.#transport_by_name.get(transport_name)
74
+ : this.#current_transport;
75
+ if (transport?.is_ready()) {
76
+ return transport;
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Gets the appropriate transport or throws an error.
82
+ * @param transport_name - optional transport type or array of types to use instead of the current
83
+ * @throws when no transport available or ready
84
+ */
85
+ #get_first_ready(transport_name) {
86
+ // First try the specified transport(s) if provided
87
+ if (transport_name) {
88
+ const transport_names = Array.isArray(transport_name) ? transport_name : [transport_name];
89
+ for (const transport_name of transport_names) {
90
+ const transport = this.#transport_by_name.get(transport_name);
91
+ if (transport?.is_ready()) {
92
+ return transport;
93
+ }
94
+ }
95
+ }
96
+ // Then try the current transport if it's ready
97
+ if (this.#current_transport?.is_ready()) {
98
+ return this.#current_transport;
99
+ }
100
+ // Finally, try any other available transport
101
+ for (const transport of this.#transport_by_name.values()) {
102
+ if (transport.is_ready()) {
103
+ return transport;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * HTTP transport — sends JSON-RPC messages via HTTP POST (or GET for reads).
3
+ *
4
+ * @module
5
+ */
6
+ import type { JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
7
+ import type { Transport } from './transports.js';
8
+ export declare class FrontendHttpTransport implements Transport {
9
+ #private;
10
+ readonly transport_name: "frontend_http_rpc";
11
+ constructor(url: string, headers?: Record<string, string>, has_side_effects?: (method: string) => boolean);
12
+ send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
13
+ send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
14
+ is_ready(): boolean;
15
+ }
16
+ //# sourceMappingURL=transports_http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transports_http.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,OAAO,KAAK,EAGX,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAE/C,qBAAa,qBAAsB,YAAW,SAAS;;IACtD,QAAQ,CAAC,cAAc,EAAG,mBAAmB,CAAU;gBAOtD,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO;IAOzC,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAuE9E,QAAQ,IAAI,OAAO;CAGnB"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * HTTP transport — sends JSON-RPC messages via HTTP POST (or GET for reads).
3
+ *
4
+ * @module
5
+ */
6
+ import { DEV } from 'esm-env';
7
+ import { ThrownJsonrpcError, jsonrpc_error_messages, http_status_to_jsonrpc_error_code, UNKNOWN_ERROR_MESSAGE, } from '../http/jsonrpc_errors.js';
8
+ import { create_jsonrpc_error_response, to_jsonrpc_message_id, is_jsonrpc_error_response, } from '../http/jsonrpc_helpers.js';
9
+ export class FrontendHttpTransport {
10
+ transport_name = 'frontend_http_rpc';
11
+ #url;
12
+ #headers;
13
+ #has_side_effects;
14
+ constructor(url, headers, has_side_effects) {
15
+ this.#url = url;
16
+ this.#headers = headers ?? { 'content-type': 'application/json', accept: 'application/json' };
17
+ this.#has_side_effects = has_side_effects;
18
+ }
19
+ async send(message) {
20
+ try {
21
+ let response;
22
+ if (this.#has_side_effects && !this.#has_side_effects(message.method) && 'id' in message) {
23
+ // GET for read-only actions (matching fuz_app's create_rpc_endpoint GET convention)
24
+ const search_params = new URLSearchParams();
25
+ search_params.set('method', message.method);
26
+ search_params.set('id', String(message.id));
27
+ if (message.params !== undefined) {
28
+ search_params.set('params', JSON.stringify(message.params));
29
+ }
30
+ const separator = this.#url.includes('?') ? '&' : '?';
31
+ response = await fetch(`${this.#url}${separator}${search_params.toString()}`, {
32
+ method: 'GET',
33
+ headers: this.#headers,
34
+ });
35
+ }
36
+ else {
37
+ response = await fetch(this.#url, {
38
+ method: 'POST',
39
+ headers: this.#headers,
40
+ body: JSON.stringify(message),
41
+ // TODO
42
+ // signal: AbortSignal.timeout(REQUEST_TIMEOUT),
43
+ });
44
+ }
45
+ const result = await response.json();
46
+ // For JSON-RPC, we always expect a 200 OK response.
47
+ // The actual error will be in the JSON-RPC error field.
48
+ if (!response.ok) {
49
+ return create_jsonrpc_error_response(to_jsonrpc_message_id(message), {
50
+ code: http_status_to_jsonrpc_error_code(response.status),
51
+ message: `HTTP error: ${response.status} ${response.statusText}`,
52
+ });
53
+ }
54
+ // In development, check if we got a JSON-RPC error with HTTP 200
55
+ // and verify the error code matches the expected HTTP status.
56
+ if (DEV && is_jsonrpc_error_response(result)) {
57
+ const expected_code = http_status_to_jsonrpc_error_code(response.status);
58
+ const actual_code = result.error.code;
59
+ if (actual_code !== expected_code) {
60
+ console.warn(`[http_transport] JSON-RPC error code mismatch: got ${actual_code} but ${response.status} should map to ${expected_code}`, result);
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ catch (error) {
66
+ if (error instanceof ThrownJsonrpcError) {
67
+ return create_jsonrpc_error_response(to_jsonrpc_message_id(message), {
68
+ code: error.code,
69
+ message: error.message,
70
+ data: error.data,
71
+ });
72
+ }
73
+ return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.internal_error('error sending request', {
74
+ error: error.message || UNKNOWN_ERROR_MESSAGE,
75
+ }));
76
+ }
77
+ }
78
+ is_ready() {
79
+ return true;
80
+ }
81
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WebSocket transport — sends JSON-RPC messages via WebSocket with request tracking.
3
+ *
4
+ * @module
5
+ */
6
+ import type { JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
7
+ import type { Transport } from './transports.js';
8
+ /**
9
+ * Minimal interface for a WebSocket connection, decoupled from the concrete Socket Cell.
10
+ */
11
+ export interface WebsocketConnection {
12
+ send: (data: object) => boolean;
13
+ readonly connected: boolean;
14
+ add_message_handler: (handler: (event: MessageEvent) => void) => () => void;
15
+ add_error_handler: (handler: (event: Event) => void) => () => void;
16
+ }
17
+ export declare class FrontendWebsocketTransport implements Transport {
18
+ #private;
19
+ readonly transport_name: "frontend_websocket_rpc";
20
+ constructor(connection: WebsocketConnection, receive: (data: unknown) => Promise<unknown>, request_timeout_ms?: number);
21
+ send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
22
+ send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
23
+ is_ready(): boolean;
24
+ dispose(): void;
25
+ }
26
+ //# sourceMappingURL=transports_ws.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transports_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,KAAK,EAGX,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAI/C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mBAAmB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC5E,iBAAiB,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CACnE;AAED,qBAAa,0BAA2B,YAAW,SAAS;;IAC3D,QAAQ,CAAC,cAAc,EAAG,wBAAwB,CAAU;gBAS3D,UAAU,EAAE,mBAAmB,EAC/B,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5C,kBAAkB,CAAC,EAAE,MAAM;IAkCtB,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA8C9E,QAAQ,IAAI,OAAO;IAInB,OAAO,IAAI,IAAI;CAUf"}