@fuzdev/fuz_app 0.21.0 → 0.22.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/register_action_ws.d.ts +56 -2
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +56 -3
- package/dist/testing/ws_round_trip.d.ts +10 -2
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +20 -6
- package/package.json +1 -1
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
* @module
|
|
23
23
|
*/
|
|
24
24
|
import type { Context, Hono } from 'hono';
|
|
25
|
-
import type { UpgradeWebSocket } from 'hono/ws';
|
|
25
|
+
import type { UpgradeWebSocket, WSContext } from 'hono/ws';
|
|
26
26
|
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.js';
|
|
27
27
|
import { type JsonrpcRequestId } from '../http/jsonrpc.js';
|
|
28
|
+
import type { Uuid } from '../uuid.js';
|
|
28
29
|
import type { ActionSpecUnion } from './action_spec.js';
|
|
29
|
-
import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
30
|
+
import { BackendWebsocketTransport, type ConnectionIdentity } from './transports_ws_backend.js';
|
|
30
31
|
/**
|
|
31
32
|
* Minimum per-request context every handler receives.
|
|
32
33
|
*
|
|
@@ -51,6 +52,45 @@ export interface BaseHandlerContext {
|
|
|
51
52
|
}
|
|
52
53
|
/** Handler signature — receives validated input and per-request context. */
|
|
53
54
|
export type WsActionHandler<TCtx extends BaseHandlerContext> = (input: unknown, ctx: TCtx) => unknown;
|
|
55
|
+
/**
|
|
56
|
+
* Context passed to the `on_socket_open` hook.
|
|
57
|
+
*
|
|
58
|
+
* Fires after the transport has registered the new connection (so
|
|
59
|
+
* `connection_id` is valid) but before any client message can dispatch.
|
|
60
|
+
* Consumers use this to bootstrap per-socket domain state — e.g. undying
|
|
61
|
+
* spawns the per-account spirit unit and pushes an initial state snapshot.
|
|
62
|
+
*/
|
|
63
|
+
export interface SocketOpenContext {
|
|
64
|
+
/** The raw WebSocket context — exposed for edge cases; prefer `notify` for sends. */
|
|
65
|
+
ws: WSContext;
|
|
66
|
+
/** Connection id assigned by `BackendWebsocketTransport.add_connection`. */
|
|
67
|
+
connection_id: Uuid;
|
|
68
|
+
/** Auth identity registered for this connection. */
|
|
69
|
+
identity: ConnectionIdentity;
|
|
70
|
+
/**
|
|
71
|
+
* Send a JSON-RPC notification to just this socket. Mirrors `ctx.notify`
|
|
72
|
+
* on per-message handler contexts — same socket-scoped semantics.
|
|
73
|
+
*/
|
|
74
|
+
notify: (method: string, params: unknown) => void;
|
|
75
|
+
/** Fires when this socket closes — threaded through to every handler's `ctx.signal`. */
|
|
76
|
+
signal: AbortSignal;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Context passed to the `on_socket_close` hook.
|
|
80
|
+
*
|
|
81
|
+
* Fires before `transport.remove_connection` runs, so consumer cleanup can
|
|
82
|
+
* still read identity before it's torn down. Fires for both client-initiated
|
|
83
|
+
* closes (Hono onClose) and server-initiated closes via audit revocation
|
|
84
|
+
* (the audit guard calls `ws.close()`, which triggers Hono's onClose).
|
|
85
|
+
*/
|
|
86
|
+
export interface SocketCloseContext {
|
|
87
|
+
/** The raw WebSocket context at close time. */
|
|
88
|
+
ws: WSContext;
|
|
89
|
+
/** Connection id captured at open time. */
|
|
90
|
+
connection_id: Uuid;
|
|
91
|
+
/** Auth identity captured at open time — still valid even if the transport already cleaned up. */
|
|
92
|
+
identity: ConnectionIdentity;
|
|
93
|
+
}
|
|
54
94
|
/** Options for `register_action_ws`. */
|
|
55
95
|
export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
|
|
56
96
|
/** Mount path (e.g., `/api/ws`). */
|
|
@@ -80,6 +120,20 @@ export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
|
|
|
80
120
|
artificial_delay?: number;
|
|
81
121
|
/** Optional logger; defaults to `[ws]` namespace. */
|
|
82
122
|
log?: LoggerType;
|
|
123
|
+
/**
|
|
124
|
+
* Called once per socket, after the transport registers the connection.
|
|
125
|
+
* Awaited before any message is dispatched. Throwing logs an error and
|
|
126
|
+
* closes the socket with an `internal_error` frame — a failing bootstrap
|
|
127
|
+
* should not leave a partially-initialized socket alive.
|
|
128
|
+
*/
|
|
129
|
+
on_socket_open?: (ctx: SocketOpenContext) => void | Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Called once per socket on close, *before* the transport removes the
|
|
132
|
+
* connection. Receives `connection_id` and `identity` captured at open
|
|
133
|
+
* time, so it is safe to read even when the audit guard has already torn
|
|
134
|
+
* down the transport's internal state. Errors are logged and swallowed.
|
|
135
|
+
*/
|
|
136
|
+
on_socket_close?: (ctx: SocketCloseContext) => void | Promise<void>;
|
|
83
137
|
}
|
|
84
138
|
/** Result of `register_action_ws`. */
|
|
85
139
|
export interface RegisterActionWsResult {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
|
|
@@ -51,7 +51,7 @@ import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
|
51
51
|
* `create_ws_auth_guard` or broadcast on audit events.
|
|
52
52
|
*/
|
|
53
53
|
export const register_action_ws = (options) => {
|
|
54
|
-
const { path, app, upgradeWebSocket, specs, handlers, extend_context, transport = new BackendWebsocketTransport(), artificial_delay = 0, log = new Logger('[ws]'), } = options;
|
|
54
|
+
const { path, app, upgradeWebSocket, specs, handlers, extend_context, transport = new BackendWebsocketTransport(), artificial_delay = 0, log = new Logger('[ws]'), on_socket_open, on_socket_close, } = options;
|
|
55
55
|
// Build spec lookup for per-action auth and input validation.
|
|
56
56
|
const spec_by_method = new Map(specs.map((spec) => [spec.method, spec]));
|
|
57
57
|
app.get(path, upgradeWebSocket((c) => {
|
|
@@ -75,10 +75,51 @@ export const register_action_ws = (options) => {
|
|
|
75
75
|
// a single socket-scoped signal is sufficient today since cancel
|
|
76
76
|
// granularity tracks connection lifetime, not individual requests.
|
|
77
77
|
const socket_abort_controller = new AbortController();
|
|
78
|
+
// Identity is assembled at upgrade time so `on_socket_close` can
|
|
79
|
+
// still read it after the audit guard tears the transport record
|
|
80
|
+
// down; `BackendWebsocketTransport.#revoke_connection` clears the
|
|
81
|
+
// identity map before Hono fires onClose.
|
|
82
|
+
const identity = { token_hash, account_id, api_token_id };
|
|
83
|
+
// Captured on open, consumed on close. Null before onOpen fires or
|
|
84
|
+
// when a consumer never opens (e.g. immediate disconnect).
|
|
85
|
+
let captured_connection_id = null;
|
|
86
|
+
// Socket-scoped notification helper — routes to this socket only,
|
|
87
|
+
// matches the `ctx.notify` semantics exposed to per-message handlers.
|
|
88
|
+
const notify_socket = (ws) => (notify_method, notify_params) => {
|
|
89
|
+
try {
|
|
90
|
+
const notification = create_jsonrpc_notification(notify_method, to_jsonrpc_params(notify_params));
|
|
91
|
+
ws.send(JSON.stringify(notification));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
log.error('notify send failed:', notify_method, error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
78
97
|
return {
|
|
79
|
-
onOpen: (event, ws) => {
|
|
98
|
+
onOpen: async (event, ws) => {
|
|
80
99
|
const connection_id = transport.add_connection(ws, token_hash, account_id, api_token_id);
|
|
100
|
+
captured_connection_id = connection_id;
|
|
81
101
|
log.debug('ws opened', connection_id, event);
|
|
102
|
+
if (on_socket_open) {
|
|
103
|
+
try {
|
|
104
|
+
await on_socket_open({
|
|
105
|
+
ws,
|
|
106
|
+
connection_id,
|
|
107
|
+
identity,
|
|
108
|
+
notify: notify_socket(ws),
|
|
109
|
+
signal: socket_abort_controller.signal,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
log.error('on_socket_open failed — closing socket:', error);
|
|
114
|
+
try {
|
|
115
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.internal_error())));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore — socket may already be dead
|
|
119
|
+
}
|
|
120
|
+
ws.close(1011, 'socket bootstrap failed');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
82
123
|
},
|
|
83
124
|
onMessage: async (event, ws) => {
|
|
84
125
|
let json;
|
|
@@ -178,8 +219,20 @@ export const register_action_ws = (options) => {
|
|
|
178
219
|
ws.send(JSON.stringify(create_jsonrpc_error_response_from_thrown(id, error)));
|
|
179
220
|
}
|
|
180
221
|
},
|
|
181
|
-
onClose: (event, ws) => {
|
|
222
|
+
onClose: async (event, ws) => {
|
|
182
223
|
socket_abort_controller.abort();
|
|
224
|
+
if (on_socket_close && captured_connection_id) {
|
|
225
|
+
try {
|
|
226
|
+
await on_socket_close({
|
|
227
|
+
ws,
|
|
228
|
+
connection_id: captured_connection_id,
|
|
229
|
+
identity,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
log.error('on_socket_close failed:', error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
183
236
|
transport.remove_connection(ws);
|
|
184
237
|
log.debug('ws closed', event);
|
|
185
238
|
},
|
|
@@ -120,8 +120,12 @@ export interface WsConnectIdentity {
|
|
|
120
120
|
export interface MockWsClient {
|
|
121
121
|
/** Send a JSON-RPC message (request or notification) to the server. */
|
|
122
122
|
send: (message: unknown) => Promise<void>;
|
|
123
|
-
/**
|
|
124
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Close the connection, firing `onClose`. Returns a promise that
|
|
125
|
+
* resolves once `on_socket_close` (and the transport's own cleanup)
|
|
126
|
+
* have completed — tests that assert on post-close state should await.
|
|
127
|
+
*/
|
|
128
|
+
close: (code?: number, reason?: string) => Promise<void>;
|
|
125
129
|
/** Every message the server has sent, in arrival order. */
|
|
126
130
|
readonly messages: ReadonlyArray<unknown>;
|
|
127
131
|
/**
|
|
@@ -140,6 +144,10 @@ export interface CreateWsTestHarnessOptions<TCtx extends BaseHandlerContext> {
|
|
|
140
144
|
transport?: BackendWebsocketTransport;
|
|
141
145
|
/** Optional logger. Defaults to a silent `[ws-test]` logger. */
|
|
142
146
|
log?: Logger;
|
|
147
|
+
/** Threaded straight through to `register_action_ws`. */
|
|
148
|
+
on_socket_open?: RegisterActionWsOptions<TCtx>['on_socket_open'];
|
|
149
|
+
/** Threaded straight through to `register_action_ws`. */
|
|
150
|
+
on_socket_close?: RegisterActionWsOptions<TCtx>['on_socket_close'];
|
|
143
151
|
}
|
|
144
152
|
/** A harness instance — transport handle + connection factory. */
|
|
145
153
|
export interface WsTestHarness {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
|
|
@@ -215,7 +215,7 @@ const build_simple_request_context = (role) => {
|
|
|
215
215
|
* `onOpen`/`onMessage`/`onClose` path against a real `WSContext`.
|
|
216
216
|
*/
|
|
217
217
|
export const create_ws_test_harness = (options) => {
|
|
218
|
-
const { specs, handlers, extend_context = (base) => base, transport = new BackendWebsocketTransport(), log = new Logger('[ws-test]', { level: 'off' }), } = options;
|
|
218
|
+
const { specs, handlers, extend_context = (base) => base, transport = new BackendWebsocketTransport(), log = new Logger('[ws-test]', { level: 'off' }), on_socket_open, on_socket_close, } = options;
|
|
219
219
|
const stub = create_stub_upgrade();
|
|
220
220
|
// Minimal Hono stub — `register_action_ws` only needs `.get(path, handler)`.
|
|
221
221
|
const stub_app = { get: () => stub_app };
|
|
@@ -228,6 +228,8 @@ export const create_ws_test_harness = (options) => {
|
|
|
228
228
|
extend_context,
|
|
229
229
|
transport,
|
|
230
230
|
log,
|
|
231
|
+
on_socket_open,
|
|
232
|
+
on_socket_close,
|
|
231
233
|
});
|
|
232
234
|
const events_factory = stub.get_create_events();
|
|
233
235
|
const connect = (identity = {}) => {
|
|
@@ -248,6 +250,9 @@ export const create_ws_test_harness = (options) => {
|
|
|
248
250
|
const received = [];
|
|
249
251
|
const waiters = [];
|
|
250
252
|
let is_closed = false;
|
|
253
|
+
// Captured in `ws.close` below; `client.close(...)` returns it so
|
|
254
|
+
// tests can await async `on_socket_close` cleanup.
|
|
255
|
+
let close_pending = null;
|
|
251
256
|
// Real WSContext backed by test-owned send/close. Parsing is done
|
|
252
257
|
// on receive so tests can assert against structured messages.
|
|
253
258
|
const ws = new WSContext({
|
|
@@ -275,16 +280,24 @@ export const create_ws_test_harness = (options) => {
|
|
|
275
280
|
reason: { value: reason ?? '', writable: false },
|
|
276
281
|
wasClean: { value: true, writable: false },
|
|
277
282
|
});
|
|
278
|
-
|
|
283
|
+
close_pending = Promise.resolve(events).then(async (e) => {
|
|
284
|
+
// onClose is typed as `void` by Hono but `register_action_ws`
|
|
285
|
+
// returns a promise when `on_socket_close` does async cleanup.
|
|
286
|
+
await e.onClose?.(close_event, ws);
|
|
287
|
+
});
|
|
279
288
|
},
|
|
280
289
|
});
|
|
281
290
|
// Resolve the (possibly async) events factory synchronously via
|
|
282
291
|
// a microtask chain. Tests always await `connect().send(...)`
|
|
283
292
|
// which sequences after.
|
|
284
293
|
let events = events_factory(fake_c);
|
|
285
|
-
const events_ready = Promise.resolve(events).then((resolved) => {
|
|
294
|
+
const events_ready = Promise.resolve(events).then(async (resolved) => {
|
|
286
295
|
events = resolved;
|
|
287
|
-
|
|
296
|
+
// onOpen is typed as `void` by Hono but `register_action_ws`
|
|
297
|
+
// returns a promise when `on_socket_open` does async bootstrap.
|
|
298
|
+
// Tests have to see the fully-bootstrapped socket before
|
|
299
|
+
// `send()`, so wait on the hook chain here.
|
|
300
|
+
await resolved.onOpen?.(new Event('open'), ws);
|
|
288
301
|
return resolved;
|
|
289
302
|
});
|
|
290
303
|
return {
|
|
@@ -302,10 +315,11 @@ export const create_ws_test_harness = (options) => {
|
|
|
302
315
|
// handler, send).
|
|
303
316
|
await resolved.onMessage?.(message_event, ws);
|
|
304
317
|
},
|
|
305
|
-
close(code, reason) {
|
|
318
|
+
async close(code, reason) {
|
|
306
319
|
if (is_closed)
|
|
307
|
-
return;
|
|
320
|
+
return close_pending ?? undefined;
|
|
308
321
|
ws.close(code, reason);
|
|
322
|
+
return close_pending ?? undefined;
|
|
309
323
|
},
|
|
310
324
|
wait_for(predicate, timeout_ms = DEFAULT_TIMEOUT_MS) {
|
|
311
325
|
for (const msg of received) {
|