@fuzdev/fuz_app 0.23.0 → 0.25.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_codegen.d.ts +25 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +39 -0
- package/dist/actions/action_peer.d.ts +7 -0
- package/dist/actions/action_peer.d.ts.map +1 -1
- package/dist/actions/action_peer.js +1 -1
- package/dist/actions/action_types.d.ts +72 -0
- package/dist/actions/action_types.d.ts.map +1 -0
- package/dist/actions/action_types.js +11 -0
- package/dist/actions/cancel.d.ts +78 -0
- package/dist/actions/cancel.d.ts.map +1 -0
- package/dist/actions/cancel.js +79 -0
- package/dist/actions/heartbeat.d.ts +51 -0
- package/dist/actions/heartbeat.d.ts.map +1 -0
- package/dist/actions/heartbeat.js +50 -0
- package/dist/actions/register_action_ws.d.ts +28 -30
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +103 -20
- package/dist/actions/rpc_client.d.ts +10 -0
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +22 -7
- package/dist/actions/socket.svelte.d.ts +88 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +322 -6
- package/dist/actions/transports.d.ts +18 -3
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +4 -0
- package/dist/actions/transports_http.d.ts +3 -3
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_http.js +4 -3
- package/dist/actions/transports_ws.d.ts +33 -6
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +43 -46
- package/dist/actions/transports_ws_backend.d.ts +12 -3
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +12 -1
- package/dist/auth/bearer_auth.js +0 -1
- package/dist/auth/keyring.d.ts.map +1 -1
- package/dist/auth/keyring.js +0 -2
- package/dist/auth/migrations.js +4 -4
- package/dist/db/migrate.d.ts +12 -2
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +25 -16
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +0 -2
- package/dist/dev/setup.js +2 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +0 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -3
- package/dist/testing/app_server.js +1 -1
- package/dist/testing/data_exposure.js +6 -8
- package/dist/testing/db.js +1 -1
- package/dist/testing/integration.js +0 -1
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +0 -6
- package/dist/testing/rpc_round_trip.js +4 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -2
- package/dist/testing/ws_round_trip.d.ts +15 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +3 -3
- package/package.json +2 -2
|
@@ -31,7 +31,12 @@ import { JSONRPC_VERSION } from '../http/jsonrpc.js';
|
|
|
31
31
|
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
32
32
|
import { create_jsonrpc_error_response, create_jsonrpc_error_response_from_thrown, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
|
|
33
33
|
import { CREDENTIAL_TYPE_KEY, AUTH_API_TOKEN_ID_KEY } from '../hono_context.js';
|
|
34
|
+
import {} from './action_types.js';
|
|
35
|
+
import { CANCEL_METHOD, CancelNotificationParams } from './cancel.js';
|
|
36
|
+
import { WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT } from './transports.js';
|
|
34
37
|
import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
38
|
+
/** Default inactivity window before the server closes a silent socket. */
|
|
39
|
+
export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
|
|
35
40
|
/**
|
|
36
41
|
* Mount a JSON-RPC WebSocket endpoint that dispatches to the supplied handler
|
|
37
42
|
* map. Per-request context is built from the base + consumer-provided
|
|
@@ -51,9 +56,24 @@ import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
|
51
56
|
* `create_ws_auth_guard` or broadcast on audit events.
|
|
52
57
|
*/
|
|
53
58
|
export const register_action_ws = (options) => {
|
|
54
|
-
const { path, app, upgradeWebSocket,
|
|
55
|
-
//
|
|
56
|
-
|
|
59
|
+
const { path, app, upgradeWebSocket, actions, extend_context, transport = new BackendWebsocketTransport(), heartbeat = true, artificial_delay = 0, log = new Logger('[ws]'), on_socket_open, on_socket_close, } = options;
|
|
60
|
+
// Fan the unified actions array into the two lookups the dispatcher
|
|
61
|
+
// consults at message time. Keeping them internal means the composable
|
|
62
|
+
// `{spec, handler}` tuple remains the only shape consumers name.
|
|
63
|
+
const spec_by_method = new Map();
|
|
64
|
+
const handlers = {};
|
|
65
|
+
for (const action of actions) {
|
|
66
|
+
spec_by_method.set(action.spec.method, action.spec);
|
|
67
|
+
if (action.handler)
|
|
68
|
+
handlers[action.spec.method] = action.handler;
|
|
69
|
+
}
|
|
70
|
+
const heartbeat_enabled = heartbeat !== false;
|
|
71
|
+
const heartbeat_config = typeof heartbeat === 'object' ? heartbeat : {};
|
|
72
|
+
const heartbeat_timeout = heartbeat_config.timeout ?? DEFAULT_SERVER_HEARTBEAT_TIMEOUT;
|
|
73
|
+
// Run the checker on timeout/2 so event-loop blockage pauses the timer
|
|
74
|
+
// itself — a dead-because-blocked socket is close enough to
|
|
75
|
+
// dead-because-unresponsive that closing is arguably correct.
|
|
76
|
+
const heartbeat_tick_interval = Math.max(100, Math.floor(heartbeat_timeout / 2));
|
|
57
77
|
app.get(path, upgradeWebSocket((c) => {
|
|
58
78
|
// Upgrade-time auth extraction — `require_auth` middleware has already
|
|
59
79
|
// rejected unauthenticated requests, so request_context is guaranteed
|
|
@@ -69,12 +89,19 @@ export const register_action_ws = (options) => {
|
|
|
69
89
|
// `close_sockets_for_token` to tear down just this socket on
|
|
70
90
|
// `token_revoke` without affecting the account's other sockets.
|
|
71
91
|
const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY);
|
|
72
|
-
// Per-socket abort controller — fires on socket close,
|
|
73
|
-
// every in-flight handler's
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
92
|
+
// Per-socket abort controller — fires on socket close, chained into
|
|
93
|
+
// every in-flight handler's per-request controller via
|
|
94
|
+
// `AbortSignal.any`. Keeping both signals lets the client
|
|
95
|
+
// cancel-one-request-by-id (via the `cancel` notification) without
|
|
96
|
+
// tearing down the whole socket.
|
|
77
97
|
const socket_abort_controller = new AbortController();
|
|
98
|
+
// Per-request controllers keyed by JSON-RPC request id — lets an
|
|
99
|
+
// incoming `cancel` notification abort just the matching handler.
|
|
100
|
+
// Populated on request dispatch, cleared in the handler's `finally`
|
|
101
|
+
// so a late-arriving cancel for a completed id (or a reused id)
|
|
102
|
+
// can't null-abort a freshly-arrived request. Idempotent: cancel
|
|
103
|
+
// for unknown ids no-ops.
|
|
104
|
+
const pending_controllers = new Map();
|
|
78
105
|
// Identity is assembled at upgrade time so `on_socket_close` can
|
|
79
106
|
// still read it after the audit guard tears the transport record
|
|
80
107
|
// down; `BackendWebsocketTransport.#revoke_connection` clears the
|
|
@@ -83,6 +110,18 @@ export const register_action_ws = (options) => {
|
|
|
83
110
|
// Captured on open, consumed on close. Null before onOpen fires or
|
|
84
111
|
// when a consumer never opens (e.g. immediate disconnect).
|
|
85
112
|
let captured_connection_id = null;
|
|
113
|
+
// Receive-silence watchdog. Seeded to open-time so the first window is
|
|
114
|
+
// exempt (cold-start grace — avoid killing mid-handshake sockets).
|
|
115
|
+
// Bumped by onMessage. Any incoming activity counts, not just
|
|
116
|
+
// heartbeats — chatty clients don't need to send extras.
|
|
117
|
+
let last_receive_time = 0;
|
|
118
|
+
let heartbeat_timer = null;
|
|
119
|
+
const stop_heartbeat_timer = () => {
|
|
120
|
+
if (heartbeat_timer !== null) {
|
|
121
|
+
clearInterval(heartbeat_timer);
|
|
122
|
+
heartbeat_timer = null;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
86
125
|
// Socket-scoped notification helper — routes to this socket only,
|
|
87
126
|
// matches the `ctx.notify` semantics exposed to per-message handlers.
|
|
88
127
|
const notify_socket = (ws) => (notify_method, notify_params) => {
|
|
@@ -99,6 +138,23 @@ export const register_action_ws = (options) => {
|
|
|
99
138
|
const connection_id = transport.add_connection(ws, token_hash, account_id, api_token_id);
|
|
100
139
|
captured_connection_id = connection_id;
|
|
101
140
|
log.debug('ws opened', connection_id, event);
|
|
141
|
+
if (heartbeat_enabled) {
|
|
142
|
+
last_receive_time = Date.now();
|
|
143
|
+
heartbeat_timer = setInterval(() => {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const silence = now - last_receive_time;
|
|
146
|
+
if (silence >= heartbeat_timeout) {
|
|
147
|
+
log.info(`heartbeat timeout (${silence}ms) — closing ${WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT}`, connection_id, identity.account_id);
|
|
148
|
+
stop_heartbeat_timer();
|
|
149
|
+
try {
|
|
150
|
+
ws.close(WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT, 'server heartbeat timeout');
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
log.error('heartbeat timeout close failed:', error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}, heartbeat_tick_interval);
|
|
157
|
+
}
|
|
102
158
|
if (on_socket_open) {
|
|
103
159
|
try {
|
|
104
160
|
await on_socket_open({
|
|
@@ -122,6 +178,7 @@ export const register_action_ws = (options) => {
|
|
|
122
178
|
}
|
|
123
179
|
},
|
|
124
180
|
onMessage: async (event, ws) => {
|
|
181
|
+
last_receive_time = Date.now();
|
|
125
182
|
let json;
|
|
126
183
|
try {
|
|
127
184
|
json = JSON.parse(String(event.data)); // eslint-disable-line @typescript-eslint/no-base-to-string
|
|
@@ -136,9 +193,26 @@ export const register_action_ws = (options) => {
|
|
|
136
193
|
ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.invalid_request('batch JSON-RPC requests are not supported on WebSocket'))));
|
|
137
194
|
return;
|
|
138
195
|
}
|
|
139
|
-
//
|
|
196
|
+
// Notifications (method + no id) — `cancel` is intercepted
|
|
197
|
+
// for request-scoped cancellation; other notifications are
|
|
198
|
+
// silenced per JSON-RPC spec (consumer notification handlers
|
|
199
|
+
// are not a feature yet).
|
|
140
200
|
if (!is_jsonrpc_request(json)) {
|
|
141
201
|
if (typeof json === 'object' && json !== null && 'method' in json && !('id' in json)) {
|
|
202
|
+
if (json.method === CANCEL_METHOD) {
|
|
203
|
+
const parsed = CancelNotificationParams.safeParse(json.params);
|
|
204
|
+
if (!parsed.success) {
|
|
205
|
+
log.debug('cancel: invalid params, ignoring', parsed.error.issues);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const controller = pending_controllers.get(parsed.data.request_id);
|
|
209
|
+
if (controller) {
|
|
210
|
+
controller.abort();
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
log.debug('cancel: no pending request for id', parsed.data.request_id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
142
216
|
return;
|
|
143
217
|
}
|
|
144
218
|
ws.send(JSON.stringify(create_jsonrpc_error_response(to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request())));
|
|
@@ -184,22 +258,27 @@ export const register_action_ws = (options) => {
|
|
|
184
258
|
await wait(artificial_delay);
|
|
185
259
|
}
|
|
186
260
|
// Socket-scoped notification — routes to originator only, not
|
|
187
|
-
// broadcast.
|
|
261
|
+
// broadcast. Same helper used in `on_socket_open` so both
|
|
262
|
+
// paths share one code path for send-and-log-on-failure.
|
|
263
|
+
// Future work: other audiences — account-scoped,
|
|
188
264
|
// ACL-filtered, broadcast — likely via a transport-level
|
|
189
265
|
// policy hook.
|
|
190
|
-
const notify = (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
266
|
+
const notify = notify_socket(ws);
|
|
267
|
+
// Per-request controller — fires on explicit `cancel` or on
|
|
268
|
+
// socket close (via the socket_abort_controller chain below).
|
|
269
|
+
// Registered before dispatch so a cancel arriving mid-handler
|
|
270
|
+
// finds it; cleared in `finally` so late cancels for a
|
|
271
|
+
// completed id (or a future request that reuses the id) can't
|
|
272
|
+
// null-abort the wrong handler.
|
|
273
|
+
const request_controller = new AbortController();
|
|
274
|
+
pending_controllers.set(id, request_controller);
|
|
199
275
|
const base = {
|
|
200
276
|
request_id: id,
|
|
277
|
+
// Populated in `onOpen` before any message can dispatch —
|
|
278
|
+
// non-null assertion is safe.
|
|
279
|
+
connection_id: captured_connection_id,
|
|
201
280
|
notify,
|
|
202
|
-
signal: socket_abort_controller.signal,
|
|
281
|
+
signal: AbortSignal.any([socket_abort_controller.signal, request_controller.signal]),
|
|
203
282
|
};
|
|
204
283
|
const ctx = extend_context(base, c);
|
|
205
284
|
try {
|
|
@@ -218,8 +297,12 @@ export const register_action_ws = (options) => {
|
|
|
218
297
|
log.error('handler error:', method, error);
|
|
219
298
|
ws.send(JSON.stringify(create_jsonrpc_error_response_from_thrown(id, error)));
|
|
220
299
|
}
|
|
300
|
+
finally {
|
|
301
|
+
pending_controllers.delete(id);
|
|
302
|
+
}
|
|
221
303
|
},
|
|
222
304
|
onClose: async (event, ws) => {
|
|
305
|
+
stop_heartbeat_timer();
|
|
223
306
|
socket_abort_controller.abort();
|
|
224
307
|
if (on_socket_close && captured_connection_id) {
|
|
225
308
|
try {
|
|
@@ -57,4 +57,14 @@ export interface CreateRpcClientOptions {
|
|
|
57
57
|
* @returns a Proxy that responds to any method name found in the environment's specs
|
|
58
58
|
*/
|
|
59
59
|
export declare const create_rpc_client: (options: CreateRpcClientOptions) => Record<string, (...args: Array<any>) => any>;
|
|
60
|
+
/**
|
|
61
|
+
* Per-call options accepted by every typed Proxy method. `signal` lets the
|
|
62
|
+
* caller cancel an in-flight request (sends the shared `cancel` notification
|
|
63
|
+
* on the WS path, aborts `fetch` on HTTP). `transport_name` overrides the
|
|
64
|
+
* per-method `transport_for_method` selector for this call.
|
|
65
|
+
*/
|
|
66
|
+
export interface RpcClientCallOptions {
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
transport_name?: TransportName;
|
|
69
|
+
}
|
|
60
70
|
//# sourceMappingURL=rpc_client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAGnD;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAC;AAM/E,8EAA8E;AAC9E,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,oBAAoB,CAAA;KAAC,KAC5E;QACA,sBAAsB,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;KAC5C,GACD,SAAS,CAAC;CACb;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,sBAAsB,CAAC;IACpC,kEAAkE;IAClE,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,sBAAsB,KAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAgB7C,CAAC;AA2DF;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B"}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { create_action_event } from './action_event.js';
|
|
13
13
|
import { is_send_request, is_notification_send, extract_action_result, } from './action_event_helpers.js';
|
|
14
|
+
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
14
15
|
/**
|
|
15
16
|
* Creates a Proxy-based API from action specs.
|
|
16
17
|
*
|
|
@@ -78,9 +79,19 @@ const create_sync_local_call_method = (environment, spec, actions) => {
|
|
|
78
79
|
/**
|
|
79
80
|
* Creates an asynchronous local call method.
|
|
80
81
|
* Returns Result for type-safe error handling.
|
|
82
|
+
*
|
|
83
|
+
* Local calls don't traverse a transport, so `transport_name` is ignored and
|
|
84
|
+
* `signal` can only short-circuit before the synchronous handler runs (no
|
|
85
|
+
* cooperative interrupt mid-handler).
|
|
81
86
|
*/
|
|
82
87
|
const create_async_local_call_method = (environment, spec, actions) => {
|
|
83
|
-
return async (input) => {
|
|
88
|
+
return async (input, options) => {
|
|
89
|
+
if (options?.signal?.aborted) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: jsonrpc_error_messages.internal_error(`${spec.method} aborted before execution`),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
84
95
|
const event = create_action_event(environment, spec, input);
|
|
85
96
|
const action = actions?.add_from_json({
|
|
86
97
|
method: spec.method,
|
|
@@ -95,7 +106,7 @@ const create_async_local_call_method = (environment, spec, actions) => {
|
|
|
95
106
|
* Creates a request/response method that communicates over the network.
|
|
96
107
|
*/
|
|
97
108
|
const create_request_response_method = (peer, environment, spec, actions, transport_for_method) => {
|
|
98
|
-
return async (input) => {
|
|
109
|
+
return async (input, options) => {
|
|
99
110
|
const event = create_action_event(environment, spec, input);
|
|
100
111
|
const action = actions?.add_from_json({
|
|
101
112
|
method: spec.method,
|
|
@@ -113,8 +124,10 @@ const create_request_response_method = (peer, environment, spec, actions, transp
|
|
|
113
124
|
if (event.data.step !== 'handled') {
|
|
114
125
|
return extract_action_result(event);
|
|
115
126
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
127
|
+
const response = await peer.send(event.data.request, {
|
|
128
|
+
transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
|
|
129
|
+
signal: options?.signal,
|
|
130
|
+
});
|
|
118
131
|
event.transition('receive_response');
|
|
119
132
|
// TODO @api shouldn't this happen in the peer like the other method calls?
|
|
120
133
|
event.set_response(response);
|
|
@@ -128,7 +141,7 @@ const create_request_response_method = (peer, environment, spec, actions, transp
|
|
|
128
141
|
* Returns Result<{value: void}> for consistency.
|
|
129
142
|
*/
|
|
130
143
|
const create_remote_notification_method = (peer, environment, spec, actions, transport_for_method) => {
|
|
131
|
-
return async (input) => {
|
|
144
|
+
return async (input, options) => {
|
|
132
145
|
const event = create_action_event(environment, spec, input);
|
|
133
146
|
const action = actions?.add_from_json({
|
|
134
147
|
method: spec.method,
|
|
@@ -139,8 +152,10 @@ const create_remote_notification_method = (peer, environment, spec, actions, tra
|
|
|
139
152
|
if (!is_notification_send(event.data))
|
|
140
153
|
throw Error(); // TODO @many maybe make this an assertion helper?
|
|
141
154
|
if (event.data.step === 'handled') {
|
|
142
|
-
const
|
|
143
|
-
|
|
155
|
+
const send_result = await peer.send(event.data.notification, {
|
|
156
|
+
transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
|
|
157
|
+
signal: options?.signal,
|
|
158
|
+
});
|
|
144
159
|
// Check if notification failed to send
|
|
145
160
|
if (send_result !== null) {
|
|
146
161
|
environment.log?.error('notification send failed:', send_result.error);
|
|
@@ -5,15 +5,26 @@
|
|
|
5
5
|
* Drop into any SvelteKit frontend as the underlying connection for
|
|
6
6
|
* `FrontendWebsocketTransport`. Handles auto-reconnect with exponential
|
|
7
7
|
* backoff, respects `WS_CLOSE_SESSION_REVOKED` (no reconnect loop after the
|
|
8
|
-
* server revokes auth),
|
|
8
|
+
* server revokes auth), exposes reactive status for UI indicators, and ships
|
|
9
|
+
* three correctness primitives default-on:
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* - {@link FrontendWebsocketClient.request} — promise-based JSON-RPC with
|
|
12
|
+
* auto-assigned ids and a pending-id map. Intercepts responses on the
|
|
13
|
+
* message path so request/response correlation is transport-level rather
|
|
14
|
+
* than re-invented per consumer.
|
|
15
|
+
* - **Durable queue** — `request()` calls made while disconnected buffer up
|
|
16
|
+
* to {@link DEFAULT_QUEUE_MAX_SIZE} requests and flush on reopen. Overflow
|
|
17
|
+
* rejects with `queue_overflow`. Raw {@link FrontendWebsocketClient.send}
|
|
18
|
+
* is drop-on-disconnect (fire-and-forget notifications want that).
|
|
19
|
+
* - **Activity-aware heartbeat** — idles fire a shared `heartbeat` request;
|
|
20
|
+
* receive-silence past {@link DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT} closes
|
|
21
|
+
* with {@link WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT} and lets auto-reconnect
|
|
22
|
+
* pick back up.
|
|
13
23
|
*
|
|
14
24
|
* @module
|
|
15
25
|
*/
|
|
16
26
|
import type { Logger } from '@fuzdev/fuz_util/log.js';
|
|
27
|
+
import { type JsonrpcRequestId } from '../http/jsonrpc.js';
|
|
17
28
|
import type { WebsocketConnection } from './transports_ws.js';
|
|
18
29
|
/** Default WebSocket close code (normal closure). */
|
|
19
30
|
export declare const DEFAULT_CLOSE_CODE = 1000;
|
|
@@ -23,6 +34,12 @@ export declare const DEFAULT_RECONNECT_DELAY = 1000;
|
|
|
23
34
|
export declare const DEFAULT_RECONNECT_DELAY_MAX = 10000;
|
|
24
35
|
/** Exponential backoff factor: delay = base * factor^(attempt-1). */
|
|
25
36
|
export declare const DEFAULT_BACKOFF_FACTOR = 1.5;
|
|
37
|
+
/** Idle interval before sending a heartbeat (ms). */
|
|
38
|
+
export declare const DEFAULT_HEARTBEAT_INTERVAL = 30000;
|
|
39
|
+
/** Max receive silence before closing with {@link WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT} (ms). */
|
|
40
|
+
export declare const DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT = 60000;
|
|
41
|
+
/** Default bound on buffered requests while disconnected. Overflow rejects. */
|
|
42
|
+
export declare const DEFAULT_QUEUE_MAX_SIZE = 100;
|
|
26
43
|
/**
|
|
27
44
|
* Client-side WebSocket status.
|
|
28
45
|
*
|
|
@@ -44,12 +61,48 @@ export interface FrontendWebsocketReconnectOptions {
|
|
|
44
61
|
/** Exponential backoff factor. Defaults to 1.5. */
|
|
45
62
|
factor?: number;
|
|
46
63
|
}
|
|
64
|
+
export interface FrontendWebsocketHeartbeatOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Idle duration (ms) after which a heartbeat is sent. Reset by any send or
|
|
67
|
+
* receive — chatty clients never emit extras. Defaults to
|
|
68
|
+
* {@link DEFAULT_HEARTBEAT_INTERVAL}.
|
|
69
|
+
*/
|
|
70
|
+
interval?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Receive-silence (ms) after which the client closes the socket with
|
|
73
|
+
* {@link WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT}, letting auto-reconnect kick
|
|
74
|
+
* in. Should be a comfortable multiple of {@link interval}. Defaults to
|
|
75
|
+
* {@link DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT}.
|
|
76
|
+
*/
|
|
77
|
+
receive_timeout?: number;
|
|
78
|
+
}
|
|
79
|
+
export interface FrontendWebsocketQueueOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Maximum number of requests held while the socket is disconnected.
|
|
82
|
+
* Enqueue past this rejects the new call with a `queue_overflow` error.
|
|
83
|
+
* Defaults to {@link DEFAULT_QUEUE_MAX_SIZE}.
|
|
84
|
+
*/
|
|
85
|
+
max_size?: number;
|
|
86
|
+
}
|
|
47
87
|
export interface FrontendWebsocketClientOptions {
|
|
48
88
|
/**
|
|
49
89
|
* Auto-reconnect policy. `false` disables reconnect entirely; `true` or
|
|
50
90
|
* omit for default timing; pass an object to customize.
|
|
51
91
|
*/
|
|
52
92
|
reconnect?: boolean | FrontendWebsocketReconnectOptions | null;
|
|
93
|
+
/**
|
|
94
|
+
* Activity-aware heartbeat. `true` or omit for defaults; `false` disables
|
|
95
|
+
* the timer entirely (only do this if the server side is also running
|
|
96
|
+
* without heartbeat); pass an object to tune `interval` / `receive_timeout`.
|
|
97
|
+
*/
|
|
98
|
+
heartbeat?: boolean | FrontendWebsocketHeartbeatOptions;
|
|
99
|
+
/**
|
|
100
|
+
* Durable queue for {@link FrontendWebsocketClient.request}. `true` or omit
|
|
101
|
+
* for defaults; `false` disables buffering (requests while disconnected
|
|
102
|
+
* reject immediately). Raw {@link FrontendWebsocketClient.send} is never
|
|
103
|
+
* queued — use `request()` for RPC semantics.
|
|
104
|
+
*/
|
|
105
|
+
queue?: boolean | FrontendWebsocketQueueOptions;
|
|
53
106
|
/** Optional logger for diagnostic messages. */
|
|
54
107
|
log?: Logger | null;
|
|
55
108
|
}
|
|
@@ -132,6 +185,37 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
|
|
|
132
185
|
/** Explicit-resource-management hook — supports `using client = new FrontendWebsocketClient(url)`. */
|
|
133
186
|
[Symbol.dispose](): void;
|
|
134
187
|
send(data: object): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Promise-based JSON-RPC over the socket. Auto-assigns a monotonic request
|
|
190
|
+
* id (or uses an explicit one supplied via `options.id` — used by
|
|
191
|
+
* `FrontendWebsocketTransport` which delegates to this method and has its
|
|
192
|
+
* own peer-minted UUID), tracks the pending promise, and resolves when the
|
|
193
|
+
* server sends a matching response (or rejects on error frame, socket
|
|
194
|
+
* close, or aborted signal).
|
|
195
|
+
*
|
|
196
|
+
* Callers supplying an explicit `options.id` are responsible for
|
|
197
|
+
* uniqueness — the pending map is keyed by id, and a duplicate silently
|
|
198
|
+
* overwrites the prior entry. Auto-minted ids are monotonic and never
|
|
199
|
+
* collide with themselves or with peer-minted UUIDs (the types differ:
|
|
200
|
+
* integer vs string).
|
|
201
|
+
*
|
|
202
|
+
* While the socket is disconnected, the request is buffered in a bounded
|
|
203
|
+
* queue (default-on, `DEFAULT_QUEUE_MAX_SIZE`) and flushed on reopen. Pass
|
|
204
|
+
* `{queue: false}` to reject immediately when disconnected — used
|
|
205
|
+
* internally by the heartbeat, which must not fight the queue for the
|
|
206
|
+
* disconnect-detection slot.
|
|
207
|
+
*
|
|
208
|
+
* On `AbortSignal` fire: rejects the local promise *and* sends the shared
|
|
209
|
+
* `cancel` notification (`CANCEL_METHOD`) so the server-side dispatcher
|
|
210
|
+
* can abort the matching handler's `ctx.signal`. Suppressed for
|
|
211
|
+
* queued-but-never-sent (server doesn't know about it) and
|
|
212
|
+
* response-beat-cancel races.
|
|
213
|
+
*/
|
|
214
|
+
request<R = unknown>(method: string, params?: unknown, options?: {
|
|
215
|
+
signal?: AbortSignal;
|
|
216
|
+
queue?: boolean;
|
|
217
|
+
id?: JsonrpcRequestId;
|
|
218
|
+
}): Promise<R>;
|
|
135
219
|
add_message_handler(handler: SocketMessageHandler): () => void;
|
|
136
220
|
add_error_handler(handler: SocketErrorHandler): () => void;
|
|
137
221
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
|