@fuzdev/fuz_app 0.22.0 → 0.24.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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared type surface for the action system — context, handler, composable Action tuple.
3
+ *
4
+ * These types sit above `action_spec.ts` (pure Zod schemas) and below the
5
+ * dispatchers (`register_action_ws.ts`, `action_rpc.ts`). Extracted so the
6
+ * shared composable fuz_app actions (e.g. `heartbeat_action`) can name them
7
+ * without pulling in server-only modules.
8
+ *
9
+ * @module
10
+ */
11
+ import type { JsonrpcRequestId } from '../http/jsonrpc.js';
12
+ import type { ActionSpecUnion } from './action_spec.js';
13
+ /**
14
+ * Minimum per-request context every server-side WS handler receives.
15
+ *
16
+ * Consumers extend this with domain-specific fields via the dispatcher's
17
+ * `extend_context` option. Mirrors the HTTP-side `ActionContext` and Rust's
18
+ * `Ctx<'a>` shape (`request_id` + `NotifyFn` + `CancellationToken`).
19
+ */
20
+ export interface BaseHandlerContext {
21
+ /** JSON-RPC envelope request id — echoed back on the response. */
22
+ request_id: JsonrpcRequestId;
23
+ /**
24
+ * Send a request-scoped JSON-RPC notification to the originating socket.
25
+ * Not a broadcast — the message only reaches the client whose request
26
+ * triggered this handler.
27
+ */
28
+ notify: (method: string, params: unknown) => void;
29
+ /** Fires on socket close; streaming handlers poll for early termination. */
30
+ signal: AbortSignal;
31
+ }
32
+ /**
33
+ * Handler signature — receives validated input and per-request context.
34
+ *
35
+ * Named to disambiguate from `actions/action_rpc.ts`'s `ActionHandler`
36
+ * (HTTP-side, `ActionContext` + two generic slots). The WS variant is
37
+ * single-slotted on the context and returns `unknown`.
38
+ */
39
+ export type WsActionHandler<TCtx extends BaseHandlerContext = BaseHandlerContext> = (input: unknown, ctx: TCtx) => unknown;
40
+ /**
41
+ * A spec paired with its optional handler — the composable unit passed to
42
+ * {@link register_action_ws} and {@link create_rpc_client}. The server uses
43
+ * both fields; the client reads only {@link spec} (the {@link handler} is
44
+ * ignored, harmless). Shared fuz_app primitives (e.g. `heartbeat_action`)
45
+ * export a complete tuple so consumers spread them into both sides'
46
+ * `actions` array without inventing per-repo ping plumbing.
47
+ *
48
+ * Left open for future fields (`rate_limit`, ACL, middleware hooks) so
49
+ * additions attach to the action itself instead of scattering across
50
+ * parallel arrays.
51
+ */
52
+ export interface Action<TCtx extends BaseHandlerContext = BaseHandlerContext> {
53
+ spec: ActionSpecUnion;
54
+ /** Server-side handler. Ignored by the client. Omit for client-only specs. */
55
+ handler?: WsActionHandler<TCtx>;
56
+ }
57
+ //# sourceMappingURL=action_types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,UAAU,EAAE,gBAAgB,CAAC;IAC7B;;;;OAIG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,4EAA4E;IAC5E,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,kBAAkB,GAAG,kBAAkB,IAAI,CACnF,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,IAAI,KACL,OAAO,CAAC;AAEb;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,MAAM,CAAC,IAAI,SAAS,kBAAkB,GAAG,kBAAkB;IAC3E,IAAI,EAAE,eAAe,CAAC;IACtB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC;CAChC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared type surface for the action system — context, handler, composable Action tuple.
3
+ *
4
+ * These types sit above `action_spec.ts` (pure Zod schemas) and below the
5
+ * dispatchers (`register_action_ws.ts`, `action_rpc.ts`). Extracted so the
6
+ * shared composable fuz_app actions (e.g. `heartbeat_action`) can name them
7
+ * without pulling in server-only modules.
8
+ *
9
+ * @module
10
+ */
11
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared heartbeat action — the first composable fuz_app primitive carrying
3
+ * both a spec and a handler in one tuple. Consumers spread
4
+ * {@link heartbeat_action} into both the server's and the client's `actions`
5
+ * array so disconnect detection works identically across every repo without
6
+ * per-consumer ping plumbing.
7
+ *
8
+ * The client's activity-aware heartbeat timer (in
9
+ * `FrontendWebsocketClient`) issues a `heartbeat` request whenever the
10
+ * connection has been idle for its configured interval; server-side the
11
+ * dispatcher tracks receive time, so incoming heartbeats keep the socket
12
+ * alive without any handler-level state.
13
+ *
14
+ * Nullary input/output today. `{client_ts, server_ts}` fields can be added
15
+ * later if clock-skew telemetry ever matters — the {@link Action} container
16
+ * is open for additions without churning consumer call sites.
17
+ *
18
+ * @module
19
+ */
20
+ import { z } from 'zod';
21
+ import type { Action } from './action_types.js';
22
+ /** Method name on the wire — shared across every fuz_app consumer. */
23
+ export declare const HEARTBEAT_METHOD = "heartbeat";
24
+ /**
25
+ * `ActionSpec` for the shared heartbeat. `authenticated` auth — upgrade-time
26
+ * auth has already admitted the socket; heartbeats don't need role gating.
27
+ * `side_effects: false` keeps it orthogonal to state changes.
28
+ */
29
+ export declare const heartbeat_action_spec: {
30
+ method: string;
31
+ initiator: "both" | "frontend" | "backend";
32
+ side_effects: boolean;
33
+ input: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
34
+ output: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
35
+ description: string;
36
+ kind: "request_response";
37
+ auth: "authenticated" | "keeper" | "public" | {
38
+ role: string;
39
+ };
40
+ async: true;
41
+ streams?: string | undefined;
42
+ };
43
+ /** Handler — nullary echo. Stateless, suitable for high-frequency pings. */
44
+ export declare const heartbeat_handler: () => Record<string, never>;
45
+ /**
46
+ * Composable tuple — spread into the server's `actions` array for dispatch
47
+ * and into the client's `actions` array so `create_rpc_client` types
48
+ * `app.api.heartbeat()` against the shared spec.
49
+ */
50
+ export declare const heartbeat_action: Action;
51
+ //# sourceMappingURL=heartbeat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAE9C,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAUhC,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,QAAO,MAAM,CAAC,MAAM,EAAE,KAAK,CAAS,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAG9B,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared heartbeat action — the first composable fuz_app primitive carrying
3
+ * both a spec and a handler in one tuple. Consumers spread
4
+ * {@link heartbeat_action} into both the server's and the client's `actions`
5
+ * array so disconnect detection works identically across every repo without
6
+ * per-consumer ping plumbing.
7
+ *
8
+ * The client's activity-aware heartbeat timer (in
9
+ * `FrontendWebsocketClient`) issues a `heartbeat` request whenever the
10
+ * connection has been idle for its configured interval; server-side the
11
+ * dispatcher tracks receive time, so incoming heartbeats keep the socket
12
+ * alive without any handler-level state.
13
+ *
14
+ * Nullary input/output today. `{client_ts, server_ts}` fields can be added
15
+ * later if clock-skew telemetry ever matters — the {@link Action} container
16
+ * is open for additions without churning consumer call sites.
17
+ *
18
+ * @module
19
+ */
20
+ import { z } from 'zod';
21
+ import { RequestResponseActionSpec } from './action_spec.js';
22
+ /** Method name on the wire — shared across every fuz_app consumer. */
23
+ export const HEARTBEAT_METHOD = 'heartbeat';
24
+ /**
25
+ * `ActionSpec` for the shared heartbeat. `authenticated` auth — upgrade-time
26
+ * auth has already admitted the socket; heartbeats don't need role gating.
27
+ * `side_effects: false` keeps it orthogonal to state changes.
28
+ */
29
+ export const heartbeat_action_spec = RequestResponseActionSpec.parse({
30
+ method: HEARTBEAT_METHOD,
31
+ kind: 'request_response',
32
+ initiator: 'frontend',
33
+ auth: 'authenticated',
34
+ side_effects: false,
35
+ input: z.strictObject({}),
36
+ output: z.strictObject({}),
37
+ async: true,
38
+ description: 'Shared activity ping — keeps the socket alive and exercises the dispatch path.',
39
+ });
40
+ /** Handler — nullary echo. Stateless, suitable for high-frequency pings. */
41
+ export const heartbeat_handler = () => ({});
42
+ /**
43
+ * Composable tuple — spread into the server's `actions` array for dispatch
44
+ * and into the client's `actions` array so `create_rpc_client` types
45
+ * `app.api.heartbeat()` against the shared spec.
46
+ */
47
+ export const heartbeat_action = {
48
+ spec: heartbeat_action_spec,
49
+ handler: heartbeat_handler,
50
+ };
@@ -24,34 +24,12 @@
24
24
  import type { Context, Hono } from 'hono';
