@fuzdev/fuz_app 0.27.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/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
|
+
};
|
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",
|