@fuzdev/fuz_app 0.10.1 → 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.
- package/dist/actions/action_bridge.d.ts +8 -8
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +5 -5
- package/dist/actions/action_codegen.d.ts +18 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +49 -6
- package/dist/actions/action_event.d.ts +60 -0
- package/dist/actions/action_event.d.ts.map +1 -0
- package/dist/actions/action_event.js +361 -0
- package/dist/actions/action_event_data.d.ts +639 -0
- package/dist/actions/action_event_data.d.ts.map +1 -0
- package/dist/actions/action_event_data.js +29 -0
- package/dist/actions/action_event_helpers.d.ts +73 -0
- package/dist/actions/action_event_helpers.d.ts.map +1 -0
- package/dist/actions/action_event_helpers.js +96 -0
- package/dist/actions/action_event_types.d.ts +31 -0
- package/dist/actions/action_event_types.d.ts.map +1 -0
- package/dist/actions/action_event_types.js +38 -0
- package/dist/actions/action_peer.d.ts +30 -0
- package/dist/actions/action_peer.d.ts.map +1 -0
- package/dist/actions/action_peer.js +146 -0
- package/dist/actions/action_spec.d.ts +1 -1
- package/dist/actions/action_spec.js +1 -1
- package/dist/actions/request_tracker.svelte.d.ts +69 -0
- package/dist/actions/request_tracker.svelte.d.ts.map +1 -0
- package/dist/actions/request_tracker.svelte.js +161 -0
- package/dist/actions/rpc_client.d.ts +43 -0
- package/dist/actions/rpc_client.d.ts.map +1 -0
- package/dist/actions/rpc_client.js +151 -0
- package/dist/actions/transports.d.ts +47 -0
- package/dist/actions/transports.d.ts.map +1 -0
- package/dist/actions/transports.js +108 -0
- package/dist/actions/transports_http.d.ts +16 -0
- package/dist/actions/transports_http.d.ts.map +1 -0
- package/dist/actions/transports_http.js +81 -0
- package/dist/actions/transports_ws.d.ts +26 -0
- package/dist/actions/transports_ws.d.ts.map +1 -0
- package/dist/actions/transports_ws.js +94 -0
- package/dist/actions/transports_ws_backend.d.ts +42 -0
- package/dist/actions/transports_ws_backend.d.ts.map +1 -0
- package/dist/actions/transports_ws_backend.js +133 -0
- package/dist/http/jsonrpc_errors.d.ts +2 -0
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +2 -0
- package/dist/http/surface.d.ts +3 -3
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/realtime/sse.d.ts +5 -3
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.d.ts +2 -2
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/server/app_server.d.ts +2 -2
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +2 -2
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/uuid.d.ts +12 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +9 -0
- 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"}
|