25
25
  import type { UpgradeWebSocket, WSContext } from 'hono/ws';
26
26
  import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.js';
27
- import { type JsonrpcRequestId } from '../http/jsonrpc.js';
28
27
  import type { Uuid } from '../uuid.js';
29
- import type { ActionSpecUnion } from './action_spec.js';
28
+ import { type Action, type BaseHandlerContext, type WsActionHandler } from './action_types.js';
30
29
  import { BackendWebsocketTransport, type ConnectionIdentity } from './transports_ws_backend.js';
31
- /**
32
- * Minimum per-request context every handler receives.
33
- *
34
- * Consumers extend this with domain-specific fields via
35
- * `RegisterActionWsOptions.extend_context` (e.g., a `backend` singleton
36
- * or the authenticated `RequestContext`). Keeping the base minimal matches
37
- * the HTTP-side `ActionContext` (from `actions/action_rpc.ts`) and mirrors
38
- * Rust's `Ctx<'a>` shape (`request_id` + `NotifyFn` + `CancellationToken`).
39
- */
40
- export interface BaseHandlerContext {
41
- /** JSON-RPC envelope request id — echoed back on the response. */
42
- request_id: JsonrpcRequestId;
43
- /**
44
- * Send a request-scoped JSON-RPC notification to the originating socket.
45
- * Not a broadcast — the message only reaches the client whose request
46
- * triggered this handler. Streaming handlers (e.g. `completion_progress`)
47
- * route chunks through this.
48
- */
49
- notify: (method: string, params: unknown) => void;
50
- /** Fires on socket close; streaming handlers poll for early termination. */
51
- signal: AbortSignal;
52
- }
53
- /** Handler signature — receives validated input and per-request context. */
54
- export type WsActionHandler<TCtx extends BaseHandlerContext> = (input: unknown, ctx: TCtx) => unknown;
30
+ export type { Action, BaseHandlerContext, WsActionHandler };
31
+ /** Default inactivity window before the server closes a silent socket. */
32
+ export declare const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60000;
55
33
  /**
56
34
  * Context passed to the `on_socket_open` hook.
57
35
  *
@@ -91,6 +69,15 @@ export interface SocketCloseContext {
91
69
  /** Auth identity captured at open time — still valid even if the transport already cleaned up. */
