@fuzdev/fuz_app 0.16.0 → 0.17.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/broadcast_api.d.ts +99 -0
- package/dist/actions/broadcast_api.d.ts.map +1 -0
- package/dist/actions/broadcast_api.js +99 -0
- package/dist/actions/transports_ws_backend.d.ts +27 -2
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-initiated broadcast notification plumbing — generic across consumers.
|
|
3
|
+
*
|
|
4
|
+
* Builds a typed `{method_name: (input) => Promise<void>}` object from a list
|
|
5
|
+
* of action specs. Each call validates input against the spec, wraps it in a
|
|
6
|
+
* JSON-RPC notification, and either broadcasts to every connection or
|
|
7
|
+
* fans out with a per-connection ACL predicate.
|
|
8
|
+
*
|
|
9
|
+
* Counterpart to `register_action_ws`: that handles request-scoped dispatch
|
|
10
|
+
* (frontend-initiated), this handles broadcast (backend-initiated). Together
|
|
11
|
+
* they cover the two primitives fuz_app consumers share. Request-scoped
|
|
12
|
+
* streaming (`completion_progress`, `tx_apply` events) stays on
|
|
13
|
+
* `ctx.notify` inside a handler — it's socket-scoped, not broadcast.
|
|
14
|
+
*
|
|
15
|
+
* Extracted from zzz's `backend_actions_api.ts` as part of the websockets
|
|
16
|
+
* quest (Phase 3) to stop the pattern from drifting across zzz, tx, and
|
|
17
|
+
* undying.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.js';
|
|
22
|
+
import type { ActionPeer } from './action_peer.js';
|
|
23
|
+
import type { ActionSpecUnion } from './action_spec.js';
|
|
24
|
+
import { type ConnectionIdentity } from './transports_ws_backend.js';
|
|
25
|
+
/**
|
|
26
|
+
* Per-connection delivery predicate for subscription ACLs.
|
|
27
|
+
*
|
|
28
|
+
* Called once per connection for every broadcast send. Returning `false`
|
|
29
|
+
* skips that connection. Keep it fast — this runs in the broadcast hot path.
|
|
30
|
+
*
|
|
31
|
+
* `input` is the already-validated payload (matches the spec's input schema);
|
|
32
|
+
* `method` is the action method name.
|
|
33
|
+
*/
|
|
34
|
+
export type ShouldDeliverFn = (connection: ConnectionIdentity, method: string, input: unknown) => boolean;
|
|
35
|
+
/** Options for `create_broadcast_api`. */
|
|
36
|
+
export interface CreateBroadcastApiOptions {
|
|
37
|
+
/** The peer holding the transport registry used for sends. */
|
|
38
|
+
peer: ActionPeer;
|
|
39
|
+
/**
|
|
40
|
+
* Notification specs to expose as broadcast methods. Typically the
|
|
41
|
+
* `remote_notification` specs whose initiator is `backend` (or `both`).
|
|
42
|
+
* Other kinds are accepted — the helper only uses `spec.method` and
|
|
43
|
+
* `spec.input` — but the typical use is notifications.
|
|
44
|
+
*/
|
|
45
|
+
specs: ReadonlyArray<ActionSpecUnion>;
|
|
46
|
+
/** Logger for validation/send errors. Defaults to a `[broadcast]` namespace. */
|
|
47
|
+
log?: LoggerType | null;
|
|
48
|
+
/**
|
|
49
|
+
* Optional per-connection ACL predicate. When set, the broadcast fans out
|
|
50
|
+
* via the transport's `broadcast_filtered` (feature-detected) — each
|
|
51
|
+
* connection's identity is checked before the message is sent. When
|
|
52
|
+
* unset, the transport broadcasts unfiltered via `transport.send`.
|
|
53
|
+
*
|
|
54
|
+
* Requires a transport that implements `FilterableBroadcastTransport`
|
|
55
|
+
* (today: only `BackendWebsocketTransport`). If set and the active
|
|
56
|
+
* transport is not filterable, the send is skipped and an error logged.
|
|
57
|
+
*/
|
|
58
|
+
should_deliver?: ShouldDeliverFn;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Loose base shape for a broadcast API. Consumers typically declare a
|
|
62
|
+
* stricter per-method interface (e.g. `BackendActionsApi`) and pin it via
|
|
63
|
+
* the type parameter on `create_broadcast_api`.
|
|
64
|
+
*/
|
|
65
|
+
export type BroadcastApi = Record<string, (input: never) => Promise<void>>;
|
|
66
|
+
/**
|
|
67
|
+
* Builds a typed broadcast API from a set of action specs.
|
|
68
|
+
*
|
|
69
|
+
* For each spec, adds a method keyed by `spec.method` that:
|
|
70
|
+
* - Validates `input` against the spec's Zod schema (logs and returns on failure)
|
|
71
|
+
* - Creates a JSON-RPC notification from the validated input
|
|
72
|
+
* - Broadcasts via the peer (filtered by `should_deliver` when supplied)
|
|
73
|
+
*
|
|
74
|
+
* Silently returns when no transport is ready (e.g. before any clients
|
|
75
|
+
* connect). Errors during send are logged but never thrown — broadcasts are
|
|
76
|
+
* fire-and-forget from the handler's perspective.
|
|
77
|
+
*
|
|
78
|
+
* ## Typed consumer surface
|
|
79
|
+
*
|
|
80
|
+
* Consumers declare an explicit interface and pin it via the type parameter:
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* export interface BackendActionsApi {
|
|
84
|
+
* filer_change: (input: ActionInputs['filer_change']) => Promise<void>;
|
|
85
|
+
* workspace_changed: (input: ActionInputs['workspace_changed']) => Promise<void>;
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* const api = create_broadcast_api<BackendActionsApi>({
|
|
89
|
+
* peer: backend.peer,
|
|
90
|
+
* specs: [filer_change_action_spec, workspace_changed_action_spec],
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* The cast is unchecked — callers must keep the interface and the `specs`
|
|
95
|
+
* array in sync. Codegen (`action_collections.gen.ts`) is a natural fit
|
|
96
|
+
* if the consumer already generates per-method type maps.
|
|
97
|
+
*/
|
|
98
|
+
export declare const create_broadcast_api: <TApi = BroadcastApi>(options: CreateBroadcastApiOptions) => TApi;
|
|
99
|
+
//# sourceMappingURL=broadcast_api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broadcast_api.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/broadcast_api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG1E,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAEN,KAAK,kBAAkB,EACvB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG,CAC7B,UAAU,EAAE,kBAAkB,EAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,KACV,OAAO,CAAC;AAEb,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACzC,8DAA8D;IAC9D,IAAI,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IACtC,gFAAgF;IAChF,GAAG,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,eAAe,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,oBAAoB,GAAI,IAAI,GAAG,YAAY,EACvD,SAAS,yBAAyB,KAChC,IAoDF,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-initiated broadcast notification plumbing — generic across consumers.
|
|
3
|
+
*
|
|
4
|
+
* Builds a typed `{method_name: (input) => Promise<void>}` object from a list
|
|
5
|
+
* of action specs. Each call validates input against the spec, wraps it in a
|
|
6
|
+
* JSON-RPC notification, and either broadcasts to every connection or
|
|
7
|
+
* fans out with a per-connection ACL predicate.
|
|
8
|
+
*
|
|
9
|
+
* Counterpart to `register_action_ws`: that handles request-scoped dispatch
|
|
10
|
+
* (frontend-initiated), this handles broadcast (backend-initiated). Together
|
|
11
|
+
* they cover the two primitives fuz_app consumers share. Request-scoped
|
|
12
|
+
* streaming (`completion_progress`, `tx_apply` events) stays on
|
|
13
|
+
* `ctx.notify` inside a handler — it's socket-scoped, not broadcast.
|
|
14
|
+
*
|
|
15
|
+
* Extracted from zzz's `backend_actions_api.ts` as part of the websockets
|
|
16
|
+
* quest (Phase 3) to stop the pattern from drifting across zzz, tx, and
|
|
17
|
+
* undying.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
22
|
+
import { create_jsonrpc_notification, to_jsonrpc_params } from '../http/jsonrpc_helpers.js';
|
|
23
|
+
import { is_filterable_broadcast_transport, } from './transports_ws_backend.js';
|
|
24
|
+
/**
|
|
25
|
+
* Builds a typed broadcast API from a set of action specs.
|
|
26
|
+
*
|
|
27
|
+
* For each spec, adds a method keyed by `spec.method` that:
|
|
28
|
+
* - Validates `input` against the spec's Zod schema (logs and returns on failure)
|
|
29
|
+
* - Creates a JSON-RPC notification from the validated input
|
|
30
|
+
* - Broadcasts via the peer (filtered by `should_deliver` when supplied)
|
|
31
|
+
*
|
|
32
|
+
* Silently returns when no transport is ready (e.g. before any clients
|
|
33
|
+
* connect). Errors during send are logged but never thrown — broadcasts are
|
|
34
|
+
* fire-and-forget from the handler's perspective.
|
|
35
|
+
*
|
|
36
|
+
* ## Typed consumer surface
|
|
37
|
+
*
|
|
38
|
+
* Consumers declare an explicit interface and pin it via the type parameter:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* export interface BackendActionsApi {
|
|
42
|
+
* filer_change: (input: ActionInputs['filer_change']) => Promise<void>;
|
|
43
|
+
* workspace_changed: (input: ActionInputs['workspace_changed']) => Promise<void>;
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* const api = create_broadcast_api<BackendActionsApi>({
|
|
47
|
+
* peer: backend.peer,
|
|
48
|
+
* specs: [filer_change_action_spec, workspace_changed_action_spec],
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* The cast is unchecked — callers must keep the interface and the `specs`
|
|
53
|
+
* array in sync. Codegen (`action_collections.gen.ts`) is a natural fit
|
|
54
|
+
* if the consumer already generates per-method type maps.
|
|
55
|
+
*/
|
|
56
|
+
export const create_broadcast_api = (options) => {
|
|
57
|
+
const { peer, specs, should_deliver } = options;
|
|
58
|
+
const log = options.log === undefined ? new Logger('[broadcast]') : options.log;
|
|
59
|
+
const api = {};
|
|
60
|
+
for (const spec of specs) {
|
|
61
|
+
const { method } = spec;
|
|
62
|
+
api[method] = async (input) => {
|
|
63
|
+
const parsed = spec.input.safeParse(input);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
log?.error(`[${method}] input validation failed:`, parsed.error.issues);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Resolve the broadcast target deterministically — no fallback.
|
|
69
|
+
// Broadcast is 1→N over a specific primary transport; falling through
|
|
70
|
+
// to "any ready transport" would send to an unexpected audience.
|
|
71
|
+
// Silent skip when no ready transport (e.g. before any clients connect).
|
|
72
|
+
const transport_name = peer.default_send_options.transport_name;
|
|
73
|
+
const transport = transport_name
|
|
74
|
+
? peer.transports.get_transport_by_name(transport_name)
|
|
75
|
+
: peer.transports.get_current_transport();
|
|
76
|
+
if (!transport?.is_ready())
|
|
77
|
+
return;
|
|
78
|
+
const notification = create_jsonrpc_notification(method, to_jsonrpc_params(parsed.data));
|
|
79
|
+
try {
|
|
80
|
+
if (should_deliver) {
|
|
81
|
+
if (!is_filterable_broadcast_transport(transport)) {
|
|
82
|
+
log?.error(`[${method}] should_deliver set but transport ${transport.transport_name} does not support per-connection filtering`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
transport.broadcast_filtered(notification, (identity) => should_deliver(identity, method, parsed.data));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const result = await transport.send(notification);
|
|
89
|
+
if (result !== null) {
|
|
90
|
+
log?.error(`[${method}] failed to send notification:`, result.error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
log?.error(`[${method}] unexpected error:`, error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return api;
|
|
99
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import type { WSContext } from 'hono/ws';
|
|
8
|
-
import type { JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
8
|
+
import type { JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
9
9
|
import { type Uuid } from '../uuid.js';
|
|
10
10
|
import { type Transport } from './transports.js';
|
|
11
11
|
/**
|
|
@@ -24,7 +24,21 @@ export interface ConnectionIdentity {
|
|
|
24
24
|
/** `api_token.id` for bearer-authenticated connections, else null. */
|
|
25
25
|
api_token_id: string | null;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Structural capability for transports that can broadcast with a
|
|
29
|
+
* per-connection ACL predicate. Named separately from `Transport` so the
|
|
30
|
+
* broadcast API can feature-detect without importing a concrete class.
|
|
31
|
+
*
|
|
32
|
+
* `ConnectionIdentity` is the auth-gated identity shape used today. When a
|
|
33
|
+
* second implementation (e.g. SSE backend transport) lands with a
|
|
34
|
+
* different identity, consider parameterizing on `TIdentity`.
|
|
35
|
+
*/
|
|
36
|
+
export interface FilterableBroadcastTransport extends Transport {
|
|
37
|
+
broadcast_filtered: (message: JsonrpcMessageFromServerToClient, predicate: (identity: ConnectionIdentity) => boolean) => number;
|
|
38
|
+
}
|
|
39
|
+
/** Type guard for `FilterableBroadcastTransport`. */
|
|
40
|
+
export declare const is_filterable_broadcast_transport: (transport: Transport) => transport is FilterableBroadcastTransport;
|
|
41
|
+
export declare class BackendWebsocketTransport implements FilterableBroadcastTransport {
|
|
28
42
|
#private;
|
|
29
43
|
readonly transport_name: "backend_websocket_rpc";
|
|
30
44
|
/**
|
|
@@ -66,6 +80,17 @@ export declare class BackendWebsocketTransport implements Transport {
|
|
|
66
80
|
close_sockets_for_token(api_token_id: string): number;
|
|
67
81
|
send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
|
|
68
82
|
send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Broadcast to connections whose identity satisfies a predicate.
|
|
85
|
+
*
|
|
86
|
+
* Used by the broadcast API when a consumer supplies a subscription ACL hook
|
|
87
|
+
* (e.g. tx's `tx_run_created` only reaches the account that owns the run).
|
|
88
|
+
* When no ACL is needed, callers should prefer `send(message)` / `#broadcast`
|
|
89
|
+
* to skip the per-connection predicate overhead.
|
|
90
|
+
*
|
|
91
|
+
* @returns the number of sockets the message was sent to
|
|
92
|
+
*/
|
|
93
|
+
broadcast_filtered(message: JsonrpcMessageFromServerToClient, predicate: (identity: ConnectionIdentity) => boolean): number;
|
|
69
94
|
is_ready(): boolean;
|
|
70
95
|
}
|
|
71
96
|
//# sourceMappingURL=transports_ws_backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAc,KAAK,IAAI,EAAC,MAAM,YAAY,CAAC;AAClD,OAAO,EAA2B,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAIzE;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,EAAE,IAAI,CAAC;IACjB,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4BAA6B,SAAQ,SAAS;IAC9D,kBAAkB,EAAE,CACnB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,KAChD,MAAM,CAAC;CACZ;AAED,qDAAqD;AACrD,eAAO,MAAM,iCAAiC,GAC7C,WAAW,SAAS,KAClB,SAAS,IAAI,4BAEqE,CAAC;AAEtF,qBAAa,yBAA0B,YAAW,4BAA4B;;IAC7E,QAAQ,CAAC,cAAc,EAAG,uBAAuB,CAAU;IAY3D;;;;;;;;OAQG;IACH,cAAc,CACb,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,IAAI,EAChB,YAAY,GAAE,MAAM,GAAG,IAAW,GAChC,IAAI;IAQP;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IA0BtC;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIrD;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM;IAInD;;;;;;;;OAQG;IACH,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAsB/C,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA4C9E;;;;;;;;;OASG;IACH,kBAAkB,CACjB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,GAClD,MAAM;IAoBT,QAAQ,IAAI,OAAO;CAGnB"}
|
|
@@ -8,6 +8,9 @@ import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
|
8
8
|
import { create_jsonrpc_error_response, to_jsonrpc_message_id, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
|
|
9
9
|
import { create_uuid } from '../uuid.js';
|
|
10
10
|
import { WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
11
|
+
/** Type guard for `FilterableBroadcastTransport`. */
|
|
12
|
+
export const is_filterable_broadcast_transport = (transport) => 'broadcast_filtered' in transport &&
|
|
13
|
+
typeof transport.broadcast_filtered === 'function';
|
|
11
14
|
export class BackendWebsocketTransport {
|
|
12
15
|
transport_name = 'backend_websocket_rpc';
|
|
13
16
|
// Map connection IDs to WebSocket contexts
|
|
@@ -135,6 +138,35 @@ export class BackendWebsocketTransport {
|
|
|
135
138
|
// TODO hack - remove if not ever needed, I assume this will need to be async so let's hold that assumption
|
|
136
139
|
return Promise.resolve();
|
|
137
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Broadcast to connections whose identity satisfies a predicate.
|
|
143
|
+
*
|
|
144
|
+
* Used by the broadcast API when a consumer supplies a subscription ACL hook
|
|
145
|
+
* (e.g. tx's `tx_run_created` only reaches the account that owns the run).
|
|
146
|
+
* When no ACL is needed, callers should prefer `send(message)` / `#broadcast`
|
|
147
|
+
* to skip the per-connection predicate overhead.
|
|
148
|
+
*
|
|
149
|
+
* @returns the number of sockets the message was sent to
|
|
150
|
+
*/
|
|
151
|
+
broadcast_filtered(message, predicate) {
|
|
152
|
+
const serialized = JSON.stringify(message);
|
|
153
|
+
let count = 0;
|
|
154
|
+
for (const [connection_id, identity] of this.#connection_identities) {
|
|
155
|
+
if (!predicate(identity))
|
|
156
|
+
continue;
|
|
157
|
+
const ws = this.#connections.get(connection_id);
|
|
158
|
+
if (!ws)
|
|
159
|
+
continue;
|
|
160
|
+
try {
|
|
161
|
+
ws.send(serialized);
|
|
162
|
+
count++;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('[backend websocket transport] Error broadcasting filtered to client:', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return count;
|
|
169
|
+
}
|
|
138
170
|
is_ready() {
|
|
139
171
|
return this.#connections.size > 0;
|
|
140
172
|
}
|