cross-tab-worker-databus 0.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +185 -0
  4. package/README.zh.md +98 -0
  5. package/dist/centrifuge-protocol.d.ts +77 -0
  6. package/dist/centrifuge-protocol.d.ts.map +1 -0
  7. package/dist/centrifuge-session.d.ts +39 -0
  8. package/dist/centrifuge-session.d.ts.map +1 -0
  9. package/dist/centrifuge.d.ts +135 -0
  10. package/dist/centrifuge.d.ts.map +1 -0
  11. package/dist/centrifuge.js +407 -0
  12. package/dist/centrifuge.js.map +7 -0
  13. package/dist/centrifuge.shared.worker.js +5220 -0
  14. package/dist/centrifuge.shared.worker.js.map +7 -0
  15. package/dist/centrifuge.worker.js +5109 -0
  16. package/dist/centrifuge.worker.js.map +7 -0
  17. package/dist/chunk-GABYBK7I.js +1527 -0
  18. package/dist/chunk-GABYBK7I.js.map +7 -0
  19. package/dist/core/cluster.d.ts +219 -0
  20. package/dist/core/cluster.d.ts.map +1 -0
  21. package/dist/core/data-bus.d.ts +133 -0
  22. package/dist/core/data-bus.d.ts.map +1 -0
  23. package/dist/core/environment.d.ts +67 -0
  24. package/dist/core/environment.d.ts.map +1 -0
  25. package/dist/core/hash.d.ts +11 -0
  26. package/dist/core/hash.d.ts.map +1 -0
  27. package/dist/core/routing.d.ts +42 -0
  28. package/dist/core/routing.d.ts.map +1 -0
  29. package/dist/core/storage-batch.d.ts +35 -0
  30. package/dist/core/storage-batch.d.ts.map +1 -0
  31. package/dist/core/trace.d.ts +126 -0
  32. package/dist/core/trace.d.ts.map +1 -0
  33. package/dist/core/types.d.ts +112 -0
  34. package/dist/core/types.d.ts.map +1 -0
  35. package/dist/index.d.ts +23 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +27 -0
  38. package/dist/index.js.map +7 -0
  39. package/dist/worker-mode.d.ts +25 -0
  40. package/dist/worker-mode.d.ts.map +1 -0
  41. package/dist/workers/centrifuge.shared.worker.d.ts +2 -0
  42. package/dist/workers/centrifuge.shared.worker.d.ts.map +1 -0
  43. package/dist/workers/centrifuge.worker.d.ts +2 -0
  44. package/dist/workers/centrifuge.worker.d.ts.map +1 -0
  45. package/dist/workers/port-reaper.d.ts +52 -0
  46. package/dist/workers/port-reaper.d.ts.map +1 -0
  47. package/docs/README.md +21 -0
  48. package/docs/api.md +261 -0
  49. package/docs/architecture.md +514 -0
  50. package/docs/capabilities.md +41 -0
  51. package/docs/configuration.md +211 -0
  52. package/docs/getting-started.md +161 -0
  53. package/docs/zh/README.md +23 -0
  54. package/docs/zh/api.md +261 -0
  55. package/docs/zh/architecture.md +515 -0
  56. package/docs/zh/capabilities.md +41 -0
  57. package/docs/zh/configuration.md +211 -0
  58. package/docs/zh/getting-started.md +161 -0
  59. package/package.json +71 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Worker-mode types and backend selection.
