@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.
@@ -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;AAE9C,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAK1E,OAAO,EAAkB,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAY1E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAErE;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,UAAU,EAAE,gBAAgB,CAAC;IAC7B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,4EAA4E;IAC5E,MAAM,EAAE,WAAW,CAAC;CACpB;AAED,4EAA4E;AAC5E,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,kBAAkB,IAAI,CAC9D,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,IAAI,KACL,OAAO,CAAC;AAEb,wCAAwC;AACxC,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,kBAAkB;IACvE,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,yFAAyF;IACzF,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IACtC,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IAChD;;;;;OAKG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,SAAS,kBAAkB,EACjE,SAAS,uBAAuB,CAAC,IAAI,CAAC,KACpC,sBA0NF,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
- /** Close the connection, firing `onClose`. */
124
- close: (code?: number, reason?: string) => void;
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
- void Promise.resolve(events).then((e) => e.onClose?.(close_event, ws));
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
- resolved.onOpen?.(new Event('open'), ws);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",