@fuzdev/fuz_app 0.26.0 → 0.28.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.
@@ -1,5 +1,11 @@
1
1
  /**
2
- * WebSocket JSON-RPC dispatch — the canonical WS transport binding.
2
+ * WebSocket JSON-RPC dispatch — the low-level WS transport binding.
3
+ *
4
+ * Most consumers should mount WS endpoints via `register_ws_endpoint`
5
+ * (`./register_ws_endpoint.js`), which wraps this function with the standard
6
+ * upgrade stack (origin check + auth + optional role). This module stays
7
+ * exported as the lower-level entry point for tests that drive the
8
+ * dispatcher directly via `create_ws_test_harness`.
3
9
  *
4
10
  * Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
5
11
  * consumer supplies action specs + a handler map, the dispatcher parses the
@@ -15,9 +21,10 @@
15
21
  * ## Auth expectations
16
22
  *
17
23
  * The consumer is responsible for rejecting unauthenticated upgrades *before*
18
- * routing to this handler (fuz_app's `require_auth` middleware). Inside the
19
- * dispatcher, `get_request_context(c)` is treated as guaranteed non-null and
20
- * per-action auth is enforced on each message.
24
+ * routing to this handler (fuz_app's `require_auth` middleware, or
25
+ * `register_ws_endpoint` which wires it for you). Inside the dispatcher,
26
+ * `get_request_context(c)` is treated as guaranteed non-null and per-action
27
+ * auth is enforced on each message.
21
28
  *
22
29
  * @module
23
30
  */