3
+ *
4
+ * Defines the WorkerMode preference flags and the selectWorkerBackend function
5
+ * that resolves the preference against browser capability to pick the actual
6
+ * backend (dedicated, shared, or local fallback).
7
+ */
8
+ /** Preferred Worker mode: 'dedicated' → try Dedicated Worker first, 'shared'/'auto' → try SharedWorker first. */
9
+ export type WorkerMode = 'dedicated' | 'shared' | 'auto';
10
+ /** Resolved backend that was actually created. */
11
+ export type WorkerBackend = 'dedicated' | 'shared' | 'local';
12
+ /** Override Worker availability for testing or environments where feature detection is unreliable. */
13
+ export interface WorkerAvailability {
14
+ worker?: boolean;
15
+ sharedWorker?: boolean;
16
+ }
17
+ /**
18
+ * Resolves the worker backend by feature detection, without touching globals
19
+ * that may be missing in SSR or embedded environments.
20
+ *
21
+ * - `dedicated` prefers Dedicated Worker, then SharedWorker, then local mode.
22
+ * - `shared` and `auto` prefer SharedWorker, then Dedicated Worker, then local.
23
+ */
24
+ export declare function selectWorkerBackend(mode: WorkerMode, availability?: WorkerAvailability): WorkerBackend;
25
+ //# sourceMappingURL=worker-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-mode.d.ts","sourceRoot":"","sources":["../src/worker-mode.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,iHAAiH;AACjH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7D,sGAAsG;AACtG,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,YAAY,GAAE,kBAAuB,GACpC,aAAa,CAOf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=centrifuge.shared.worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"centrifuge.shared.worker.d.ts","sourceRoot":"","sources":["../../src/workers/centrifuge.shared.worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=centrifuge.worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"centrifuge.worker.d.ts","sourceRoot":"","sources":["../../src/workers/centrifuge.worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Reaper for SharedWorker ports.
3
+ *
4
+ * A `MessagePort` has no `close` event, so the SharedWorker cannot tell whether
5
+ * a tab died without sending STOP. The main thread therefore sends periodic
6
+ * PING messages, and this reaper closes any session whose port has been silent
7
+ * longer than its timeout.
8
+ *
9
+ * The cadence is adaptive: the reaper runs at the smallest configured heartbeat
10
+ * interval across active ports, and stops entirely when the last port is
11
+ * removed, so a long-lived SharedWorker does not run a perpetual no-op interval
12
+ * between connect bursts.
13
+ *
14
+ * The timers and clock are injected so the reaper can be unit-tested without a
15
+ * real worker or DOM.
16
+ */
17
+ /** A reaped port: close it (stop message delivery) and stop its session. */
18
+ export interface ReapTarget {
19
+ close(): void;
20
+ stop(): void;
21
+ }
22
+ type SetTimer = (callback: () => void, ms: number) => number;
23
+ type ClearTimer = (handle: number) => void;
24
+ export declare class PortReaper {
25
+ private readonly now;
26
+ private readonly targets;
27
+ private readonly lastSeenAt;
28
+ private readonly sessionTimeoutMs;
29
+ private handle;
30
+ private intervalMs;
31
+ constructor(now?: () => number, setTimer?: SetTimer, clearTimer?: ClearTimer);
32
+ private readonly setTimer;
33
+ private readonly clearTimer;
34
+ /** Track a newly connected port and its session. The default timeout applies
35
+ * until the port sends an INIT with its own heartbeat config. */
36
+ register(port: MessagePort, target: ReapTarget): void;
37
+ /** Record activity on a port (any incoming message). No-op for untracked ports. */
38
+ touch(port: MessagePort): void;
39
+ /** Override a port's session timeout from its INIT heartbeat config. */
40
+ setTimeout(port: MessagePort, heartbeatIntervalMs: number): void;
41
+ /** Remove a port (STOP message). The caller is responsible for closing the
42
+ * port and stopping the session. */
43
+ remove(port: MessagePort): void;
44
+ /** Clear all state and stop the reaper (worker shutdown). */
45
+ dispose(): void;
46
+ /** Recompute the reaper cadence, or stop it entirely when no ports remain. */
47
+ private schedule;
48
+ /** Close sessions whose port has been silent longer than its timeout. */
49
+ private reap;
50
+ }
51
+ export {};
52
+ //# sourceMappingURL=port-reaper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;CACd;AAED,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;AAC7D,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;IACnE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAiC;gBAGhC,GAAG,GAAE,MAAM,MAAiB,EAC7C,QAAQ,GAAE,QAA2E,EACrF,UAAU,GAAE,UAA4C;IAM1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC;qEACiE;IACjE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD,mFAAmF;IACnF,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B,wEAAwE;IACxE,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI;IAMhE;wCACoC;IACpC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAQ/B,6DAA6D;IAC7D,OAAO,IAAI,IAAI;IAQf,8EAA8E;IAC9E,OAAO,CAAC,QAAQ;IAiBhB,yEAAyE;IACzE,OAAO,CAAC,IAAI;CAuBb"}
package/docs/README.md ADDED
@@ -0,0 +1,21 @@
1
+ > [中文](./zh/README.md) | English
2
+
3
+ # Documentation Index
4
+
5
+ | Document | Description |
6
+ |---|---|
7
+ | [Getting Started](./getting-started.md) | Installation, creating instances, subscribing, publishing, and destroying |
8
+ | [Configuration](./configuration.md) | Core configuration, Centrifuge configuration, defaults, and constraints |
9
+ | [API Reference](./api.md) | Public API, types, methods, return values, and behavior |
10
+ | [Architecture](./architecture.md) | Worker cluster, routing, storage, migration, and degradation design |
11
+ | [Capabilities Matrix](./capabilities.md) | Implemented, not implemented, and planned capabilities matrix |
12
+ | [../examples/demo](../examples/demo) | Runnable multi-tab browser demo |
13
+ | [../CHANGELOG.md](../CHANGELOG.md) | Version changelog |
14
+
15
+ ## Reading Order
16
+
17
+ 1. For first-time setup, read [Getting Started](./getting-started.md).
18
+ 2. For production configuration, read [Configuration](./configuration.md).
19
+ 3. When developing wrappers or custom transports, read [API Reference](./api.md).
20
+ 4. When troubleshooting cross-tab behavior, read [Architecture](./architecture.md).
21
+ 5. When evaluating current boundaries and future plans, read [Capabilities Matrix](./capabilities.md).
package/docs/api.md ADDED
@@ -0,0 +1,261 @@
1
+ > [中文](./zh/api.md) | English
2
+
3
+ # API Reference
4
+
5
+ ## Package Entry Points
6
+
7
+ ```ts
8
+ import {
9
+ CrossTabDataBus,
10
+ type DataBusTraceEvent,
11
+ WorkerClusterRuntime,
12
+ createBrowserEnvironment,
13
+ createOpaqueKey,
14
+ selectWorkerBackend,
15
+ type WorkerMode
16
+ } from 'cross-tab-worker-databus';
17
+
18
+ import {
19
+ CentrifugeWorkerTransport,
20
+ createCentrifugeDataBus
21
+ } from 'cross-tab-worker-databus/centrifuge';
22
+ ```
23
+
24
+ Business integration should prefer `CrossTabDataBus` or `createCentrifugeDataBus`. `WorkerClusterRuntime` is an advanced coordination API.
25
+
26
+ ## `CrossTabDataBus<TConfig, TData>`
27
+
28
+ ### constructor
29
+
30
+ ```ts
31
+ new CrossTabDataBus<TConfig, TData>(options)
32
+ ```
33
+
34
+ Creates a DataBus. When `initialConfig` is provided, it starts automatically by default.
35
+
36
+ ### `start(config)`
37
+
38
+ ```ts
39
+ start(config: TConfig): Promise<void>
40
+ ```
41
+
42
+ Starts cluster coordination and transport. The first call actually starts the transport; concurrent calls during startup share the same start Promise without creating a duplicate transport. After startup succeeds or fails, the internal gate resets: subsequent calls are no-ops on an already-started instance (immediately resolve) and do not restart; after `stop()`, it can be called again to restart.
43
+
44
+ ### `ready()`
45
+
46
+ ```ts
47
+ ready(): Promise<void>
48
+ ```
49
+
50
+ Waits for the current transport's `start` to complete. The Promise rejects when auto-start fails; calling again can trigger a retry based on `initialConfig`.
51
+
52
+ When no `initialConfig` is provided and `start(config)` has not been called, `ready()` returns a rejected Promise instead of throwing synchronously, so callers can attach `.catch` and decide whether to start explicitly.
53
+
54
+ `ready()` is not equivalent to the server being connected; protocol connection status is obtained via `onStatus`.
55
+
56
+ ### `subscribe(topic, handler)`
57
+
58
+ ```ts
59
+ subscribe(
60
+ topic: string,
61
+ handler: DataBusMessageHandler<TData>
62
+ ): () => void
63
+ ```
64
+
65
+ Registers a local subscription and returns a cleanup function.
66
+
67
+ - Multiple handlers for the same topic use reference counting.
68
+ - The first handler in the current tab registers a cluster subscription.
69
+ - The current tab only leaves the topic after the last handler is released.
70
+ - Subscriptions are automatically queued when the transport is not yet ready.
71
+
72
+ ### `unsubscribe(topic, handler?)`
73
+
74
+ ```ts
75
+ unsubscribe(topic: string, handler?: DataBusMessageHandler<TData>): void
76
+ ```
77
+
78
+ When a handler is provided, only that callback is released; when omitted, all handlers for the topic on the current instance are released.
79
+
80
+ Prefer using the cleanup function returned by `subscribe` to avoid accidentally removing callbacks from other modules.
81
+
82
+ ### `publish(topic, data)`
83
+
84
+ ```ts
85
+ publish(topic: string, data: unknown): void
86
+ ```
87
+
88
+ Routes the publish operation to the current topic owner; uses the current Worker when no valid route exists.
89
+
90
+ Published data must satisfy the serialization constraints of the underlying transport. The SDK does not persist or defer replay of publish commands during page suspension.
91
+
92
+ When the owning Worker is a remote Tab and the publish control message cannot be posted (for example the BroadcastChannel fails to clone the payload), `publish()` reports the failure through `onError` instead of silently dropping it.
93
+
94
+ ### `onStatus(handler)`
95
+
96
+ ```ts
97
+ onStatus(handler: DataBusStatusHandler): () => void
98
+ ```
99
+
100
+ Listens for transport status. Receives the current status immediately upon registration.
101
+
102
+ ### `onError(handler)`
103
+
104
+ ```ts
105
+ onError(handler: DataBusErrorHandler): () => void
106
+ ```
107
+
108
+ Listens for errors in startup, subscription, unsubscription, publishing, and Worker operations.
109
+
110
+ ### `getStatus()`
111
+
112
+ ```ts
113
+ getStatus(): WorkerStatus
114
+ ```
115
+
116
+ Returns the current status: `connecting`, `connected`, `disconnected`, or `error`.
117
+
118
+ ### `getClusterSnapshot()`
119
+
120
+ Returns a diagnostic snapshot:
121
+
122
+ ```ts
123
+ interface WorkerClusterSnapshot {
124
+ coordinated: boolean;
125
+ suspended: boolean;
126
+ currentWorker: WorkerRecord;
127
+ workers: WorkerRecord[];
128
+ /** Routes with the plaintext topic injected from the in-memory knownTopics cache. */
129
+ routes: Array<WorkerRoute & { topic: string | null }>;
130
+ subscribedTopics: string[];
131
+ assignedTopics: string[];
132
+ /** Opaque key → plaintext topic mapping for debugging. */
133
+ knownTopics: Array<{ topicKey: string; topic: string }>;
134
+ }
135
+ ```
136
+
137
+ The snapshot is intended for diagnostics and testing, and should not be used as a business state source.
138
+
139
+ Use `console.table(snapshot.routes)` to inspect all routes with their plaintext topics, or `snapshot.knownTopics` to correlate opaque keys with topics.
140
+
141
+ ### `trace`
142
+
143
+ Enables optional diagnostics via construction config:
144
+
145
+ ```ts
146
+ trace: {
147
+ enabled: true,
148
+ mode: 'all',
149
+ metricsIntervalMs: 5000,
150
+ sink: (event: DataBusTraceEvent) => report(event)
151
+ }
152
+ ```
153
+
154
+ Low-frequency event types include `lifecycle`, `status`, `subscription`, `coordination`, and `error`; high-frequency data is output as `message_metrics` per window, containing receive/dispatch counts, active topic count, and dispatch latency aggregates (`dispatchSamples`, `dispatchAvgMs`, `dispatchP50Ms`, `dispatchP95Ms`, `dispatchMaxMs`). All public events use a fixed structure and do not contain raw topics, message payloads, connection addresses, or error bodies. Errors thrown by the sink are isolated and do not interrupt message dispatch, but are output to `console.warn` to facilitate diagnosing configuration issues. The sink should ideally not throw — capture expected error conditions in the event data rather than raising exceptions.
155
+
156
+ ### `stop()`
157
+
158
+ ```ts
159
+ stop(): Promise<void>
160
+ ```
161
+
162
+ Permanently destroys the current instance: cleans up handlers, cluster registration, routes, Workers, and transport. Normal page hide and restore do not require calling this method.
163
+
164
+ ## `DataBusTransport<TConfig, TData>`
165
+
166
+ ```ts
167
+ interface DataBusTransport<TConfig, TData> {
168
+ start(config, handlers): void | Promise<void>;
169
+ subscribe(topic): void | Promise<void>;
170
+ unsubscribe(topic): void | Promise<void>;
171
+ publish(topic, data): void | Promise<void>;
172
+ stop(): void | Promise<void>;
173
+ }
174
+ ```
175
+
176
+ Implementation requirements:
177
+
178
+ - `subscribe` and `unsubscribe` must be idempotent.
179
+ - After `stop`, it must allow `start` to be called again, for BFCache restoration.
180
+ - When data is received, call `handlers.onMessage({ topic, data })`.
181
+ - When status changes, call `handlers.onStatus(status)`.
182
+ - Asynchronous errors are reported via rejection or `handlers.onError(error)`.
183
+
184
+ ## `createCentrifugeDataBus<TData>(options)`
185
+
186
+ ```ts
187
+ createCentrifugeDataBus<TData>(options): CrossTabDataBus<CentrifugeDataBusConfig, TData>
188
+ ```
189
+
190
+ Creates an auto-starting Centrifuge DataBus. Defaults:
191
+
192
+ - `clusterKey = connection.url`
193
+ - `workerMode = 'dedicated'`, each tab uses an independent Dedicated Worker
194
+ - Uses the bundled `centrifuge.worker.js`
195
+ - Worker name is `cross-tab-worker-databus`
196
+
197
+ SharedWorker mode uses the bundled `centrifuge.shared.worker.js`. With `workerMode: 'auto'`, it degrades from SharedWorker to Dedicated Worker to local mode. See [configuration.md](./configuration.md) for full configuration.
198
+
199
+ ## `CentrifugeWorkerTransport<TData>`
200
+
201
+ Low-level Centrifuge transport. Only create directly when custom DataBus assembly is needed:
202
+
203
+ ```ts
204
+ const transport = new CentrifugeWorkerTransport({
205
+ workerMode: 'auto',
206
+ workerFactory: () => new Worker(customWorkerUrl, { type: 'module' }),
207
+ sharedWorkerFactory: () => new SharedWorker(customSharedWorkerUrl, { type: 'module' })
208
+ });
209
+ ```
210
+
211
+ Available options:
212
+
213
+ - `workerMode`: `'dedicated'` (default), `'shared'`, or `'auto'`; the `auto` degradation chain is SharedWorker -> Dedicated Worker -> local mode
214
+ - `transferable`: `boolean`, default `false`; when enabled, ArrayBuffer payloads use Transferable transport, while the object message API remains unchanged
215
+ - `heartbeatIntervalMs`: `number`, default `10000`; SharedWorker PING heartbeat interval in ms. `Infinity` disables heartbeats. Must be a positive number or `Infinity` — `0`, a negative number, or `NaN` throws a `TypeError` in the transport constructor. See [configuration](./configuration.md#sharedworker-session-reaper) for details
216
+ - `workerFactory`: custom Dedicated Worker loading method
217
+ - `sharedWorkerFactory`: custom SharedWorker loading method
218
+
219
+ ## `WorkerClusterRuntime`
220
+
221
+ Advanced API responsible for Worker registration, heartbeat, visibility, routing, BroadcastChannel protocol, and migration. Business modules should not operate on it directly.
222
+
223
+ Main methods:
224
+
225
+ - `start()` / `stop()`
226
+ - `setStatus(status)`
227
+ - `subscribe(topic)` / `unsubscribe(topic)`
228
+ - `publish(topic, data)`
229
+ - `broadcastEvent(eventType, payload)`
230
+ - `isAssigned(topic)`
231
+ - `isActiveWorker()`
232
+ - `hasLocalSubscriber(topic)`
233
+ - `getSnapshot()`
234
+
235
+ ## Utility Functions
236
+
237
+ ### `createOpaqueKey(value)`
238
+
239
+ Generates a stable 128-bit hexadecimal opaque key. Used to avoid writing raw connection or topic text into coordination metadata; it is not a cryptographic digest and should not be used for password storage or security signing.
240
+
241
+ ### `createBrowserEnvironment()`
242
+
243
+ Creates a default browser environment adapter, including storage, BroadcastChannel, timers, and page lifecycle events.
244
+
245
+ ### `selectWorkerBackend(mode, availability?)`
246
+
247
+ Selects the actual backend based on `WorkerMode` and capability detection, returning `'shared' | 'dedicated' | 'local'`:
248
+
249
+ - `shared` / `auto`: SharedWorker -> Dedicated Worker -> local mode
250
+ - `dedicated` (default): Dedicated Worker -> SharedWorker -> local mode
251
+
252
+ `availability` can explicitly pass `worker` / `sharedWorker` capability flags, for use in SSR, testing, or embedded environments, avoiding access to non-existent global objects.
253
+
254
+ ### Routing Functions
255
+
256
+ - `selectActiveWorkers`
257
+ - `selectLeastLoadedWorker`
258
+ - `selectRebalanceTarget`
259
+ - `hasActiveOwner`
260
+
261
+ These pure functions are primarily used for testing, diagnostics, and custom coordination strategies.