92
70
  identity: ConnectionIdentity;
93
71
  }
72
+ export interface ServerHeartbeatOptions {
73
+ /**
74
+ * Receive-silence (ms) past which the server closes the socket with
75
+ * {@link WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT}. Any incoming message resets
76
+ * the counter — chatty clients never trip it. First {@link timeout}
77
+ * window after socket open is exempt (cold-start grace).
78
+ */
79
+ timeout?: number;
80
+ }
94
81
  /** Options for `register_action_ws`. */
95
82
  export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
96
83
  /** Mount path (e.g., `/api/ws`). */
@@ -99,10 +86,14 @@ export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
99
86
  app: Hono;
100
87
  /** Hono's `upgradeWebSocket` helper from the runtime adapter. */
101
88
  upgradeWebSocket: UpgradeWebSocket;
102
- /** Action specs — drives method lookup, per-action auth, and input/output validation. */
103
- specs: ReadonlyArray<ActionSpecUnion>;
104
- /** Handler map keyed by `spec.method`. */
105
- handlers: Record<string, WsActionHandler<TCtx>>;
89
+ /**
90
+ * The actions registered on this endpoint — each carries a spec (drives
91
+ * method lookup, per-action auth, input/output validation) and an
92
+ * optional handler (omit for client-only specs like inbound
93
+ * notifications). Include the shared {@link heartbeat_action} here to
94
+ * complete the disconnect-detection pairing with the frontend client.
95
+ */
96
+ actions: ReadonlyArray<Action<TCtx>>;
106
97
  /**
107
98
  * Build the per-request context from the base and the upgrade-time Hono
108
99
  * context. Called once per incoming message. Consumers use this to attach
@@ -116,6 +107,13 @@ export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
116
107
  * handle for `create_ws_auth_guard` and `send_to`/`broadcast`.
117
108
  */