@@ -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,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;AAG7F,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,sBA8WF,CAAC"}
1
+ {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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;AAG7F,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,sBA8WF,CAAC"}
@@ -1,5 +1,11 @@
1
1
  /**
2
- * WebSocket JSON-RPC dispatch — the canonical WS transport binding.
2
+ * WebSocket JSON-RPC dispatch — the low-level WS transport binding.
3
+ *
4
+ * Most consumers should mount WS endpoints via `register_ws_endpoint`
5
+ * (`./register_ws_endpoint.js`), which wraps this function with the standard
6
+ * upgrade stack (origin check + auth + optional role). This module stays
7
+ * exported as the lower-level entry point for tests that drive the
8
+ * dispatcher directly via `create_ws_test_harness`.
3
9
  *
4
10
  * Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
5
11
  * consumer supplies action specs + a handler map, the dispatcher parses the
@@ -15,9 +21,10 @@
15
21
  * ## Auth expectations
16
22
  *
17
23
  * The consumer is responsible for rejecting unauthenticated upgrades *before*
18
- * routing to this handler (fuz_app's `require_auth` middleware). Inside the
19
- * dispatcher, `get_request_context(c)` is treated as guaranteed non-null and
20
- * per-action auth is enforced on each message.
24
+ * routing to this handler (fuz_app's `require_auth` middleware, or
25
+ * `register_ws_endpoint` which wires it for you). Inside the dispatcher,
26
+ * `get_request_context(c)` is treated as guaranteed non-null and per-action
27
+ * auth is enforced on each message.
21
28
  *
22
29
  * @module
23
30
  */
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Composed WebSocket endpoint registration — the idiomatic consumer entry
3
+ * point for mounting a fuz_app WS endpoint.
4
+ *
5
+ * Wraps the standard upgrade stack every consumer writes by hand:
6
+ *
7
+ * 1. `verify_request_source(allowed_origins)` — reject disallowed origins
8
+ * before the upgrade handshake runs.
9
+ * 2. `require_auth` — reject unauthenticated upgrades.
10
+ * 3. Optional `require_role(required_role)` — for endpoints gated to a
11
+ * specific role.
12
+ *
13
+ * Then delegates to {@link register_action_ws} for per-message JSON-RPC
14
+ * dispatch.
15
+ *
16
+ * @module
17
+ */
18
+ import type { RoleName } from '../auth/role_schema.js';
19
+ import { type RegisterActionWsOptions, type RegisterActionWsResult } from './register_action_ws.js';
20
+ import type { BaseHandlerContext } from './action_types.js';
21
+ /** Options for {@link register_ws_endpoint}. */
22
+ export interface RegisterWsEndpointOptions<TCtx extends BaseHandlerContext> extends RegisterActionWsOptions<TCtx> {
23
+ /**
24
+ * Origin allowlist regexes — typically parsed from the `ALLOWED_ORIGINS`
25
+ * env var via `parse_allowed_origins`. Passed straight to
26
+ * `verify_request_source`.
27
+ */
28
+ allowed_origins: Array<RegExp>;
29
+ /**
30
+ * Role required to upgrade. Omit for any authenticated account (`require_auth`
31
+ * alone); set to e.g. `ROLE_ADMIN` to gate the endpoint behind a role. The
32
+ * per-action `auth` in each spec still applies at dispatch time — this is
33
+ * a coarse upgrade-time gate.
34
+ */
35
+ required_role?: RoleName;
36
+ }
37
+ /**
38
+ * Mount a WebSocket endpoint with the standard upgrade stack (origin check
39
+ * + auth + optional role) and JSON-RPC dispatch.
40
+ *
41
+ * Returns the {@link BackendWebsocketTransport} (supplied or freshly
42
+ * created), same as {@link register_action_ws} — retain it to wire
43
+ * `create_ws_auth_guard` on `on_audit_event` or to broadcast.
44
+ */
45
+ export declare const register_ws_endpoint: <TCtx extends BaseHandlerContext>(options: RegisterWsEndpointOptions<TCtx>) => RegisterActionWsResult;
46
+ //# sourceMappingURL=register_ws_endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAE1D,gDAAgD;AAChD,MAAM,WAAW,yBAAyB,CACzC,IAAI,SAAS,kBAAkB,CAC9B,SAAQ,uBAAuB,CAAC,IAAI,CAAC;IACtC;;;;OAIG;IACH,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,IAAI,SAAS,kBAAkB,EACnE,SAAS,yBAAyB,CAAC,IAAI,CAAC,KACtC,sBAUF,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Composed WebSocket endpoint registration — the idiomatic consumer entry
3
+ * point for mounting a fuz_app WS endpoint.
4
+ *
5
+ * Wraps the standard upgrade stack every consumer writes by hand:
6
+ *
7
+ * 1. `verify_request_source(allowed_origins)` — reject disallowed origins
8
+ * before the upgrade handshake runs.
9
+ * 2. `require_auth` — reject unauthenticated upgrades.
10
+ * 3. Optional `require_role(required_role)` — for endpoints gated to a
11
+ * specific role.
12
+ *
13
+ * Then delegates to {@link register_action_ws} for per-message JSON-RPC
14
+ * dispatch.
15
+ *
16
+ * @module
17
+ */
18
+ import { Logger } from '@fuzdev/fuz_util/log.js';
19
+ import { require_auth, require_role } from '../auth/request_context.js';
20
+ import { verify_request_source } from '../http/origin.js';
21
+ import { register_action_ws, } from './register_action_ws.js';
22
+ /**
23
+ * Mount a WebSocket endpoint with the standard upgrade stack (origin check
24
+ * + auth + optional role) and JSON-RPC dispatch.
25
+ *
26
+ * Returns the {@link BackendWebsocketTransport} (supplied or freshly
27
+ * created), same as {@link register_action_ws} — retain it to wire
28
+ * `create_ws_auth_guard` on `on_audit_event` or to broadcast.
29
+ */
30
+ export const register_ws_endpoint = (options) => {
31
+ const { app, path, allowed_origins, required_role, log = new Logger('[ws]'), ...rest } = options;
32
+ app.use(path, verify_request_source(allowed_origins));
33
+ app.use(path, require_auth);
34
+ if (required_role !== undefined) {
35
+ app.use(path, require_role(required_role));
36
+ }
37
+ return register_action_ws({ app, path, log, ...rest });
38
+ };
@@ -24,6 +24,7 @@
24
24
  * @module
25
25
  */
26
26
  import type { Logger } from '@fuzdev/fuz_util/log.js';
27
+ import type { AsyncStatus } from '@fuzdev/fuz_util/async.js';
27
28
  import { type JsonrpcRequestId } from '../http/jsonrpc.js';
28
29
  import type { WebsocketConnection } from './transports_ws.js';
29
30
  /** Default WebSocket close code (normal closure). */
@@ -91,11 +92,11 @@ export interface FrontendWebsocketClientOptions {
91
92
  */
92
93
  reconnect?: boolean | FrontendWebsocketReconnectOptions | null;
93
94
  /**
94
- * Activity-aware heartbeat. `true` or omit for defaults; `false` disables
95
+ * Activity-aware heartbeat. `true`/`null`/omit for defaults; `false` disables
95
96
  * the timer entirely (only do this if the server side is also running
96
97
  * without heartbeat); pass an object to tune `interval` / `receive_timeout`.
97
98
  */
98
- heartbeat?: boolean | FrontendWebsocketHeartbeatOptions;
99
+ heartbeat?: boolean | FrontendWebsocketHeartbeatOptions | null;
99
100
  /**
100
101
  * Durable queue for {@link FrontendWebsocketClient.request}. `true` or omit
101
102
  * for defaults; `false` disables buffering (requests while disconnected
@@ -163,6 +164,33 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
163
164
  * does not synthesize a reconnect — wait for the next close.
164
165
  */
165
166
  set_reconnect(reconnect?: boolean | FrontendWebsocketReconnectOptions | null): void;
167
+ /**
168
+ * Swap the heartbeat policy in place. Accepts the same shape as the
169
+ * constructor's `heartbeat` option: `false` disables the timer, `true` or
170
+ * `null`/omitted restores the defaults, or a config object customizes
171
+ * specific fields (missing fields fall back to defaults, not "keep
172
+ * current" — each call defines the whole policy atomically, same as the
173
+ * constructor and {@link set_reconnect}).
174
+ *
175
+ * When connected, the live timer is restarted immediately so the new
176
+ * `interval` / `receive_timeout` take effect without a reconnect; when
177
+ * disconnected, just stashes the policy for the next open.
178
+ */
179
+ set_heartbeat(heartbeat?: boolean | FrontendWebsocketHeartbeatOptions | null): void;
180
+ /**
181
+ * Cancel a scheduled reconnect without closing the client or disabling
182
+ * auto-reconnect. Transitions status from `reconnecting` → `closed` and
183
+ * resets the backoff counters — the next close still triggers a fresh
184
+ * reconnect cycle under the current policy. No-op when no reconnect is
185
+ * pending.
186
+ *
187
+ * Use this when UI state asks "stop trying for now" without the finality
188
+ * of {@link disconnect} (which also rejects pending/queued requests and
189
+ * clears heartbeat) or the policy change of `set_reconnect(false)`
190
+ * (which disables future reconnects). The queue stays intact so that
191
+ * calling {@link connect} later flushes buffered work.
192
+ */
193
+ cancel_reconnect(): void;
166
194
  get url(): string;
167
195
  /**
168
196
  * Whether the server has permanently closed the session. Once `true`, all
@@ -219,4 +247,17 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
219
247
  add_message_handler(handler: SocketMessageHandler): () => void;
220
248
  add_error_handler(handler: SocketErrorHandler): () => void;
221
249
  }
250
+ /**
251
+ * Project {@link SocketStatus} onto fuz_util's {@link AsyncStatus} — the
252
+ * 5-way → 4-way mapping every consumer re-derives to surface connection state
253
+ * to UI (loading indicators, retry banners). Collapses `reconnecting` into
254
+ * `failure` (UI shows "lost, retrying") and splits `closed` by `revoked` so
255
+ * a terminal session-revocation read as `failure` while a clean client-
256
+ * initiated close reads as `initial` (the "not connected, not trying" state).
257
+ *
258
+ * @param status - the socket's current {@link SocketStatus}
259
+ * @param revoked - whether the session has been permanently revoked
260
+ * (typically `FrontendWebsocketClient.revoked`)
261
+ */
262
+ export declare const socket_status_to_async_status: (status: SocketStatus, revoked: boolean) => AsyncStatus;
222
263
  //# sourceMappingURL=socket.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
@@ -111,7 +111,7 @@ export class FrontendWebsocketClient {
111
111
  this.#backoff_factor = config.factor ?? DEFAULT_BACKOFF_FACTOR;
112
112
  const heartbeat = options.heartbeat;
113
113
  this.#heartbeat_enabled = heartbeat !== false;
114
- const heartbeat_config = typeof heartbeat === 'object' ? heartbeat : {};
114
+ const heartbeat_config = typeof heartbeat === 'object' && heartbeat !== null ? heartbeat : {};
115
115
  this.#heartbeat_interval = heartbeat_config.interval ?? DEFAULT_HEARTBEAT_INTERVAL;
116
116
  this.#heartbeat_receive_timeout =
117
117
  heartbeat_config.receive_timeout ?? DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT;
@@ -152,8 +152,7 @@ export class FrontendWebsocketClient {
152
152
  if (!next_auto) {
153
153
  this.#cancel_reconnect();
154
154
  this.status = 'closed';
155
- this.reconnect_count = 0;
156
- this.current_reconnect_delay = 0;
155
+ this.#reset_reconnect_counters();
157
156
  return;
158
157
  }
159
158
  // Auto-reconnect still on: monotonically shorten the pending wait if
@@ -176,6 +175,50 @@ export class FrontendWebsocketClient {
176
175
  this.connect();
177
176
  }, new_remaining);
178
177
  }
178
+ /**
179
+ * Swap the heartbeat policy in place. Accepts the same shape as the
180
+ * constructor's `heartbeat` option: `false` disables the timer, `true` or
181
+ * `null`/omitted restores the defaults, or a config object customizes
182
+ * specific fields (missing fields fall back to defaults, not "keep
183
+ * current" — each call defines the whole policy atomically, same as the
184
+ * constructor and {@link set_reconnect}).
185
+ *
186
+ * When connected, the live timer is restarted immediately so the new
187
+ * `interval` / `receive_timeout` take effect without a reconnect; when
188
+ * disconnected, just stashes the policy for the next open.
189
+ */
190
+ set_heartbeat(heartbeat = null) {
191
+ this.#heartbeat_enabled = heartbeat !== false;
192
+ const config = typeof heartbeat === 'object' && heartbeat !== null ? heartbeat : {};
193
+ this.#heartbeat_interval = config.interval ?? DEFAULT_HEARTBEAT_INTERVAL;
194
+ this.#heartbeat_receive_timeout = config.receive_timeout ?? DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT;
195
+ if (this.connected) {
196
+ this.#start_heartbeat();
197
+ }
198
+ else {
199
+ this.#cancel_heartbeat();
200
+ }
201
+ }
202
+ /**
203
+ * Cancel a scheduled reconnect without closing the client or disabling
204
+ * auto-reconnect. Transitions status from `reconnecting` → `closed` and
205
+ * resets the backoff counters — the next close still triggers a fresh
206
+ * reconnect cycle under the current policy. No-op when no reconnect is
207
+ * pending.
208
+ *
209
+ * Use this when UI state asks "stop trying for now" without the finality
210
+ * of {@link disconnect} (which also rejects pending/queued requests and
211
+ * clears heartbeat) or the policy change of `set_reconnect(false)`
212
+ * (which disables future reconnects). The queue stays intact so that
213
+ * calling {@link connect} later flushes buffered work.
214
+ */
215
+ cancel_reconnect() {
216
+ if (this.#reconnect_timeout === null)
217
+ return;
218
+ this.#cancel_reconnect();
219
+ this.status = 'closed';
220
+ this.#reset_reconnect_counters();
221
+ }
179
222
  get url() {
180
223
  return this.#url;
181
224
  }
@@ -229,8 +272,7 @@ export class FrontendWebsocketClient {
229
272
  this.#cancel_heartbeat();
230
273
  this.#teardown(code);
231
274
  this.status = 'closed';
232
- this.reconnect_count = 0;
233
- this.current_reconnect_delay = 0;
275
+ this.#reset_reconnect_counters();
234
276
  this.#reject_all('client disconnected');
235
277
  }
236
278
  /** Explicit-resource-management hook — supports `using client = new FrontendWebsocketClient(url)`. */
@@ -535,10 +577,14 @@ export class FrontendWebsocketClient {
535
577
  }
536
578
  this.#reconnect_scheduled_at = null;
537
579
  }
538
- #handle_open = (_event) => {
539
- this.status = 'connected';
580
+ /** Reset the reactive reconnect counters — the pair always travels together. */
581
+ #reset_reconnect_counters() {
540
582
  this.reconnect_count = 0;
541
583
  this.current_reconnect_delay = 0;
584
+ }
585
+ #handle_open = (_event) => {
586
+ this.status = 'connected';
587
+ this.#reset_reconnect_counters();
542
588
  this.last_connect_time = Date.now();
543
589
  this.#cancel_reconnect();
544
590
  this.#start_heartbeat();
@@ -556,8 +602,7 @@ export class FrontendWebsocketClient {
556
602
  this.#revoked = true;
557
603
  this.status = 'closed';
558
604
  this.#cancel_reconnect();
559
- this.reconnect_count = 0;
560
- this.current_reconnect_delay = 0;
605
+ this.#reset_reconnect_counters();
561
606
  this.#reject_all('session revoked');
562
607
  return;
563
608
  }
@@ -630,3 +675,29 @@ export class FrontendWebsocketClient {
630
675
  }
631
676
  };
