@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.
- package/dist/actions/register_action_ws.d.ts +11 -4
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +11 -4
- package/dist/actions/register_ws_endpoint.d.ts +46 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -0
- package/dist/actions/register_ws_endpoint.js +38 -0
- package/dist/actions/socket.svelte.d.ts +43 -2
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +80 -9
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +66 -13
- package/dist/runtime/deps.d.ts +48 -4
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +3 -2
- package/dist/runtime/mock.d.ts.map +1 -1
- package/dist/runtime/mock.js +60 -4
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +58 -6
- package/package.json +4 -4
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WebSocket JSON-RPC dispatch — the
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;
|
|
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
|
|
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
|
|
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
|
-
|
|
539
|
-
|
|
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
|
|
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;
|
|
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"}
|
package/dist/runtime/deno.js
CHANGED
|
@@ -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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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);
|
package/dist/runtime/deps.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
75
|
-
|
|
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":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;CACtB;AAED
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/mock.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/runtime/mock.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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"}
|
package/dist/runtime/node.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
49
|
-
"@fuzdev/fuz_util": "^0.
|
|
50
|
-
"@fuzdev/gro": "^0.
|
|
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",
|