118
109
  transport?: BackendWebsocketTransport;
110
+ /**
111
+ * Server-side heartbeat policy. Default-on (receive-silence detection,
112
+ * 60s timeout). `false` disables the timer entirely — only do this if
113
+ * the upstream stack (TCP keepalive, Cloudflare idle timeout, etc.)
114
+ * already owns disconnect detection. Pass an object to tune the timeout.
115
+ */
116
+ heartbeat?: boolean | ServerHeartbeatOptions;
119
117
  /** Optional per-message delay for testing loading states. Ignored when `0`. */
120
118
  artificial_delay?: number;
121
119
  /** Optional logger; defaults to `[ws]` namespace. */
@@ -1 +1 @@
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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"}
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,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAgB1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AAErC,OAAO,EAAC,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAE7F,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAE9F,YAAY,EAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,EAAC,CAAC;AAE1D,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,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;;;;;;OAMG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC;;;;;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;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;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,sBA0UF,CAAC"}
@@ -31,7 +31,11 @@ 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 { WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT } from './transports.js';
34
36
  import { BackendWebsocketTransport } from './transports_ws_backend.js';
37
+ /** Default inactivity window before the server closes a silent socket. */
38
+ export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
35
39
  /**
36
40
  * Mount a JSON-RPC WebSocket endpoint that dispatches to the supplied handler
37
41
  * map. Per-request context is built from the base + consumer-provided
@@ -51,9 +55,24 @@ import { BackendWebsocketTransport } from './transports_ws_backend.js';
51
55
  * `create_ws_auth_guard` or broadcast on audit events.
52
56
  */
53
57
  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]'), on_socket_open, on_socket_close, } = options;