632
677
  }
678
+ /**
679
+ * Project {@link SocketStatus} onto fuz_util's {@link AsyncStatus} — the
680
+ * 5-way → 4-way mapping every consumer re-derives to surface connection state
681
+ * to UI (loading indicators, retry banners). Collapses `reconnecting` into
682
+ * `failure` (UI shows "lost, retrying") and splits `closed` by `revoked` so
683
+ * a terminal session-revocation read as `failure` while a clean client-
684
+ * initiated close reads as `initial` (the "not connected, not trying" state).
685
+ *
686
+ * @param status - the socket's current {@link SocketStatus}
687
+ * @param revoked - whether the session has been permanently revoked
688
+ * (typically `FrontendWebsocketClient.revoked`)
689
+ */
690
+ export const socket_status_to_async_status = (status, revoked) => {
691
+ switch (status) {
692
+ case 'initial':
693
+ return 'initial';
694
+ case 'connecting':
695
+ return 'pending';
696
+ case 'connected':
697
+ return 'success';
698
+ case 'reconnecting':
699
+ return 'failure';
700
+ case 'closed':
701
+ return revoked ? 'failure' : 'initial';
702
+ }
703
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"deno.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deno.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AAsCtE;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,WAwEhE,CAAC"}
1
+ {"version":3,"file":"deno.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deno.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AAwDtE;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,WA4HhE,CAAC"}
@@ -38,6 +38,33 @@ export const create_deno_runtime = (args) => ({
38
38
  mkdir: (path, options) => Deno.mkdir(path, options),
39
39
  read_text_file: (path) => Deno.readTextFile(path),
40
40
  read_file: (path) => Deno.readFile(path),
41
+ read_text_from_offset: async (path, offset) => {
42
+ const s = await Deno.stat(path);
43
+ const file_size = s.size;
44
+ const bytes_to_read = Math.max(0, file_size - offset);
45
+ if (bytes_to_read === 0)
46
+ return { content: '', bytes_read: 0, file_size };
47
+ const handle = await Deno.open(path, { read: true });
48
+ try {
49
+ await handle.seek(offset, Deno.SeekMode.Start);
50
+ const buffer = new Uint8Array(bytes_to_read);
51
+ const bytes_read = (await handle.read(buffer)) ?? 0;
52
+ return {
53
+ content: new TextDecoder().decode(buffer.subarray(0, bytes_read)),
54
+ bytes_read,
55
+ file_size,
56
+ };
57
+ }
58
+ finally {
59
+ handle.close();
60
+ }
61
+ },
62
+ readdir: async (path) => {
63
+ const names = [];
64
+ for await (const entry of Deno.readDir(path))
65
+ names.push(entry.name);
66
+ return names;
67
+ },
41
68
  write_text_file: (path, content) => Deno.writeTextFile(path, content),
42
69
  write_file: (path, data) => Deno.writeFile(path, data),
43
70
  rename: (old_path, new_path) => Deno.rename(old_path, new_path),
@@ -45,20 +72,46 @@ export const create_deno_runtime = (args) => ({
45
72
  // === HTTP ===
46
73
  fetch: globalThis.fetch,
47
74
  // === Local Commands ===
48
- run_command: async (cmd, args) => {
75
+ run_command: async (cmd, args, options) => {
49
76
  try {
50
- const proc = new Deno.Command(cmd, {
51
- args,
52
- stdout: 'piped',
53
- stderr: 'piped',
54
- });
55
- const result = await proc.output();
56
- return {
57
- success: result.code === 0,
58
- code: result.code,
59
- stdout: new TextDecoder().decode(result.stdout),
60
- stderr: new TextDecoder().decode(result.stderr),
61
- };
77
+ const controller = options?.timeout_ms !== undefined ? new AbortController() : null;
78
+ const signal = controller && options?.signal
79
+ ? AbortSignal.any([controller.signal, options.signal])
80
+ : (controller?.signal ?? options?.signal);
81
+ const timer = controller && options?.timeout_ms !== undefined
82
+ ? setTimeout(() => controller.abort(), options.timeout_ms)
83
+ : null;
84
+ let timed_out = false;
85
+ if (controller) {
86
+ controller.signal.addEventListener('abort', () => {
87
+ if (options?.signal?.aborted)
88
+ return;
89
+ timed_out = true;
90
+ }, { once: true });
91
+ }
92
+ try {
93
+ const proc = new Deno.Command(cmd, {
94
+ args,
95
+ cwd: options?.cwd,
96
+ signal,
97
+ stdout: 'piped',
98
+ stderr: 'piped',
99
+ });
100
+ const result = await proc.output();
101
+ const base = {
102
+ success: result.code === 0 && !timed_out,
103
+ code: result.code,
104
+ stdout: new TextDecoder().decode(result.stdout),
105
+ stderr: new TextDecoder().decode(result.stderr),
106
+ };
107
+ if (options?.timeout_ms !== undefined)
108
+ base.timed_out = timed_out;
109
+ return base;
110
+ }
111
+ finally {
112
+ if (timer !== null)
113
+ clearTimeout(timer);
114
+ }
62
115
  }
63
116
  catch (error) {
64
117
  const message = error instanceof Error ? error.message : String(error);
@@ -16,12 +16,28 @@ export interface StatResult {
16
16
  }
17
17
  /**
18
18
  * Result of executing a command.
19
+ *
20
+ * `timed_out` is present only when `timeout_ms` was passed in `RunCommandOptions`
21
+ * and the process was killed after exceeding the timeout. Callers that pass
22
+ * `timeout_ms` should check this flag to distinguish timeout from exit-code failure.
19
23
  */
20
24
  export interface CommandResult {
21
25
  success: boolean;
22
26
  code: number;
23
27
  stdout: string;
24
28
  stderr: string;
29
+ timed_out?: boolean;
30
+ }
31
+ /**
32
+ * Options for `run_command`.
33
+ */
34
+ export interface RunCommandOptions {
35
+ /** Working directory for the child process. */
36
+ cwd?: string;
37
+ /** AbortSignal to terminate the child process. */
38
+ signal?: AbortSignal;
39
+ /** Kill the process and return `timed_out: true` after this many milliseconds. */
40
+ timeout_ms?: number;
25
41
  }
26
42
  /**
27
43
  * Environment variable access.
@@ -32,16 +48,37 @@ export interface EnvDeps {
32
48
  /** Set an environment variable. */
33
49
  env_set: (name: string, value: string) => void;
34
50
  }
51
+ /**
52
+ * Result of reading text from a byte offset.
53
+ */
54
+ export interface ReadTextFromOffsetResult {
55
+ /** Decoded text content read from the offset. */
56
+ content: string;
57
+ /** Number of bytes actually read. */
58
+ bytes_read: number;
59
+ /** Total file size at the time of the read (for truncation detection). */
60
+ file_size: number;
61
+ }
35
62
  /**
36
63
  * File system read operations.
37
64
  */
38
65
  export interface FsReadDeps {
39
66
  /** Get file/directory stats, or null if path doesn't exist. */
40
67
  stat: (path: string) => Promise<StatResult | null>;
41
- /** Read a file as text. */
68
+ /** Read a file as text. Throws if the file does not exist. */
42
69
  read_text_file: (path: string) => Promise<string>;
43
- /** Read a file as bytes. */
70
+ /** Read a file as bytes. Throws if the file does not exist. */
44
71
  read_file: (path: string) => Promise<Uint8Array>;
72
+ /**
73
+ * Read text starting from a byte offset. Throws if the file does not exist.
74
+ *
75
+ * Returns `content`, `bytes_read`, and `file_size` so callers can detect
76
+ * truncation (when `file_size < offset`) and tail incrementally without
77
+ * re-reading the whole file.
78
+ */
79
+ read_text_from_offset: (path: string, offset: number) => Promise<ReadTextFromOffsetResult>;
80
+ /** List directory entries (names, not full paths). Throws if the directory does not exist. */
81
+ readdir: (path: string) => Promise<Array<string>>;
45
82
  }
46
83
  /**
47
84
  * File system write operations.
@@ -71,8 +108,15 @@ export interface FsRemoveDeps {
71
108
  * Command execution.
72
109
  */
73
110
  export interface CommandDeps {
74
- /** Run a command and return the result. */
75
- run_command: (cmd: string, args: Array<string>) => Promise<CommandResult>;
111
+ /**
112
+ * Run a command and return the result. Never throws — failures surface as
113
+ * `success: false`.
114
+ *
115
+ * `options.cwd` sets the child's working directory. `options.signal` aborts
116
+ * the child when the signal fires. `options.timeout_ms` kills the child
117
+ * after the given duration and returns `timed_out: true` on the result.
118
+ */
119
+ run_command: (cmd: string, args: Array<string>, options?: RunCommandOptions) => Promise<CommandResult>;
76
120
  }
77
121
  /**
78
122
  * HTTP fetch capability.
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import type { RuntimeDeps, CommandResult } from './deps.js';
10
+ import type { RuntimeDeps, CommandResult, RunCommandOptions } from './deps.js';
11
11
  /**
12
12
  * Mock `RuntimeDeps` with observable state for assertions.
13
13
  */
@@ -22,10 +22,11 @@ export interface MockRuntime extends RuntimeDeps {
22
22
  mock_dirs: Set<string>;
23
23
  /** Exit calls recorded (exit codes). */
24
24
  exit_calls: Array<number>;
25
- /** Commands executed. */
25
+ /** Commands executed. Captures `options` when passed so tests can assert cwd/timeout/signal. */
26
26
  command_calls: Array<{
27
27
  cmd: string;
28
28
  args: Array<string>;
29
+ options?: RunCommandOptions;
29
30
  }>;
30
31
  /** Commands executed with inherit. */
31
32
  command_inherit_calls: Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"mock.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAAc,aAAa,EAAC,MAAM,WAAW,CAAC;AAItE;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,+CAA+C;IAC/C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,mCAAmC;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,yBAAyB;IACzB,aAAa,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAC,CAAC,CAAC;IACzD,sCAAsC;IACtC,qBAAqB,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAC,CAAC,CAAC;IACjE,8BAA8B;IAC9B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjD,yCAAyC;IACzC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,4BAA4B;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAA;KAAC,CAAC,CAAC;IACxE,wDAAwD;IACxD,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAM,KAAK,CAAC,MAAM,CAAM,KAAG,WA4K9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,KAAG,IAazD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,KAAG,IAEpE,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM;CAKxB"}
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,WAAW,EAAc,aAAa,EAAE,iBAAiB,EAAC,MAAM,WAAW,CAAC;AAIzF;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,+CAA+C;IAC/C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,mCAAmC;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAC,CAAC,CAAC;IACtF,sCAAsC;IACtC,qBAAqB,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAC,CAAC,CAAC;IACjE,8BAA8B;IAC9B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjD,yCAAyC;IACzC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,4BAA4B;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAA;KAAC,CAAC,CAAC;IACxE,wDAAwD;IACxD,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAM,KAAK,CAAC,MAAM,CAAM,KAAG,WAkO9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,KAAG,IAazD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,KAAG,IAEpE,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM;CAKxB"}
@@ -114,6 +114,55 @@ export const create_mock_runtime = (args = []) => {
114
114
  error.code = 'ENOENT';
115
115
  throw error;
116
116
  },
117
+ read_text_from_offset: async (path, offset) => {
118
+ let bytes;
119
+ const stored_bytes = mock_fs_bytes.get(path);
120
+ if (stored_bytes !== undefined) {
121
+ bytes = stored_bytes;
122
+ }
123
+ else {
124
+ const content = mock_fs.get(path);
125
+ if (content === undefined) {
126
+ const error = new Error(`ENOENT: no such file or directory: ${path}`);
127
+ error.code = 'ENOENT';
128
+ throw error;
129
+ }
130
+ bytes = new TextEncoder().encode(content);
131
+ }
132
+ const file_size = bytes.length;
133
+ const bytes_to_read = Math.max(0, file_size - offset);
134
+ if (bytes_to_read === 0)
135
+ return { content: '', bytes_read: 0, file_size };
136
+ const slice = bytes.subarray(offset, offset + bytes_to_read);
137
+ return {
138
+ content: new TextDecoder().decode(slice),
139
+ bytes_read: slice.length,
140
+ file_size,
141
+ };
142
+ },
143
+ readdir: async (path) => {
144
+ const prefix = path.endsWith('/') ? path : path + '/';
145
+ const seen = new Set();
146
+ const collect = (key) => {
147
+ if (!key.startsWith(prefix))
148
+ return;
149
+ const rest = key.slice(prefix.length);
150
+ const slash = rest.indexOf('/');
151
+ seen.add(slash === -1 ? rest : rest.slice(0, slash));
152
+ };
153
+ for (const key of mock_fs.keys())
154
+ collect(key);
155
+ for (const key of mock_fs_bytes.keys())
156
+ collect(key);
157
+ for (const key of mock_dirs)
158
+ collect(key);
159
+ if (seen.size === 0 && !mock_dirs.has(path)) {
160
+ const error = new Error(`ENOENT: no such file or directory: ${path}`);
161
+ error.code = 'ENOENT';
162
+ throw error;
163
+ }
164
+ return Array.from(seen).sort();
165
+ },
117
166
  write_text_file: async (path, content) => {
118
167
  mock_fs.set(path, content);
119
168
  },
@@ -163,13 +212,20 @@ export const create_mock_runtime = (args = []) => {
163
212
  throw new TypeError(`fetch failed (no mock for ${url})`);
164
213
  },
165
214
  // === Local Commands ===
166
- run_command: async (cmd, args) => {
167
- command_calls.push({ cmd, args });
215
+ run_command: async (cmd, args, options) => {
216
+ command_calls.push(options ? { cmd, args, options } : { cmd, args });
168
217
  const key = `${cmd} ${args.join(' ')}`;
169
218
  const mocked = mock_command_results.get(key);
170
- if (mocked)
219
+ if (mocked) {
220
+ if (options?.timeout_ms !== undefined && mocked.timed_out === undefined) {
221
+ return { ...mocked, timed_out: false };
222
+ }
171
223
  return mocked;
172
- return { success: true, code: 0, stdout: '', stderr: '' };
224
+ }
225
+ const result = { success: true, code: 0, stdout: '', stderr: '' };
226
+ if (options?.timeout_ms !== undefined)
227
+ result.timed_out = false;
228
+ return result;
173
229
  },
174
230
  run_command_inherit: async (cmd, args) => {
175
231
  command_inherit_calls.push({ cmd, args });
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAM,aAAa,CAAC,MAAM,CAAyB,KACjD,WAkHD,CAAC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAC,WAAW,EAA4B,MAAM,WAAW,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAM,aAAa,CAAC,MAAM,CAAyB,KACjD,WAmKD,CAAC"}
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Buffer } from 'node:buffer';
10
10
  import { spawn } from 'node:child_process';
11
- import { stat, mkdir, readFile, writeFile, rename, rm } from 'node:fs/promises';
11
+ import { stat, mkdir, readFile, readdir, writeFile, rename, rm, open } from 'node:fs/promises';
12
12
  import process from 'node:process';
13
13
  /**
14
14
  * Create a `RuntimeDeps` backed by Node.js APIs.
@@ -42,6 +42,27 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
42
42
  },
43
43
  read_text_file: (path) => readFile(path, 'utf-8'),
44
44
  read_file: (path) => readFile(path).then((buf) => new Uint8Array(buf)),
45
+ read_text_from_offset: async (path, offset) => {
46
+ const s = await stat(path);
47
+ const file_size = s.size;
48
+ const bytes_to_read = Math.max(0, file_size - offset);
49
+ if (bytes_to_read === 0)
50
+ return { content: '', bytes_read: 0, file_size };
51
+ const handle = await open(path, 'r');
52
+ try {
53
+ const buffer = Buffer.alloc(bytes_to_read);
54
+ const { bytesRead } = await handle.read(buffer, 0, bytes_to_read, offset);
55
+ return {
56
+ content: buffer.toString('utf-8', 0, bytesRead),
57
+ bytes_read: bytesRead,
58
+ file_size,
59
+ };
60
+ }
61
+ finally {
62
+ await handle.close();
63
+ }
64
+ },
65
+ readdir: (path) => readdir(path),
45
66
  write_text_file: (path, content) => writeFile(path, content, 'utf-8'),
46
67
  write_file: (path, data) => writeFile(path, data),
47
68
  rename: (old_path, new_path) => rename(old_path, new_path),
@@ -49,17 +70,45 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
49
70
  // === HTTP ===
50
71
  fetch: globalThis.fetch,
51
72
  // === Local Commands ===
52
- run_command: (cmd, args) => {
73
+ run_command: (cmd, args, options) => {
53
74
  return new Promise((resolve) => {
54
75
  const proc = spawn(cmd, args, {
55
76
  stdio: ['ignore', 'pipe', 'pipe'],
77
+ cwd: options?.cwd,
56
78
  });
57
79
  const stdout_chunks = [];
58
80
  const stderr_chunks = [];
81
+ let timed_out = false;
82
+ let done = false;
83
+ const finish = (result) => {
84
+ if (done)
85
+ return;
86
+ done = true;
87
+ if (timer !== null)
88
+ clearTimeout(timer);
89
+ if (options?.signal)
90
+ options.signal.removeEventListener('abort', on_abort);
91
+ resolve(result);
92
+ };
93
+ const on_abort = () => {
94
+ proc.kill();
95
+ };
96
+ const timer = options?.timeout_ms !== undefined
97
+ ? setTimeout(() => {
98
+ timed_out = true;
99
+ proc.kill();
100
+ }, options.timeout_ms)
101
+ : null;
102
+ if (options?.signal) {
103
+ if (options.signal.aborted)
104
+ proc.kill();
105
+ else
106
+ options.signal.addEventListener('abort', on_abort, { once: true });
107
+ }
59
108
  proc.stdout.on('data', (chunk) => stdout_chunks.push(chunk));
60
109
  proc.stderr.on('data', (chunk) => stderr_chunks.push(chunk));
61
110
  proc.on('error', (error) => {
62
- resolve({
111
+ finish({
63
112
  success: false,
64
113
  code: 1,
65
114
  stdout: '',
@@ -67,12 +116,15 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
67
116
  });
68
117
  });
69
118
  proc.on('close', (code) => {
70
- resolve({
71
- success: code === 0,
119
+ const result = {
120
+ success: code === 0 && !timed_out,
72
121
  code: code ?? 1,
73
122
  stdout: Buffer.concat(stdout_chunks).toString('utf-8').trim(),
74
123
  stderr: Buffer.concat(stderr_chunks).toString('utf-8').trim(),
75
- });
124
+ };
125
+ if (options?.timeout_ms !== undefined)
126
+ result.timed_out = timed_out;
127
+ finish(result);
76
128
  });
77
129
  });
78
130
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -45,9 +45,9 @@
45
45
  "@fuzdev/blake3_wasm": "^0.1.0",
46
46
  "@fuzdev/fuz_code": "^0.45.1",
47
47
  "@fuzdev/fuz_css": "^0.58.0",
48
- "@fuzdev/fuz_ui": "^0.191.2",
49
- "@fuzdev/fuz_util": "^0.56.0",
50
- "@fuzdev/gro": "^0.197.3",
48
+ "@fuzdev/fuz_ui": "^0.191.4",
49
+ "@fuzdev/fuz_util": "^0.57.0",
50
+ "@fuzdev/gro": "^0.198.0",
51
51
  "@jridgewell/trace-mapping": "^0.3.31",
52
52
  "@node-rs/argon2": "^2.0.2",
53
53
  "@ryanatkn/eslint-config": "^0.11.0",