55
- // Build spec lookup for per-action auth and input validation.
56
- const spec_by_method = new Map(specs.map((spec) => [spec.method, spec]));
58
+ 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;
59
+ // Fan the unified actions array into the two lookups the dispatcher
60
+ // consults at message time. Keeping them internal means the composable
61
+ // `{spec, handler}` tuple remains the only shape consumers name.
62
+ const spec_by_method = new Map();
63
+ const handlers = {};
64
+ for (const action of actions) {
65
+ spec_by_method.set(action.spec.method, action.spec);
66
+ if (action.handler)
67
+ handlers[action.spec.method] = action.handler;
68
+ }
69
+ const heartbeat_enabled = heartbeat !== false;
70
+ const heartbeat_config = typeof heartbeat === 'object' ? heartbeat : {};
71
+ const heartbeat_timeout = heartbeat_config.timeout ?? DEFAULT_SERVER_HEARTBEAT_TIMEOUT;
72
+ // Run the checker on timeout/2 so event-loop blockage pauses the timer
73
+ // itself — a dead-because-blocked socket is close enough to
74
+ // dead-because-unresponsive that closing is arguably correct.
75
+ const heartbeat_tick_interval = Math.max(100, Math.floor(heartbeat_timeout / 2));
57
76
  app.get(path, upgradeWebSocket((c) => {
58
77
  // Upgrade-time auth extraction — `require_auth` middleware has already
59
78
  // rejected unauthenticated requests, so request_context is guaranteed
@@ -83,6 +102,18 @@ export const register_action_ws = (options) => {
83
102
  // Captured on open, consumed on close. Null before onOpen fires or
84
103
  // when a consumer never opens (e.g. immediate disconnect).
85
104
  let captured_connection_id = null;
105
+ // Receive-silence watchdog. Seeded to open-time so the first window is
106
+ // exempt (cold-start grace — avoid killing mid-handshake sockets).
107
+ // Bumped by onMessage. Any incoming activity counts, not just
108
+ // heartbeats — chatty clients don't need to send extras.
109
+ let last_receive_time = 0;
110
+ let heartbeat_timer = null;
111
+ const stop_heartbeat_timer = () => {
112
+ if (heartbeat_timer !== null) {
113
+ clearInterval(heartbeat_timer);
114
+ heartbeat_timer = null;
115
+ }
116
+ };
86
117
  // Socket-scoped notification helper — routes to this socket only,
87
118
  // matches the `ctx.notify` semantics exposed to per-message handlers.
88
119
  const notify_socket = (ws) => (notify_method, notify_params) => {
@@ -99,6 +130,23 @@ export const register_action_ws = (options) => {
99
130
  const connection_id = transport.add_connection(ws, token_hash, account_id, api_token_id);
100
131
  captured_connection_id = connection_id;
101
132
  log.debug('ws opened', connection_id, event);
133
+ if (heartbeat_enabled) {
134
+ last_receive_time = Date.now();
135
+ heartbeat_timer = setInterval(() => {
136
+ const now = Date.now();
137
+ const silence = now - last_receive_time;
138
+ if (silence >= heartbeat_timeout) {
139
+ log.info(`heartbeat timeout (${silence}ms) — closing ${WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT}`, connection_id, identity.account_id);
140
+ stop_heartbeat_timer();
141
+ try {
142
+ ws.close(WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT, 'server heartbeat timeout');
143
+ }
144
+ catch (error) {
145
+ log.error('heartbeat timeout close failed:', error);
146
+ }
147
+ }
148
+ }, heartbeat_tick_interval);
149
+ }
102
150
  if (on_socket_open) {
103
151
  try {
104
152
  await on_socket_open({
@@ -122,6 +170,7 @@ export const register_action_ws = (options) => {
122
170
  }
123
171
  },
124
172
  onMessage: async (event, ws) => {
173
+ last_receive_time = Date.now();
125
174
  let json;
126
175
  try {
127
176
  json = JSON.parse(String(event.data)); // eslint-disable-line @typescript-eslint/no-base-to-string
@@ -220,6 +269,7 @@ export const register_action_ws = (options) => {
220
269
  }
221
270
  },
222
271
  onClose: async (event, ws) => {
272
+ stop_heartbeat_timer();
223
273
  socket_abort_controller.abort();
224
274
  if (on_socket_close && captured_connection_id) {
225
275
  try {
@@ -5,11 +5,21 @@
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), and exposes reactive status for UI indicators.
8
+ * server revokes auth), exposes reactive status for UI indicators, and ships
9
+ * three correctness primitives default-on:
9
10
  *
10
- * First cut: no message queue, no heartbeat. Those live in consumer-specific
11
- * wrappers today (see zzz's `Socket` Cell); extract into fuz_app when two
12
- * independent consumers motivate the shape.
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
  */
@@ -23,6 +33,12 @@ export declare const DEFAULT_RECONNECT_DELAY = 1000;
23
33
  export declare const DEFAULT_RECONNECT_DELAY_MAX = 10000;
24
34
  /** Exponential backoff factor: delay = base * factor^(attempt-1). */
25
35
  export declare const DEFAULT_BACKOFF_FACTOR = 1.5;
36
+ /** Idle interval before sending a heartbeat (ms). */
37
+ export declare const DEFAULT_HEARTBEAT_INTERVAL = 30000;
38
+ /** Max receive silence before closing with {@link WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT} (ms). */
39
+ export declare const DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT = 60000;
40
+ /** Default bound on buffered requests while disconnected. Overflow rejects. */
41
+ export declare const DEFAULT_QUEUE_MAX_SIZE = 100;
26
42
  /**
27
43
  * Client-side WebSocket status.
28
44
  *
@@ -44,12 +60,48 @@ export interface FrontendWebsocketReconnectOptions {
44
60
  /** Exponential backoff factor. Defaults to 1.5. */
45
61
  factor?: number;
46
62
  }
63
+ export interface FrontendWebsocketHeartbeatOptions {
64
+ /**
65
+ * Idle duration (ms) after which a heartbeat is sent. Reset by any send or
66
+ * receive — chatty clients never emit extras. Defaults to
67
+ * {@link DEFAULT_HEARTBEAT_INTERVAL}.
68
+ */
69
+ interval?: number;
70
+ /**
71
+ * Receive-silence (ms) after which the client closes the socket with
72
+ * {@link WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT}, letting auto-reconnect kick
73
+ * in. Should be a comfortable multiple of {@link interval}. Defaults to
74
+ * {@link DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT}.
75
+ */
76
+ receive_timeout?: number;
77
+ }
78
+ export interface FrontendWebsocketQueueOptions {
79
+ /**
80
+ * Maximum number of requests held while the socket is disconnected.
81
+ * Enqueue past this rejects the new call with a `queue_overflow` error.
82
+ * Defaults to {@link DEFAULT_QUEUE_MAX_SIZE}.
83
+ */
84
+ max_size?: number;
85
+ }
47
86
  export interface FrontendWebsocketClientOptions {
48
87
  /**
49
88
  * Auto-reconnect policy. `false` disables reconnect entirely; `true` or
50
89
  * omit for default timing; pass an object to customize.
51
90
  */
52
91
  reconnect?: boolean | FrontendWebsocketReconnectOptions | null;
92
+ /**
93
+ * Activity-aware heartbeat. `true` or omit for defaults; `false` disables
94
+ * the timer entirely (only do this if the server side is also running
95
+ * without heartbeat); pass an object to tune `interval` / `receive_timeout`.
96
+ */
97
+ heartbeat?: boolean | FrontendWebsocketHeartbeatOptions;
98
+ /**
99
+ * Durable queue for {@link FrontendWebsocketClient.request}. `true` or omit
100
+ * for defaults; `false` disables buffering (requests while disconnected
101
+ * reject immediately). Raw {@link FrontendWebsocketClient.send} is never
102
+ * queued — use `request()` for RPC semantics.
103
+ */
104
+ queue?: boolean | FrontendWebsocketQueueOptions;
53
105
  /** Optional logger for diagnostic messages. */
54
106
  log?: Logger | null;
55
107
  }
@@ -132,6 +184,26 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
132
184
  /** Explicit-resource-management hook — supports `using client = new FrontendWebsocketClient(url)`. */
133
185
  [Symbol.dispose](): void;
134
186
  send(data: object): boolean;
187
+ /**
188
+ * Promise-based JSON-RPC over the socket. Auto-assigns a monotonic request
189
+ * id, tracks the pending promise, and resolves when the server sends a
190
+ * matching response (or rejects on error frame, socket close, or aborted
191
+ * signal).
192
+ *
193
+ * While the socket is disconnected, the request is buffered in a bounded
194
+ * queue (default-on, {@link DEFAULT_QUEUE_MAX_SIZE}) and flushed on
195
+ * reopen. Pass `{queue: false}` to reject immediately when disconnected
196
+ * — used internally by the heartbeat, which must not fight the queue for
197
+ * the disconnect-detection slot.
198
+ *
199
+ * `AbortSignal` integration today rejects the local promise; the
200
+ * server-side cancel protocol (sending a `cancel` notification to abort
201
+ * the in-flight handler) lands in Phase 3c as a follow-up PR.
202
+ */
203
+ request<R = unknown>(method: string, params?: unknown, options?: {
204
+ signal?: AbortSignal;
205
+ queue?: boolean;
206
+ }): Promise<R>;
135
207
  add_message_handler(handler: SocketMessageHandler): () => void;
136
208
  add_error_handler(handler: SocketErrorHandler): () => void;
137
209
  }
@@ -1 +1 @@
1
- {"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAE5D,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,OAAO,CAAC;AACvC,kCAAkC;AAClC,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAC5C,8DAA8D;AAC9D,eAAO,MAAM,2BAA2B,QAAQ,CAAC;AACjD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE9F,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;AACjE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAExD,MAAM,WAAW,iCAAiC;IACjD,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,8BAA8B;IAC9C;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,iCAAiC,GAAG,IAAI,CAAC;IAC/D,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,uBAAwB,YAAW,mBAAmB,EAAE,UAAU;;IAQ9E,EAAE,EAAE,SAAS,GAAG,IAAI,CAAoB;IACxC,MAAM,EAAE,YAAY,CAAyB;IAE7C,eAAe,EAAE,MAAM,CAAiB;IACxC,uBAAuB,EAAE,MAAM,CAAiB;IAChD,2EAA2E;IAC3E,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAoB;IACpD,yEAAyE;IACzE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAoB;IAClD,kFAAkF;IAClF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAoB;IAClD,qEAAqE;IACrE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAoB;IACpD;;;;;;;;OAQG;IACH,eAAe,EAAE,KAAK,GAAG,IAAI,CAAoB;IASjD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAyC;gBAExD,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,8BAAmC;IAWrE;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAAa,CAAC,SAAS,GAAE,OAAO,GAAG,iCAAiC,GAAG,IAAW,GAAG,IAAI;IA4CzF,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED;;;;OAIG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,OAAO,IAAI,IAAI;IA2Bf;;;;OAIG;IACH,UAAU,CAAC,IAAI,GAAE,MAA2B,GAAG,IAAI;IAQnD,sGAAsG;IACtG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAIxB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAa3B,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,IAAI;IAK9D,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,IAAI;CAiH1D"}
1
+ {"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}