cross-tab-worker-databus 0.20.71 → 0.20.85
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/CHANGELOG.md +204 -2
- package/README.md +8 -0
- package/README.zh.md +4 -0
- package/dist/centrifuge-protocol.d.ts +59 -26
- package/dist/centrifuge-protocol.d.ts.map +1 -1
- package/dist/centrifuge-session.d.ts +9 -0
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.d.ts +12 -2
- package/dist/centrifuge.d.ts.map +1 -1
- package/dist/centrifuge.js +168 -88
- package/dist/centrifuge.js.map +3 -3
- package/dist/centrifuge.shared.worker.js +146 -38
- package/dist/centrifuge.shared.worker.js.map +3 -3
- package/dist/centrifuge.worker.js +140 -33
- package/dist/centrifuge.worker.js.map +3 -3
- package/dist/{chunk-D2SIT473.js → chunk-PW63EWIK.js} +1090 -494
- package/dist/chunk-PW63EWIK.js.map +7 -0
- package/dist/chunk-TZ7ZP7YD.js +175 -0
- package/dist/chunk-TZ7ZP7YD.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +1403 -637
- package/dist/cjs/centrifuge.cjs.map +4 -4
- package/dist/cjs/hooks.cjs +37 -2
- package/dist/cjs/hooks.cjs.map +3 -3
- package/dist/cjs/index.cjs +1453 -730
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/vue.cjs +45 -2
- package/dist/cjs/vue.cjs.map +3 -3
- package/dist/core/cluster.d.ts +40 -4
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +93 -69
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/dedup-manager.d.ts +98 -0
- package/dist/core/dedup-manager.d.ts.map +1 -0
- package/dist/core/environment.d.ts +43 -3
- package/dist/core/environment.d.ts.map +1 -1
- package/dist/core/replay-manager.d.ts +128 -0
- package/dist/core/replay-manager.d.ts.map +1 -0
- package/dist/core/replay-persistence.d.ts +2 -1
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/routing.d.ts +22 -3
- package/dist/core/routing.d.ts.map +1 -1
- package/dist/core/storage-batch.d.ts +0 -4
- package/dist/core/storage-batch.d.ts.map +1 -1
- package/dist/core/trace.d.ts +55 -15
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/types.d.ts +58 -9
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/hooks.d.ts +12 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +28 -2
- package/dist/hooks.js.map +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +228 -162
- package/dist/index.js.map +3 -3
- package/dist/utils/constants.d.ts +170 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/error-utils.d.ts +28 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/metadata.d.ts +16 -0
- package/dist/utils/metadata.d.ts.map +1 -0
- package/dist/utils/storage-utils.d.ts +24 -0
- package/dist/utils/storage-utils.d.ts.map +1 -0
- package/dist/utils/validation.d.ts +76 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/vue.d.ts +13 -1
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +36 -2
- package/dist/vue.js.map +2 -2
- package/dist/websocket.d.ts +6 -1
- package/dist/websocket.d.ts.map +1 -1
- package/dist/worker-mode.d.ts +6 -2
- package/dist/worker-mode.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/api.md +122 -2
- package/docs/architecture.md +80 -0
- package/docs/benchmarks.md +24 -0
- package/docs/capabilities.md +24 -3
- package/docs/configuration.md +58 -0
- package/docs/getting-started.md +35 -0
- package/docs/release-checklist.md +28 -5
- package/docs/roadmap.md +74 -5
- package/docs/zh/README.md +2 -1
- package/docs/zh/api.md +121 -2
- package/docs/zh/architecture.md +49 -0
- package/docs/zh/benchmarks.md +24 -0
- package/docs/zh/capabilities.md +21 -3
- package/docs/zh/configuration.md +53 -0
- package/docs/zh/getting-started.md +35 -0
- package/docs/zh/release-checklist.md +29 -6
- package/docs/zh/roadmap.md +86 -5
- package/package.json +39 -16
- package/dist/chunk-D2SIT473.js.map +0 -7
package/dist/vue.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/vue.ts"],
|
|
4
|
-
"sourcesContent": ["/** Vue 3 composables adapter for cross-tab-worker-databus.\n * Vue is an optional peer dependency; this module is a separate entry point.\n */\nimport { onBeforeUnmount, onMounted, ref, shallowRef, watch, type Ref } from 'vue';\nimport type { CrossTabDataBus } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\n\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: ReadonlyArray<Ref<unknown> | (() => unknown)> = []\n): Ref<CrossTabDataBus<TConfig, TData> | null> {\n const bus = shallowRef<CrossTabDataBus<TConfig, TData> | null>(null);\n let instance: CrossTabDataBus<TConfig, TData> | null = null;\n let lifecycleGeneration = 0;\n const stop = async () => { const current = instance; instance = null; bus.value = null; if (current) await current.stop(); };\n const start = () => {\n const generation = ++lifecycleGeneration;\n void stop().then(() => {\n if (generation !== lifecycleGeneration) return;\n const next = create();\n instance = next;\n bus.value = next;\n void next.ready().catch(() => {});\n });\n };\n onMounted(start);\n onBeforeUnmount(() => { void stop()
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/** Vue 3 composables adapter for cross-tab-worker-databus.\n * Vue is an optional peer dependency; this module is a separate entry point.\n */\nimport { onBeforeUnmount, onMounted, ref, shallowRef, watch, type Ref } from 'vue';\nimport type { CrossTabDataBus, DataBusHealthSummary } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\nimport { WORKER_STATUS } from './utils/constants';\n\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: ReadonlyArray<Ref<unknown> | (() => unknown)> = []\n): Ref<CrossTabDataBus<TConfig, TData> | null> {\n const bus = shallowRef<CrossTabDataBus<TConfig, TData> | null>(null);\n let instance: CrossTabDataBus<TConfig, TData> | null = null;\n let lifecycleGeneration = 0;\n const stop = async () => { const current = instance; instance = null; bus.value = null; if (current) await current.stop(); };\n const start = () => {\n const generation = ++lifecycleGeneration;\n void stop().then(() => {\n if (generation !== lifecycleGeneration) return;\n const next = create();\n instance = next;\n bus.value = next;\n void next.ready().catch(() => {});\n });\n };\n onMounted(start);\n onBeforeUnmount(() => {\n // Bump the generation so a start() still awaiting its stop() sees itself\n // superseded. Without this the pending continuation would run create()\n // after the component is gone, leaving a live bus with no owner to stop\n // it (the React adapter has no such window: its create() is synchronous\n // inside useEffect).\n lifecycleGeneration += 1;\n void stop();\n });\n if (deps.length > 0) watch(deps, start);\n return bus as Ref<CrossTabDataBus<TConfig, TData> | null>;\n}\n\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>, topic: Ref<string> | string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n let currentBus: CrossTabDataBus<TConfig, TData> | null = null;\n let currentTopic: string | null = null;\n let cleanup: (() => void) | undefined;\n let latestHandler = handler;\n const stop = () => { cleanup?.(); cleanup = undefined; currentBus = null; currentTopic = null; };\n const sync = () => {\n const nextBus = bus.value;\n const nextTopic = typeof topic === 'string' ? topic : topic.value;\n if (nextBus === currentBus && currentTopic === nextTopic && cleanup) return;\n stop();\n if (!nextBus) return;\n currentBus = nextBus;\n currentTopic = nextTopic;\n cleanup = nextBus.subscribe(nextTopic, message => latestHandler(message));\n };\n watch(bus, sync, { immediate: true });\n if (typeof topic !== 'string') watch(topic, sync);\n watch(() => handler, value => { latestHandler = value; });\n onBeforeUnmount(stop);\n}\n\nexport function useCrossTabStatus<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>\n): Ref<WorkerStatus> {\n const status = ref<WorkerStatus>(WORKER_STATUS.CONNECTING);\n let cleanup: (() => void) | undefined;\n watch(bus, next => {\n cleanup?.(); cleanup = undefined;\n status.value = next?.getStatus() ?? WORKER_STATUS.CONNECTING;\n if (next) cleanup = next.onStatus(value => { status.value = value; });\n }, { immediate: true });\n onBeforeUnmount(() => cleanup?.());\n return status;\n}\n\n/** Options for {@link useCrossTabHealth}. */\nexport interface UseCrossTabHealthOptions {\n /** Polling cadence in ms for the health snapshot. Default 1000; `0` disables\n * polling and relies on status/error events only. */\n intervalMs?: number;\n}\n\n/**\n * Mirror the bus health summary into a Vue ref. `getHealthSummary()` is a\n * snapshot, so the composable polls it on an interval (default 1000 ms) and\n * refreshes on status changes and errors. Returns `null` until the bus exists.\n */\nexport function useCrossTabHealth<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>,\n options?: UseCrossTabHealthOptions\n): Ref<DataBusHealthSummary | null> {\n const health = ref<DataBusHealthSummary | null>(null);\n let timer: ReturnType<typeof setInterval> | null = null;\n let cleanups: Array<() => void> = [];\n const teardown = () => {\n if (timer) clearInterval(timer);\n timer = null;\n for (const cleanup of cleanups) cleanup();\n cleanups = [];\n };\n watch(bus, next => {\n teardown();\n if (!next) {\n health.value = null;\n return;\n }\n const refresh = () => { health.value = next.getHealthSummary(); };\n refresh();\n cleanups.push(next.onStatus(refresh));\n cleanups.push(next.onError(refresh));\n const intervalMs = options?.intervalMs ?? 1_000;\n if (intervalMs > 0) timer = setInterval(refresh, intervalMs);\n }, { immediate: true });\n onBeforeUnmount(teardown);\n return health as Ref<DataBusHealthSummary | null>;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;AAGA,SAAS,iBAAiB,WAAW,KAAK,YAAY,aAAuB;AAKtE,SAAS,mBACd,QACA,OAAsD,CAAC,GACV;AAC7C,QAAM,MAAM,WAAmD,IAAI;AACnE,MAAI,WAAmD;AACvD,MAAI,sBAAsB;AAC1B,QAAM,OAAO,YAAY;AAAE,UAAM,UAAU;AAAU,eAAW;AAAM,QAAI,QAAQ;AAAM,QAAI,QAAS,OAAM,QAAQ,KAAK;AAAA,EAAG;AAC3H,QAAM,QAAQ,MAAM;AAClB,UAAM,aAAa,EAAE;AACrB,SAAK,KAAK,EAAE,KAAK,MAAM;AACrB,UAAI,eAAe,oBAAqB;AACxC,YAAM,OAAO,OAAO;AACpB,iBAAW;AACX,UAAI,QAAQ;AACZ,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAClC,CAAC;AAAA,EACH;AACA,YAAU,KAAK;AACf,kBAAgB,MAAM;AAMpB,2BAAuB;AACvB,SAAK,KAAK;AAAA,EACZ,CAAC;AACD,MAAI,KAAK,SAAS,EAAG,OAAM,MAAM,KAAK;AACtC,SAAO;AACT;AAEO,SAAS,wBACd,KAAkD,OAClD,SACM;AACN,MAAI,aAAqD;AACzD,MAAI,eAA8B;AAClC,MAAI;AACJ,MAAI,gBAAgB;AACpB,QAAM,OAAO,MAAM;AAAE,cAAU;AAAG,cAAU;AAAW,iBAAa;AAAM,mBAAe;AAAA,EAAM;AAC/F,QAAM,OAAO,MAAM;AACjB,UAAM,UAAU,IAAI;AACpB,UAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC5D,QAAI,YAAY,cAAc,iBAAiB,aAAa,QAAS;AACrE,SAAK;AACL,QAAI,CAAC,QAAS;AACd,iBAAa;AACb,mBAAe;AACf,cAAU,QAAQ,UAAU,WAAW,aAAW,cAAc,OAAO,CAAC;AAAA,EAC1E;AACA,QAAM,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC;AACpC,MAAI,OAAO,UAAU,SAAU,OAAM,OAAO,IAAI;AAChD,QAAM,MAAM,SAAS,WAAS;AAAE,oBAAgB;AAAA,EAAO,CAAC;AACxD,kBAAgB,IAAI;AACtB;AAEO,SAAS,kBACd,KACmB;AACnB,QAAM,SAAS,IAAkB,cAAc,UAAU;AACzD,MAAI;AACJ,QAAM,KAAK,UAAQ;AACjB,cAAU;AAAG,cAAU;AACvB,WAAO,QAAQ,MAAM,UAAU,KAAK,cAAc;AAClD,QAAI,KAAM,WAAU,KAAK,SAAS,WAAS;AAAE,aAAO,QAAQ;AAAA,IAAO,CAAC;AAAA,EACtE,GAAG,EAAE,WAAW,KAAK,CAAC;AACtB,kBAAgB,MAAM,UAAU,CAAC;AACjC,SAAO;AACT;AAcO,SAAS,kBACd,KACA,SACkC;AAClC,QAAM,SAAS,IAAiC,IAAI;AACpD,MAAI,QAA+C;AACnD,MAAI,WAA8B,CAAC;AACnC,QAAM,WAAW,MAAM;AACrB,QAAI,MAAO,eAAc,KAAK;AAC9B,YAAQ;AACR,eAAW,WAAW,SAAU,SAAQ;AACxC,eAAW,CAAC;AAAA,EACd;AACA,QAAM,KAAK,UAAQ;AACjB,aAAS;AACT,QAAI,CAAC,MAAM;AACT,aAAO,QAAQ;AACf;AAAA,IACF;AACA,UAAM,UAAU,MAAM;AAAE,aAAO,QAAQ,KAAK,iBAAiB;AAAA,IAAG;AAChE,YAAQ;AACR,aAAS,KAAK,KAAK,SAAS,OAAO,CAAC;AACpC,aAAS,KAAK,KAAK,QAAQ,OAAO,CAAC;AACnC,UAAM,aAAa,SAAS,cAAc;AAC1C,QAAI,aAAa,EAAG,SAAQ,YAAY,SAAS,UAAU;AAAA,EAC7D,GAAG,EAAE,WAAW,KAAK,CAAC;AACtB,kBAAgB,QAAQ;AACxB,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/websocket.d.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Wire protocol (JSON text frames):
|
|
10
10
|
* - client → server: `{"op":"subscribe"|"unsubscribe"|"publish","topic":...,"data":...}`
|
|
11
|
+
* - client → server (batched): `{"op":"publishBatch","topic":...,"items":[{data,...}]}`
|
|
11
12
|
* - server → client: `{"topic":...,"data":...}` for publications; anything
|
|
12
13
|
* without a string `topic` field is ignored (forward-compatible).
|
|
13
14
|
*/
|
|
14
15
|
import { CrossTabDataBus } from './core/data-bus';
|
|
15
16
|
import type { CrossTabDataBusOptions } from './core/data-bus';
|
|
16
|
-
import type { DataBusTransport, DataBusTransportHandlers, DataBusPublishOptions, MaybePromise, WorkerStatus } from './core/types';
|
|
17
|
+
import type { DataBusTransport, DataBusTransportHandlers, DataBusPublishOptions, DataBusPublicationItem, MaybePromise, WorkerStatus } from './core/types';
|
|
17
18
|
/** Minimal WebSocket surface used by the transport. Matches the browser
|
|
18
19
|
* `WebSocket` subset the transport touches; injectable for tests and runtimes. */
|
|
19
20
|
export interface WebSocketLike {
|
|
@@ -68,6 +69,10 @@ export declare class WebSocketTransport<TData = unknown> implements DataBusTrans
|
|
|
68
69
|
unsubscribe(topic: string): MaybePromise<void>;
|
|
69
70
|
/** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
|
|
70
71
|
publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void>;
|
|
72
|
+
/** Publish many items for one topic as a single wire frame. One-item
|
|
73
|
+
* batches delegate to `publish` so the legacy single-publication frame
|
|
74
|
+
* shape (including binary framing) is preserved. */
|
|
75
|
+
publishBatch(topic: string, items: ReadonlyArray<DataBusPublicationItem>): MaybePromise<void>;
|
|
71
76
|
/** Close the socket and drop all state. Safe to call multiple times. */
|
|
72
77
|
stop(): MaybePromise<void>;
|
|
73
78
|
/** Send one JSON frame. Frames are dropped with an `onError` report when
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,OAAO,KAAK,EACV,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EAEtB,YAAY,EACZ,YAAY,EACb,MAAM,cAAc,CAAC;AAEtB;kFACkF;AAClF,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED,+DAA+D;AAC/D,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B;6CACyC;IACzC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,aAAa,CAAC;CAClF;AAED,0FAA0F;AAC1F,MAAM,WAAW,6BAA6B,CAAC,KAAK,GAAG,OAAO,CAC5D,SAAQ,IAAI,CACV,sBAAsB,CAAC,sBAAsB,EAAE,KAAK,CAAC,EACrD,WAAW,GAAG,YAAY,GAAG,eAAe,GAAG,WAAW,CAC3D;IACD,0CAA0C;IAC1C,UAAU,EAAE,sBAAsB,CAAC;IACnC,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;;qEAIqE;AACrE,qBAAa,kBAAkB,CAAC,KAAK,GAAG,OAAO,CAC7C,YAAW,gBAAgB,CAAC,sBAAsB,EAAE,KAAK,CAAC;IAQ9C,OAAO,CAAC,QAAQ,CAAC,UAAU;IANvC,QAAQ,CAAC,eAAe,eAAe;IACvC,QAAQ,CAAC,kBAAkB,sBAAsB;IACjD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,QAAQ,CAAgD;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;gBAEzB,UAAU,EAAE,sBAAsB;IAE/D;0EACsE;IACtE,KAAK,CAAC,MAAM,EAAE,sBAAsB,EAAE,QAAQ,EAAE,wBAAwB,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC;IAoCpG;iDAC6C;IAC7C,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC;IAK5C,6DAA6D;IAC7D,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC;IAK9C,0EAA0E;IAC1E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,YAAY,CAAC,IAAI,CAAC;IAc1F;;wDAEoD;IACpD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,sBAAsB,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC;IA0B7F,wEAAwE;IACxE,IAAI,IAAI,YAAY,CAAC,IAAI,CAAC;IAQ1B;;iEAE6D;IAC7D,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,eAAe;IA8BvB;;+CAE2C;YAC7B,aAAa;CAkC5B;AAUD;;iEAEiE;AACjE,wBAAgB,sBAAsB,CAAC,KAAK,GAAG,OAAO,EACpD,OAAO,EAAE,6BAA6B,CAAC,KAAK,CAAC,GAC5C,eAAe,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAShD;AAED,wEAAwE;AACxE,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
package/dist/worker-mode.d.ts
CHANGED
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* Defines the WorkerMode preference flags and the selectWorkerBackend function
|
|
5
5
|
* that resolves the preference against browser capability to pick the actual
|
|
6
6
|
* backend (dedicated, shared, or local fallback).
|
|
7
|
+
*
|
|
8
|
+
* The literal values are derived from `utils/constants.ts` so the preference and
|
|
9
|
+
* resolved-backend strings referenced across the transport stay in one place.
|
|
7
10
|
*/
|
|
11
|
+
import { WORKER_BACKEND, WORKER_MODE } from './utils/constants';
|
|
8
12
|
/** Preferred Worker mode.
|
|
9
13
|
* - `dedicated` → try Dedicated Worker first (one WebSocket per tab).
|
|
10
14
|
* - `shared` → try SharedWorker first (one process, per-port connections).
|
|
11
15
|
* - `auto` → same as `shared` (alias for forward compatibility). */
|
|
12
|
-
export type WorkerMode =
|
|
16
|
+
export type WorkerMode = (typeof WORKER_MODE)[keyof typeof WORKER_MODE];
|
|
13
17
|
/** Resolved backend that was actually created. `local` means the session
|
|
14
18
|
* runs on the main thread (fallback when no Worker API is available). */
|
|
15
|
-
export type WorkerBackend =
|
|
19
|
+
export type WorkerBackend = (typeof WORKER_BACKEND)[keyof typeof WORKER_BACKEND];
|
|
16
20
|
/** Override Worker availability for testing or environments where feature
|
|
17
21
|
* detection is unreliable (e.g. sandboxed iframes). When a field is omitted,
|
|
18
22
|
* the global `typeof Worker` / `typeof SharedWorker` check is used. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-mode.d.ts","sourceRoot":"","sources":["../src/worker-mode.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"worker-mode.d.ts","sourceRoot":"","sources":["../src/worker-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhE;;;oEAGoE;AACpE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAExE;yEACyE;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,CAAC;AAEjF;;uEAEuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,YAAY,GAAE,kBAAuB,GACpC,aAAa,CAOf"}
|
package/docs/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [Architecture](./architecture.md) | Worker cluster, routing, storage, migration, and degradation design |
|
|
12
12
|
| [Capabilities Matrix](./capabilities.md) | Implemented, not implemented, and planned capabilities matrix |
|
|
13
13
|
| [Roadmap](./roadmap.md) | Release-oriented priorities and verification checklist |
|
|
14
|
+
| [Benchmark trend](./benchmarks.md) | Auto-generated browser benchmark history from `bench-results/` |
|
|
14
15
|
| [Release checklist](./release-checklist.md) | Local gates, tagging, publishing, and post-release verification |
|
|
15
16
|
| [../examples/demo](../examples/demo) | Runnable multi-tab browser demo |
|
|
16
17
|
| [../CHANGELOG.md](../CHANGELOG.md) | Version changelog |
|
package/docs/api.md
CHANGED
|
@@ -86,7 +86,7 @@ Registers a local subscription and returns a cleanup function.
|
|
|
86
86
|
- The current tab only leaves the topic after the last handler is released.
|
|
87
87
|
- Subscriptions are automatically queued when the transport is not yet ready.
|
|
88
88
|
- Wildcard subscriptions: a topic ending in `.*` (`chat.*`) matches any remainder, and `*` matches everything. The pattern is routed, owned, and transport-subscribed as a literal channel; publications tagged with a matching concrete topic (or with the pattern itself) are delivered to wildcard handlers. See `topicMatchesPattern` below.
|
|
89
|
-
- Replay (opt-in): construct the bus with `replay: { maxPerTopic }` and pass `{ replay: true | n }` as the third `subscribe()` argument. `maxPerTopic` must be a positive safe integer. The new handler immediately receives the buffered history (up to `n`, capped by `maxPerTopic`, default 100) with `message.replayed: true`, so late joiners do not miss earlier publications. Only dispatched publications are buffered (a topic with no local subscriber drops them as unowned); buffers are in-memory and cleared when the last handler for the topic unsubscribes. Wildcard subscriptions replay across every buffered topic matching the pattern. For reload/BFCache persistence, pass an optional `persistence` created by `createIndexedDbReplayPersistence({ maxPerTopic })`; persistence is asynchronous and failures are reported through `onError` without breaking live delivery. Set `retentionMs` to automatically prune durable history via `clearBefore` during hydration and after appends. Set `persistenceRetry: { maxAttempts, backoffMs }` to retry transient persistence failures; defaults preserve one-attempt behavior.
|
|
89
|
+
- Replay (opt-in): construct the bus with `replay: { maxPerTopic }` and pass `{ replay: true | n }` as the third `subscribe()` argument. `maxPerTopic` must be a positive safe integer. The new handler immediately receives the buffered history (up to `n`, capped by `maxPerTopic`, default 100) with `message.replayed: true`, so late joiners do not miss earlier publications. Only dispatched publications are buffered (a topic with no local subscriber drops them as unowned); buffers are in-memory and cleared when the last handler for the topic unsubscribes. Wildcard subscriptions replay across every buffered topic matching the pattern. For reload/BFCache persistence, pass an optional `persistence` created by `createIndexedDbReplayPersistence({ maxPerTopic })`; persistence is asynchronous and failures are reported through `onError` without breaking live delivery. Set `retentionMs` to automatically prune durable history via `clearBefore` during hydration and after appends. Set `persistenceRetry: { maxAttempts, backoffMs }` to retry transient persistence failures; defaults preserve one-attempt behavior. Set `pruneStrategy` to `'count'` (default), `'age'`, or `'both'` to cap by `maxPerTopic`, prune by `retentionMs`, or apply both.
|
|
90
90
|
When tracing is enabled, retries emit `reliability` events with `operation: 'persistence_retry'`, a bounded `persistenceOperation`, and `attempt`.
|
|
91
91
|
|
|
92
92
|
The WebSocket transport accepts binary publications delivered as either `ArrayBuffer` or browser `Blob` frames.
|
|
@@ -132,6 +132,19 @@ When supplied, `options.messageId` and `options.timestamp` are propagated throug
|
|
|
132
132
|
}
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
### `publishBatch(topic, items)`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
publishBatch(
|
|
139
|
+
topic: string,
|
|
140
|
+
items: Array<{ data: unknown; messageId?: string; timestamp?: number }>
|
|
141
|
+
): void
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Publishes many items to one topic as a single unit of work. The bundled WebSocket transport packs the whole batch into one wire frame (the `publishBatch` op) rather than one frame per item; a transport that does not implement the optional `DataBusTransport.publishBatch` hook falls back to per-item `publish()`, so the call is safe either way.
|
|
145
|
+
|
|
146
|
+
Per-item `messageId` and `timestamp` survive the wire frame, and dedup, replay, and ordering apply per item in source order. An empty batch is a no-op; a single-item batch delegates to `publish()`. `WorkerClusterRuntime` exposes the same method for callers that coordinate directly.
|
|
147
|
+
|
|
135
148
|
### `clearReplay()`
|
|
136
149
|
|
|
137
150
|
```ts
|
|
@@ -172,6 +185,65 @@ getStatus(): WorkerStatus
|
|
|
172
185
|
|
|
173
186
|
Returns the current status: `connecting`, `connected`, `disconnected`, or `error`.
|
|
174
187
|
|
|
188
|
+
### `getHealthSummary()`
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
getHealthSummary(): DataBusHealthSummary
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Compact readiness verdict for dashboards, readiness probes, and support bundles. Answers "is the bus usable right now" first, then attaches the failure and recovery context that explains the verdict:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
interface DataBusHealthSummary {
|
|
198
|
+
healthy: boolean; // started, not suspended, transport ready
|
|
199
|
+
state: 'stopped' | 'starting' | 'healthy' | 'recovering' | 'suspended' | 'degraded';
|
|
200
|
+
status: WorkerStatus;
|
|
201
|
+
sdkVersion: string;
|
|
202
|
+
started: boolean;
|
|
203
|
+
suspended: boolean; // true while the tab is hidden (BFCache)
|
|
204
|
+
transport: { name; backend; ready; status };
|
|
205
|
+
recovery: { attempt; exhausted; maxAttempts; generation; lastSuccessAt; hasError; errorMessage; errorAt };
|
|
206
|
+
lastFailure: { source: 'transport' | 'persistence' | 'dispatch'; message: string; at: number } | null;
|
|
207
|
+
persistence: { failures: number; lastFailureAt: number | null; lastErrorMessage: string | null };
|
|
208
|
+
metrics: DataBusMetricsSnapshot | null; // live trace window, or null when trace metrics are inactive
|
|
209
|
+
trace: { asyncSink: boolean; pendingEvents: number }; // sink back-pressure visibility
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`state` semantics: `stopped` (not started), `starting` (initial open in flight), `recovering` (automatic transport recovery in progress), `suspended` (tab hidden, resumes on pageshow), `degraded` (automatic recovery exhausted — call `start()` or subscribe again to recover manually), `healthy`. `lastFailure` is a unified ledger across all failure sources and resets on every explicit `start()`.
|
|
214
|
+
|
|
215
|
+
### `getRecoveryStats()` / `getPersistenceStats()`
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
getRecoveryStats(): { attempt; exhausted; maxAttempts; hasError; errorMessage; errorAt; generation; lastSuccessAt }
|
|
219
|
+
getPersistenceStats(): { failures; lastFailureAt; lastErrorMessage }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`recovery.generation` increments on every successful transport open (initial start and each recovery); `lastSuccessAt` is the timestamp of that open (`null` before the first one). Persistence counters cover the optional replay persistence backend only.
|
|
223
|
+
|
|
224
|
+
### `getDiagnostics()`
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
getDiagnostics(): DataBusDiagnostics
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Full diagnostics snapshot combining lifecycle, transport identity (`name`, `backend`, live `status`, `suspended`), recovery, dedup, replay, persistence, protocol (`version`, `unknownMessages`, `peers`), the cluster snapshot, plus two diagnostics-only additions:
|
|
231
|
+
|
|
232
|
+
- `metrics: DataBusMetricsSnapshot | null` — the current trace metrics window (throughput, dispatch latency percentiles, dedup outcomes), or `null` when trace metrics are inactive (disabled or events-only mode).
|
|
233
|
+
- `trace: { asyncSink: boolean; pendingEvents: number }` — sink delivery mode and queued-event depth; a growing `pendingEvents` under `asyncSink: true` is the first sign of sink back-pressure.
|
|
234
|
+
|
|
235
|
+
`sdkVersion` is injected from `package.json` at build time. Prefer `getHealthSummary()` when a consumer only needs the readiness verdict.
|
|
236
|
+
|
|
237
|
+
### `getMetrics()`
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
getMetrics(): DataBusMetricsSnapshot | null
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Synchronous, non-destructive snapshot of the current trace metrics window — the same derived counters a periodic `message_metrics` event carries (received, dispatched, topics, dispatch latency avg/p50/p95/max, dedup accepted/suppressed), readable on demand without a sink or an interval flush. Returns `null` when trace metrics are inactive.
|
|
244
|
+
|
|
245
|
+
`getDiagnostics().replay` ships `{ enabled, topics, messages, bytes }` — `bytes` is the approximate in-memory payload footprint of the buffered replay rings (same string/binary/number sizing heuristic as adaptive load weighting), computed on demand so the hot append path never pays for it.
|
|
246
|
+
|
|
175
247
|
### `getClusterSnapshot()`
|
|
176
248
|
|
|
177
249
|
Returns a diagnostic snapshot:
|
|
@@ -208,7 +280,9 @@ trace: {
|
|
|
208
280
|
}
|
|
209
281
|
```
|
|
210
282
|
|
|
211
|
-
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`), and deduplication outcomes (`dedupAccepted`, `dedupSuppressed`). 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.
|
|
283
|
+
Low-frequency event types include `lifecycle`, `status`, `subscription`, `coordination`, `reliability`, 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`), and deduplication outcomes (`dedupAccepted`, `dedupSuppressed`). Route ownership changes surface as `reliability` events: a graceful handoff reports `operation: 'route_migration'`, while a re-election that recovered a stranded unconfirmed handoff (previous owner gone, ACK never arrived) reports `operation: 'route_migration_recovery'`, so trace consumers can tell recoveries apart from routine handoffs. The `coordination` event is emitted after each transport open and carries `coordinated`, `activeWorkers`, `workers` (formatted worker records), and `routes` (`topicKey@workerId|confirmed=…`) reflecting the settled route list. 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.
|
|
284
|
+
|
|
285
|
+
**`asyncSink: true` delivery semantics.** By default (`false`) the sink runs synchronously for each emitted event. With `asyncSink: true`, events are queued in memory and delivered in one microtask batch: the first event of a task schedules `queueMicrotask`, and every event emitted before that microtask runs is sent to the sink in FIFO order in a single pass (including the periodic `message_metrics` snapshots). The hot message path therefore never blocks on sink work. Error isolation is unchanged from the synchronous mode — a throwing sink is caught and reported via `console.warn` and never interrupts dispatch or the remaining events in the batch. Ordering boundary: order **within** a batch is guaranteed, but delivery is deferred to the next microtask, so a trace event is no longer observed before your next statement runs, and events emitted after a batch flushed settle into a later batch. Use the default synchronous sink (or consolidate counters yourself) when each event must be visible before the following line executes.
|
|
212
286
|
|
|
213
287
|
### `stop()`
|
|
214
288
|
|
|
@@ -226,6 +300,9 @@ interface DataBusTransport<TConfig, TData> {
|
|
|
226
300
|
subscribe(topic): void | Promise<void>;
|
|
227
301
|
unsubscribe(topic): void | Promise<void>;
|
|
228
302
|
publish(topic, data): void | Promise<void>;
|
|
303
|
+
/** Optional: one wire frame for many items. The DataBus falls back to
|
|
304
|
+
* per-item `publish` calls when this is absent. */
|
|
305
|
+
publishBatch?(topic, items): void | Promise<void>;
|
|
229
306
|
stop(): void | Promise<void>;
|
|
230
307
|
}
|
|
231
308
|
```
|
|
@@ -273,6 +350,18 @@ Available options:
|
|
|
273
350
|
- `workerFactory`: custom Dedicated Worker loading method
|
|
274
351
|
- `sharedWorkerFactory`: custom SharedWorker loading method
|
|
275
352
|
|
|
353
|
+
## `createStorageEventChannel(options)`
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
createStorageEventChannel(options: {
|
|
357
|
+
name: string;
|
|
358
|
+
storage: StorageLike | null;
|
|
359
|
+
win: StorageEventWindow | null;
|
|
360
|
+
}): ClusterChannel | null
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Creates a `ClusterChannel` backed by localStorage `storage` events — the coordination fallback for environments without BroadcastChannel. Returns `null` when storage or a storage-event source is unavailable. Delivery semantics mirror BroadcastChannel (no echo to the sender, JSON-serializable messages, closed channels refuse further posts); a monotonic sequence in the payload envelope keeps consecutive identical messages deliverable. Opt in via `createBrowserEnvironment({ channelFallback: 'storage-event' })`; see the security note in [configuration.md](./configuration.md#coordination-channel-fallback-broadcastchannel-unavailable).
|
|
364
|
+
|
|
276
365
|
## WebSocket Transport Backend
|
|
277
366
|
|
|
278
367
|
A dependency-free transport over a plain WebSocket. Any server speaking the JSON frame protocol below can back the same cross-tab clustering stack (owner dedup, sticky routes, failover) as the Centrifuge backend.
|
|
@@ -336,6 +425,10 @@ Attaches a message handler with automatic cleanup. The handler is read through a
|
|
|
336
425
|
|
|
337
426
|
Mirrors `bus.onStatus()` into React state and reads the current value synchronously whenever the bus identity changes. Returns `'connecting' | 'connected' | 'disconnected' | 'error'`.
|
|
338
427
|
|
|
428
|
+
### `useCrossTabHealth(bus, options?)`
|
|
429
|
+
|
|
430
|
+
Mirrors `bus.getHealthSummary()` into React state (`DataBusHealthSummary | null`). Because the summary is a snapshot, the hook polls it on an interval (default 1000 ms; pass `{ intervalMs: 0 }` for event-driven refreshes only) and refreshes immediately on status changes and errors. Returns `null` while the bus has not been created yet.
|
|
431
|
+
|
|
339
432
|
## Vue Composables (`cross-tab-worker-databus/vue`)
|
|
340
433
|
|
|
341
434
|
Vue 3.3+ is an optional peer dependency; this entry is separate from the core package.
|
|
@@ -348,6 +441,10 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
|
|
|
348
441
|
|
|
349
442
|
`useCrossTabDataBus` returns a Vue `Ref` that is populated on mount and stopped on unmount. `useCrossTabSubscription` accepts a string or `Ref<string>` topic and rebinds when the bus or topic changes. `useCrossTabStatus` returns a `Ref<WorkerStatus>` synchronized with `bus.onStatus()`.
|
|
350
443
|
|
|
444
|
+
### `useVueCrossTabHealth(bus, options?)`
|
|
445
|
+
|
|
446
|
+
The Vue binding of `useCrossTabHealth`: mirrors `bus.getHealthSummary()` into a `Ref<DataBusHealthSummary | null>`. Because the summary is a snapshot rather than an event stream, the composable polls it on an interval (default 1000 ms; pass `{ intervalMs: 0 }` for event-driven refreshes only) and refreshes immediately on status changes and errors. Returns `null` while the bus has not been created yet.
|
|
447
|
+
|
|
351
448
|
## `WorkerClusterRuntime`
|
|
352
449
|
|
|
353
450
|
Advanced API responsible for Worker registration, heartbeat, visibility, routing, BroadcastChannel protocol, and migration. Business modules should not operate on it directly.
|
|
@@ -358,6 +455,7 @@ Main methods:
|
|
|
358
455
|
- `setStatus(status)`
|
|
359
456
|
- `subscribe(topic)` / `unsubscribe(topic)`
|
|
360
457
|
- `publish(topic, data)`
|
|
458
|
+
- `publishBatch(topic, items)`
|
|
361
459
|
- `broadcastEvent(eventType, payload)`
|
|
362
460
|
- `isAssigned(topic)`
|
|
363
461
|
- `isActiveWorker()`
|
|
@@ -374,6 +472,12 @@ Generates a stable 128-bit hexadecimal opaque key. Used to avoid writing raw con
|
|
|
374
472
|
|
|
375
473
|
Creates a default browser environment adapter, including storage, BroadcastChannel, timers, and page lifecycle events.
|
|
376
474
|
|
|
475
|
+
### `getOrCreateTabId(environment, key?)`
|
|
476
|
+
|
|
477
|
+
Returns the stable identity of the current page/tab instance, reading it from session storage and creating one (`tab-<random>`) on first call, so a reloaded tab reclaims its routes instead of looking like a brand new one.
|
|
478
|
+
|
|
479
|
+
The stored value is deliberately *not* reused when `window.opener` is present: `window.open()` clones the opener's `sessionStorage` into the child, so a blind reuse would make two live tabs share one identity. When storage is unavailable or throws, a fresh ID is generated instead.
|
|
480
|
+
|
|
377
481
|
### `selectWorkerBackend(mode, availability?)`
|
|
378
482
|
|
|
379
483
|
Selects the actual backend based on `WorkerMode` and capability detection, returning `'shared' | 'dedicated' | 'local'`:
|
|
@@ -383,6 +487,22 @@ Selects the actual backend based on `WorkerMode` and capability detection, retur
|
|
|
383
487
|
|
|
384
488
|
`availability` can explicitly pass `worker` / `sharedWorker` capability flags, for use in SSR, testing, or embedded environments, avoiding access to non-existent global objects.
|
|
385
489
|
|
|
490
|
+
### `effectiveWorkerLoad(worker, options?)`
|
|
491
|
+
|
|
492
|
+
The pure scoring function behind least-loaded owner selection: `worker.load` (the owned-Topic count) plus the weighted traffic and scheduling-lag terms when `options` carries the `loadWeighting` weights.
|
|
493
|
+
|
|
494
|
+
The score is always finite. A Worker with no throughput sample, an unset (all-zero) weight set, a non-positive or non-finite sample window, or a non-finite weighted sum scores as its raw Topic count — a non-finite score would never compare correctly and would make owner selection depend on the order of the Worker array rather than on load. See [configuration.md](./configuration.md#adaptive-owner-weighting) for the weights.
|
|
495
|
+
|
|
496
|
+
### `approximatePayloadBytes(payload)`
|
|
497
|
+
|
|
498
|
+
Cheap, allocation-free estimate of a payload's wire size, used for the byte side of an adaptive load sample. It only runs when adaptive routing is enabled, so approximate sizes are fine — the goal is a stable cross-Worker comparison, not an exact byte count.
|
|
499
|
+
|
|
500
|
+
Sizes: `null`/`undefined` and symbols/functions `0`, booleans `4`, numbers and bigints `8`, strings their length, `ArrayBuffer`s and typed-array views their `byteLength`, arrays an 8-byte header plus their elements, and plain objects the sum of their values.
|
|
501
|
+
|
|
502
|
+
### `DEFAULT_MAX_ACTIVE_WORKERS`
|
|
503
|
+
|
|
504
|
+
The default cap on how many Workers may own Topics concurrently (`3`). It bounds fan-out breadth: only that many Workers are eligible to become new-route owners, so a cluster of twenty tabs still concentrates ownership on a few rather than spreading it thin. Override it with the `maxActiveWorkers` cluster option.
|
|
505
|
+
|
|
386
506
|
### Routing Functions
|
|
387
507
|
|
|
388
508
|
- `selectActiveWorkers`
|
package/docs/architecture.md
CHANGED
|
@@ -48,10 +48,36 @@ Because `MessagePort` has no `close` event, a tab that crashes before sending `S
|
|
|
48
48
|
| Layer | Entry | Responsibility |
|
|
49
49
|
|---|---|---|
|
|
50
50
|
| DataBus | `CrossTabDataBus` | Local handler reference counting, message dispatch, state and transport lifecycle |
|
|
51
|
+
| Replay | `ReplayManager` | Bounded per-topic history ring, durable IndexedDB persistence, retention cleanup, retry policy |
|
|
52
|
+
| Dedup | `DedupManager` | Opt-in bounded duplicate suppression by `messageId`, adaptive TTL, expiry sweep |
|
|
51
53
|
| Cluster Coordination | `WorkerClusterRuntime` | Worker registration, roles, heartbeat, Topic owner, migration and broadcast protocol |
|
|
52
54
|
| Transport | `DataBusTransport` | Executes subscribe, unsubscribe, publish on the real Worker/connection |
|
|
53
55
|
| Centrifuge | `CentrifugeWorkerTransport` | Protocol adaptation between the main thread and the built-in Centrifuge Worker |
|
|
54
56
|
|
|
57
|
+
## Source Layout & Shared Utils
|
|
58
|
+
|
|
59
|
+
The `src/` tree keeps platform adapters and the coordination core beside a small
|
|
60
|
+
`src/utils/` toolbox:
|
|
61
|
+
|
|
62
|
+
| File | Contents | Consumers |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `utils/constants.ts` | Every runtime string literal in one place — statuses, roles, actions, cluster/worker/protocol message types, trace event discriminants, enums, namespace prefixes. All literal-derived types (`WorkerStatus`, message `type` fields, trace `action`/`operation`, …) are derived from these constants with `(typeof X)[keyof typeof X]`, so a value and its type cannot drift apart. | all modules |
|
|
65
|
+
| `utils/metadata.ts` | `publicationMetadata(messageId, timestamp)` — spreads only defined metadata, previously duplicated across four modules. | data-bus, cluster, centrifuge, centrifuge-session |
|
|
66
|
+
| `utils/storage-utils.ts` | `readJson` / `writeJson` / `listKeys` / `readAllByPrefix` — fault-tolerant storage primitives (corrupt JSON → absent; failed write → swallowed). | cluster |
|
|
67
|
+
| `utils/validation.ts` | Constructor/option validation asserts (`assertReplayOptions`, `assertDedupOptions`, `assertRecoveryOptions`, `assertHeartbeatInterval`, …). Optional fields are validated only when explicitly provided; defaults are always valid. | data-bus, replay-persistence, centrifuge |
|
|
68
|
+
| `utils/error-utils.ts` | `serializeError` / `deserializeWorkerError` + `SerializedWorkerError` — Error round-tripping across the Worker boundary. | centrifuge, centrifuge-session |
|
|
69
|
+
|
|
70
|
+
Extracting these was deliberate: they are side-effect-free, dependency-light
|
|
71
|
+
helpers whose duplicated copies had already started to drift (e.g. four
|
|
72
|
+
near-identical `publicationMetadata` implementations). Stateful cross-cutting
|
|
73
|
+
concerns with their own lifecycle — replay buffering and dedup — live as
|
|
74
|
+
self-contained `DedupManager` / `ReplayManager` classes that the DataBus
|
|
75
|
+
delegates to; they own their maps/timers/stats and expose thin start/stop/
|
|
76
|
+
record/clear surfaces. The DataBus lifecycle state machine (start/stop/
|
|
77
|
+
suspend/resume, promise gates, recovery pacing) was kept inside
|
|
78
|
+
`CrossTabDataBus` on purpose: those flags interlock tightly and extracting
|
|
79
|
+
them would re-introduce the race conditions the gates exist to prevent.
|
|
80
|
+
|
|
55
81
|
## Glossary
|
|
56
82
|
|
|
57
83
|
Terms are explained in plain language; the code and the rest of this document use the short names.
|
|
@@ -307,6 +333,46 @@ The owning Worker filters every publication it receives with `isAssigned(topic)`
|
|
|
307
333
|
6. Only a Topic without a route, or a route whose owner has departed or expired by heartbeat TTL, is assigned to the least-loaded candidate Worker.
|
|
308
334
|
7. A new route is considered unconfirmed until the owner writes `confirmedAt`; the subscriber will automatically resend the control message.
|
|
309
335
|
|
|
336
|
+
### Adaptive owner weighting (`loadWeighting`)
|
|
337
|
+
|
|
338
|
+
By default "least-loaded" means the fewest owned Topics. The opt-in `loadWeighting`
|
|
339
|
+
option adds traffic and scheduling signals, still only for NEW or orphaned routes —
|
|
340
|
+
existing routes stay sticky and are never migrated:
|
|
341
|
+
|
|
342
|
+
- Each Worker samples its own fan-out activity between heartbeats and publishes a
|
|
343
|
+
`WorkerThroughputSample` (`windowMs`, `messageCount`, `byteCount`, `overrunMs`,
|
|
344
|
+
`sampledAt`) with the worker record.
|
|
345
|
+
- `overrunMs` is the positive excess of the sample window over the nominal heartbeat
|
|
346
|
+
interval. A starved event loop (the browser-observable proxy for CPU saturation)
|
|
347
|
+
lands heartbeats late and stretches the window, so this is a cheap native signal.
|
|
348
|
+
- `effectiveWorkerLoad` (a pure function in `routing.ts`) scores a Worker as
|
|
349
|
+
`load + messageRateWeight × msg/s + byteRateWeight × B/s + scheduleLagWeight × (overrunMs ÷ windowMs)`.
|
|
350
|
+
All weights default to `0`, keeping the legacy pure topic-count score byte-identical.
|
|
351
|
+
- The scorer is deterministic and reads the same persisted records on every tab, so
|
|
352
|
+
routing agrees cluster-wide; a scheduling-laggy Worker becomes less attractive for
|
|
353
|
+
new routes even when it carries fewer topics.
|
|
354
|
+
|
|
355
|
+
### Async credential refresh bridge (`credentialProvider`)
|
|
356
|
+
|
|
357
|
+
Centrifuge client options are structured-cloned into the Worker, so function-valued
|
|
358
|
+
`getToken` / `getChannelToken` cannot travel with the config. The opt-in
|
|
359
|
+
`credentialProvider` (`{ getToken, getChannelToken }`) on `createCentrifugeDataBus`
|
|
360
|
+
runs on the main thread instead:
|
|
361
|
+
|
|
362
|
+
1. When a provider is configured, INIT carries `tokenBridge: true` and the Worker's
|
|
363
|
+
`CentrifugeSession` wires `getToken` / `getChannelToken` to issue a
|
|
364
|
+
`TOKEN_REQUEST` output (`requestId`, `kind`, optional `channel`).
|
|
365
|
+
2. The transport resolves it on the main thread: `resolveTokenRequest` calls the
|
|
366
|
+
provider and posts `TOKEN_RESPONSE` (or `TOKEN_ERROR` on rejection / empty token)
|
|
367
|
+
back with the matching `requestId`.
|
|
368
|
+
3. The session settles the pending promise by `requestId`; a `STOP` rejects all
|
|
369
|
+
in-flight requests so a stopped worker never awaits a response forever.
|
|
370
|
+
4. Without a provider, INIT omits `tokenBridge` and the config stays byte-identical
|
|
371
|
+
to legacy — a server that never asks for a token triggers no requests.
|
|
372
|
+
5. Dedicated, SharedWorker, and local backends share `CentrifugeSession`, so the
|
|
373
|
+
bridge works across all three; the token never crosses the Worker boundary as a
|
|
374
|
+
function, only as a resolved string.
|
|
375
|
+
|
|
310
376
|
## Subscription Flow
|
|
311
377
|
|
|
312
378
|
```mermaid
|
|
@@ -465,6 +531,20 @@ If a Tab still subscribed to a Topic finds that the owner Worker has departed or
|
|
|
465
531
|
|
|
466
532
|
This process prevents overlap during graceful owner handoff while retaining availability during failure recovery; it does not guarantee exactly-once delivery.
|
|
467
533
|
|
|
534
|
+
## Stability Invariants
|
|
535
|
+
|
|
536
|
+
These invariants are pinned by regression tests (see `tests/stability.test.ts` and `tests/replay-persistence.test.ts`) and must hold through future refactors:
|
|
537
|
+
|
|
538
|
+
- **Handoff ACK validity.** A `ROUTE_RELEASED` is accepted only when the route still points at the receiver, the release comes from the recorded `handoffFromWorkerId`, and the ACK generation is at least as new as the stored route generation. Replayed ACKs from an earlier handoff round (e.g. an a↔b ping-pong) carry an older generation and are dropped.
|
|
539
|
+
- **Replay persistence cleanup ordering.** A batched persistence flush queued behind the current task is filtered against the cleanup that wins the race: `unsubscribe` and `clearReplayTopic` drop the topic's pending entries, `clearReplayBefore` drops entries older than the cutoff. Cleared history is never re-appended by an in-flight flush.
|
|
540
|
+
- **Storage write recovery.** Coalesced writes retry with exponential backoff (50 ms → 1.6 s cap). A structurally failing key is dropped after 5 attempts (with a `console.warn`) without permanently blocking other queued keys, and the backoff delay resets once the queue fully drains or `clear()` cancels the retries.
|
|
541
|
+
- **Transport recovery budget.** Automatic recovery is paced by a cooldown, bounded by `recovery.maxAttempts`, and reports `exhausted` when the budget is spent. A successful reopen resets the attempt counter and the exhausted flag; explicit `subscribe` on a down transport can still recover manually.
|
|
542
|
+
- **BFCache suspension.** Hiding the tab stops the transport, bumps the persistence-retry generation (cancelling in-flight persistence retries without surfacing errors), and gates dispatch; pageshow reopens the transport and re-establishes subscriptions exactly once per cycle.
|
|
543
|
+
- **Handoff channel close ordering.** `pause()` defers the physical `channel.close()` by one task. Closing synchronously would discard messages still queued for delivery — including the handoff's `ROUTE_RELEASED` — stranding the handoff target with an unconfirmed route.
|
|
544
|
+
- **Stranded-handoff recovery.** If the previous owner is gone and its `ROUTE_RELEASED` never arrives (dropped channel message under load, or a crash between the route write and the ACK), the reconcile loop re-elects a live owner once the unconfirmed handoff has been stuck longer than a worker TTL (10 s default): the route is rewritten with a fresh generation and the handoff marker cleared, so the normal confirmation path completes (pinned by regression). The age gate matters — a fresh unconfirmed route may simply be waiting out its confirmation flush — and while the previous owner is still alive the new owner keeps waiting, so the strict handoff keeps its no-overlap guarantee.
|
|
545
|
+
- **Loss and recovery matrix.** Each coordination message has a bounded recovery path: a lost `CONTROL/SUBSCRIBE` is re-sent by the heartbeat reconcile for any route still lacking `confirmedAt`; a lost `REGISTRY` nudge costs at most one heartbeat interval (3 s default) because every tick reconciles anyway; a lost `ROUTE_RELEASED` is recovered by reconcile re-election once the previous owner is gone and the handoff has been stuck longer than a worker TTL (see the stranded-handoff invariant above, pinned by regression); publications dropped during a transport disconnect window are the one documented unrecoverable loss (transport contract). The storage-event fallback channel guarantees value-change delivery via a monotonic sequence in the envelope, and a dropped dispatch recovers through the same reconcile loop.
|
|
546
|
+
- **Recovery diagnostics.** `getHealthSummary()` derives a single readiness verdict from the lifecycle flags (`stopped` / `starting` / `healthy` / `recovering` / `suspended` / `degraded`); the unified `lastFailure` ledger and persistence counters reset on every explicit `start()`.
|
|
547
|
+
|
|
468
548
|
## Transport Reconnection
|
|
469
549
|
|
|
470
550
|
DataBus separates "business subscription intent" from "transport current subscription state". When the transport reports `disconnected` / `error`, it only clears the underlying subscription flag, not the business handler; when it re-enters `connected`, DataBus automatically replays the Topics the current Worker is responsible for.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- Generated by scripts/bench-trend.mjs — the whole file is machine-generated; edit the script, not this doc. -->
|
|
2
|
+
|
|
3
|
+
# Browser Benchmark Trend
|
|
4
|
+
|
|
5
|
+
> Data through 2026-09-11, from the 12 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
|
|
6
|
+
|
|
7
|
+
The comparison baseline for release gating is `pnpm bench:compare --fail-above-pct 50` between the two most recent reports (50% ceiling absorbs shared-runner noise). This doc records the long-run picture: values are per-metric latencies where lower is better, and the all-time best marks the healthiest observed run on this machine.
|
|
8
|
+
|
|
9
|
+
<!-- BENCH-TREND:BEGIN (machine-generated table) -->
|
|
10
|
+
| Metric | Previous (ms) | Latest (ms) | Δ | All-time best (ms) |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| publish per-message (ms, lower is better) — dedicated | 43.8388 | 40.8227 | -3.02 | 40.8227 |
|
|
13
|
+
| publish per-message (ms, lower is better) — shared | 34.2105 | 35.4339 | +1.22 | 33.8206 |
|
|
14
|
+
| wildcard dispatch ×1000 (ms, lower is better) | 6.5 | 6.7 | +0.20 | 0.1 |
|
|
15
|
+
| publishBatch ×1000 (ms, lower is better) | 4.2 | 4 | -0.20 | 0.4 |
|
|
16
|
+
| dedup ×1000 (ms, lower is better) | 15.5 | 13.9 | -1.60 | 0 |
|
|
17
|
+
| trace + publish ×1000 (ms, lower is better) | 5.8 | 5.3 | -0.50 | 4.8 |
|
|
18
|
+
| first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
|
|
19
|
+
<!-- BENCH-TREND:END -->
|
|
20
|
+
|
|
21
|
+
Notes:
|
|
22
|
+
|
|
23
|
+
- publish rows measure the full demo round-trip (publish click → transport → demo server → EVENT fan-out → receiver metric), so they include real browser and server latency; the databus rows are in-page hot-path microbenchmarks.
|
|
24
|
+
- A sustained upward drift across several runs — not a single outlier — is the regression signal. Investigate the hot-path changes in that window before tagging a release.
|
package/docs/capabilities.md
CHANGED
|
@@ -20,20 +20,41 @@ Status Legend: `✅ Implemented` means the current version has code and test cov
|
|
|
20
20
|
| Exception Recovery | Worker TTL, stale owner migration, and coordination cache cleanup | ✅ Implemented | Reclaims dead Workers, orphaned subscribers, and expired routes with no subscribers |
|
|
21
21
|
| transport | Replays owner Topics after reconnect | ✅ Implemented | Business handlers and subscription intent are not cleared on disconnect |
|
|
22
22
|
| Degradation | Runs locally when localStorage or BroadcastChannel is unavailable | ✅ Implemented | Preserves the current Tab's connection and subscription capabilities |
|
|
23
|
+
| Degradation | Opt-in localStorage storage-event coordination channel when BroadcastChannel is unavailable | ✅ Implemented | `createBrowserEnvironment({ channelFallback: 'storage-event' })`; coordination payloads (plaintext topic names) persist to localStorage — documented trade-off |
|
|
23
24
|
| Centrifuge | Built-in Dedicated / Shared Worker transport | ✅ Implemented | Supports subscribe, unsubscribe, publish, connection status, and error reporting; `auto` degrades from SharedWorker → Dedicated Worker → main-thread WebSocket |
|
|
24
25
|
| Security Boundary | localStorage uses opaque keys derived from connection and Topic; BroadcastChannel coordination messages carry plaintext topic names | ✅ Implemented | Does not persist URLs, raw Topic names, credentials, or publication payloads. BroadcastChannel coordination messages are in-memory only and carry plaintext topic names — they are not persisted. |
|
|
25
|
-
| Diagnostics | Aggregates lifecycle events, throughput, delivery latency, dedup outcomes, recovery retries, route acknowledgments, and migrations | ✅ Implemented | Disabled by default; metrics are emitted every 5 seconds by default, with bounded reliability events for recovery and route coordination |
|
|
26
|
+
| Diagnostics | Aggregates lifecycle events, throughput, delivery latency, dedup outcomes, recovery retries, route acknowledgments, and migrations | ✅ Implemented | Disabled by default; metrics are emitted every 5 seconds by default, with bounded reliability events for recovery and route coordination. On-demand `getMetrics()` / `getDiagnostics().metrics` expose the current window synchronously without a sink |
|
|
27
|
+
| Diagnostics | `getHealthSummary()` single-object readiness verdict and unified failure ledger | ✅ Implemented | `healthy`/`state` covers stopped, starting, recovering, BFCache-suspended, and recovery-exhausted degraded; `lastFailure` unifies transport/persistence/dispatch sources and resets on `start()` |
|
|
26
28
|
| Performance | Batched writes of coordination metadata with backoff retry | ✅ Implemented | Heartbeat, route, and subscriber writes are merged and flushed in a microtask; failures use exponential backoff; `pagehide` / `stop()` flush synchronously |
|
|
27
29
|
| Performance | Optional ArrayBuffer Transferable transport | ✅ Implemented | With `transferable: true`, binary publish / receive bypasses structured clone copying; the object message API is unchanged |
|
|
30
|
+
| Performance | Optional `DataBusTransport.publishBatch` one-frame burst publishing | ✅ Implemented | The bundled WebSocket transport sends multi-item batches in one wire frame (`publishBatch` op, demo server supported); transports without batch support fall back to per-item `publish` |
|
|
28
31
|
| Message Semantics | exactly-once delivery | Not Implemented | Graceful handoff avoids overlap, but crash recovery and transport/server behavior still do not provide an exactly-once guarantee |
|
|
29
32
|
| Message Semantics | Pluggable publication deduplication | ✅ Implemented | Opt-in bounded inbound suppression by `DataBusMessage.messageId`; default is disabled and transport/server IDs remain caller-controlled |
|
|
30
|
-
| Authentication | Async credential refresh bridge inside the Worker |
|
|
31
|
-
| Load Policy | Adaptive weighting by message rate, byte count, or
|
|
33
|
+
| Authentication | Async credential refresh bridge inside the Worker | ✅ Implemented | Opt-in `credentialProvider` ({ getToken, getChannelToken }) on `createCentrifugeDataBus`. Worker config stays structured-cloneable; the Worker asks the main thread for each fresh token over a TOKEN_REQUEST / TOKEN_RESPONSE exchange, and the provider supplies it from application context |
|
|
34
|
+
| Load Policy | Adaptive weighting by message rate, byte count, or scheduling lag | ✅ Implemented | Opt-in `loadWeighting` ({ messageRateWeight, byteRateWeight, scheduleLagWeight }): workers sample their own fan-out traffic and heartbeat scheduling overrun per window and publish it with the worker record; new-route owner selection adds the normalized per-second rates and lag ratio to the topic count. Default (unset) keeps pure topic-count routing; existing routes stay sticky. |
|
|
32
35
|
| Observability | Metrics/events for owner acknowledgments, migrations, and recovery attempts | ✅ Implemented | `DataBusReliabilityTraceEvent` reports bounded route ack/migration and transport recovery events; exact server-side ack remains transport-specific |
|
|
33
36
|
| Runtime Model | SharedWorker / Dedicated Worker transport | ✅ Implemented | `workerMode` supports `dedicated`, `shared`, and `auto`, defaulting to `dedicated` |
|
|
34
37
|
| Runtime Model | Service Worker transport | Not Implemented | Deliberately deferred; lifetime and long-lived connection constraints are documented in `docs/architecture.md` |
|
|
35
38
|
| Durable Messages | Persisting publications or publish commands across page close | Not Implemented | The SDK does not persist business payloads, nor does it replay publish commands after restoration |
|
|
36
39
|
|
|
40
|
+
## Verified Guarantees (pinned by the regression suite)
|
|
41
|
+
|
|
42
|
+
These invariants are consolidated from the "Stability Invariants" section of
|
|
43
|
+
`docs/architecture.md` (pinned by `tests/stability.test.ts` and
|
|
44
|
+
`tests/replay-persistence.test.ts`) so each guarantee below is both documented
|
|
45
|
+
and regression-locked:
|
|
46
|
+
|
|
47
|
+
| Invariant | Area | Regressed guarantee |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Handoff ACK validity | coordination | `ROUTE_RELEASED` is honored only when the route still points to the receiver, the release matches `handoffFromWorkerId`, and the ACK generation is at least as new as the stored route — stale ACKs from an earlier a↔b ping-pong are dropped |
|
|
50
|
+
| Replay persistence cleanup ordering | durability | Queued batch flushes are filtered against the winning cleanup (`unsubscribe`/`clearReplayTopic` drop the topic's pending entries, `clearReplayBefore` drops entries older than the cutoff); cleared history is never re-appended by an in-flight flush |
|
|
51
|
+
| Storage write recovery | coordination | Coalesced writes retry with exponential backoff (50 ms → 1.6 s cap); a structurally failing key is dropped after 5 attempts (with `console.warn`) without blocking other queued keys; backoff resets once the queue drains or `clear()` cancels the retries |
|
|
52
|
+
| Transport recovery budget | lifecycle | Auto-recovery is paced by a cooldown, bounded by `recovery.maxAttempts`, and reports `exhausted` when the budget is spent; a successful reopen resets attempt + exhausted, and an explicit `subscribe` on a down transport can still recover manually |
|
|
53
|
+
| BFCache suspension | lifecycle | Hiding the tab stops the transport and silently cancels in-flight persistence retries; pageshow reopens the transport and re-establishes subscriptions exactly once per cycle |
|
|
54
|
+
| Handoff channel close ordering | coordination | `pause()` defers the physical `channel.close()` by one task so queued handoff frames (including `ROUTE_RELEASED`) flush before the channel dies |
|
|
55
|
+
| Loss & recovery matrix | coordination | Every coordination message has a bounded recovery path (re-send of unconfirmed SUBSCRIBE, REGISTRY nudge, TTL + re-election for lost ACKs); publications lost during a transport disconnect window are the one documented unrecoverable loss |
|
|
56
|
+
| Recovery diagnostics | observability | `getHealthSummary()` derives a single readiness verdict, and the unified `lastFailure` ledger persists across transport failures, resetting on every explicit `start()` |
|
|
57
|
+
|
|
37
58
|
## Acceptance Criteria
|
|
38
59
|
|
|
39
60
|
- "Implemented" does not mean every browser environment provides cross-Tab capability; when localStorage or BroadcastChannel is missing, it falls back to local degradation by design.
|
package/docs/configuration.md
CHANGED
|
@@ -56,6 +56,27 @@ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced dur
|
|
|
56
56
|
|
|
57
57
|
`replay.persistenceRetry` optionally controls transient persistence recovery. `maxAttempts` is the total number of attempts (default `1`), and `backoffMs` is the initial delay before retry (default `50`). Delays grow exponentially and are capped; final failures retain the existing `onError` and reliability behavior.
|
|
58
58
|
|
|
59
|
+
### Replay Options
|
|
60
|
+
|
|
61
|
+
| Config | Type | Default | Description |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `maxPerTopic` | `number` | `100` | Maximum buffered publications per topic; oldest are evicted first (positive safe integer) |
|
|
64
|
+
| `persistence` | `DataBusReplayPersistence` | — | Optional durable backend (`createIndexedDbReplayPersistence`); omitted keeps history in memory only |
|
|
65
|
+
| `retentionMs` | `number` | — | Producer-timestamp retention window; history older than the cutoff is pruned through the adapter's `clearBefore` |
|
|
66
|
+
| `pruneStrategy` | `'count' \| 'age' \| 'both'` | `'count'` | `count` caps each topic at `maxPerTopic`; `age` prunes by `retentionMs`; `both` applies both. `age` without `retentionMs` has nothing to prune by and falls back to the count cap |
|
|
67
|
+
| `retentionSweepMs` | `number` | — | Periodic durable-retention sweep for quiet topics; requires `retentionMs` and a `clearBefore` adapter |
|
|
68
|
+
| `persistenceRetry` | `{ maxAttempts, backoffMs }` | `1` / `50` | Bounded retry for transient persistence failures; delays grow exponentially and are capped |
|
|
69
|
+
|
|
70
|
+
### Deduplication Options
|
|
71
|
+
|
|
72
|
+
| Config | Type | Default | Description |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `maxEntries` | `number` | `1000` | Remembered message IDs before the oldest (FIFO) entry is evicted (positive safe integer) |
|
|
75
|
+
| `ttlMs` | `number` | `60000` | How long a remembered ID suppresses a repeat (positive finite) |
|
|
76
|
+
| `sweepMs` | `number` | — | Optional periodic sweep that expires quiet IDs; disabled by default |
|
|
77
|
+
| `now` | `() => number` | `Date.now` | Injectable clock for deterministic TTL tests and non-wall-clock hosts |
|
|
78
|
+
| `adaptiveTtl` | `{ minMs, maxMs }` | — | Optional bounded adaptive TTL: a high recent message rate shortens the window toward `minMs`, a quiet period relaxes it to `maxMs` (both required, `minMs <= maxMs`) |
|
|
79
|
+
|
|
59
80
|
When tracing is enabled, each retry before the final attempt emits a bounded `reliability` event with `operation: persistence_retry`, `persistenceOperation` (`load`, `append`, `clear`, `clearTopic`, or `clearBefore`), and the failed attempt number. No payload, URL, credential, or error body is included.
|
|
60
81
|
|
|
61
82
|
WebSocket binary frames may arrive as either `ArrayBuffer` or browser `Blob`; both use the same compact binary publication format. Blob conversion is asynchronous and conversion failures are reported through the transport error handler.
|
|
@@ -100,6 +121,36 @@ The active set is used only when a Topic needs a new owner. An existing live own
|
|
|
100
121
|
- `2-3`: Balances resource reuse and fault recovery.
|
|
101
122
|
- Larger values: Suitable for scenarios with many Topics and where a single connection faces server-side limits.
|
|
102
123
|
|
|
124
|
+
### Adaptive Owner Weighting
|
|
125
|
+
|
|
126
|
+
By default a new Topic is assigned to the owner with the fewest owned Topics. The
|
|
127
|
+
opt-in `loadWeighting` option adds traffic and scheduling signals so new routes
|
|
128
|
+
favor quieter, healthier Workers — existing routes stay sticky and never migrate.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const bus = createCentrifugeDataBus({
|
|
132
|
+
connection: { url: getConnectionUrl() },
|
|
133
|
+
loadWeighting: {
|
|
134
|
+
messageRateWeight: 0.5, // weight per message-per-second
|
|
135
|
+
byteRateWeight: 0.001, // weight per byte-per-second
|
|
136
|
+
scheduleLagWeight: 2 // weight for heartbeat scheduling-lag ratio
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Option | Default | Effect |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `messageRateWeight` | `0` | Each Worker samples its own fan-out message rate per heartbeat window; the normalized rate is added to the effective load. |
|
|
144
|
+
| `byteRateWeight` | `0` | Same, for approximate payload bytes per second. |
|
|
145
|
+
| `scheduleLagWeight` | `0` | Weights the scheduling-lag ratio (`overrunMs ÷ windowMs`). A starved event loop lands heartbeats late, so this steers new routes away from a throttled Worker. |
|
|
146
|
+
|
|
147
|
+
All weights default to `0`, keeping the legacy pure topic-count behavior byte-identical.
|
|
148
|
+
Each weight must be a non-negative finite number; a negative or non-finite weight throws a
|
|
149
|
+
`TypeError` at construction, since a negative weight would bias new routes toward the
|
|
150
|
+
*busiest* Worker instead.
|
|
151
|
+
The per-Worker sample (`WorkerThroughputSample`) is published with the heartbeat and
|
|
152
|
+
carries `windowMs`, `messageCount`, `byteCount`, `overrunMs`, and `sampledAt`.
|
|
153
|
+
|
|
103
154
|
## Centrifuge Configuration
|
|
104
155
|
|
|
105
156
|
Main configuration for `createCentrifugeDataBus<TData>(options)`:
|
|
@@ -114,6 +165,7 @@ Main configuration for `createCentrifugeDataBus<TData>(options)`:
|
|
|
114
165
|
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING heartbeat interval (see SharedWorker Session Reaper below); `Infinity` disables heartbeats entirely. Distinct from the Core cluster heartbeat (default 3000 ms) which tracks worker liveness via localStorage |
|
|
115
166
|
| `workerFactory` | `() => Worker` | Built-in Worker | For testing or custom Worker loading |
|
|
116
167
|
| `sharedWorkerFactory` | `() => SharedWorker` | Built-in SharedWorker | For testing or custom SharedWorker loading |
|
|
168
|
+
| `credentialProvider` | `{ getToken?, getChannelToken? }` | `undefined` | Async credential refresh bridge: the Worker asks the main thread for each fresh token (`getToken` / `getChannelToken`) and this provider supplies it from application context. Required because function-valued Centrifuge options cannot be structured-cloned into the Worker |
|
|
117
169
|
| Other Core config | Corresponding type | Core defaults | `storagePrefix`, heartbeat, TTL, etc. |
|
|
118
170
|
|
|
119
171
|
```ts
|
|
@@ -146,6 +198,12 @@ The full chain for `auto` is **SharedWorker -> Dedicated Worker -> main-thread l
|
|
|
146
198
|
|
|
147
199
|
When neither `sharedWorkerFactory` nor `workerFactory` is provided, the transport detects global `SharedWorker` / `Worker` capability at startup. When a custom factory is provided, the corresponding backend is considered available, avoiding false negatives from global capability detection in Node, SSR, or embedded environments. All modes perform the same structured clone validation; config and `publish` data must be structured-clonable.
|
|
148
200
|
|
|
201
|
+
## Coordination Channel Fallback (BroadcastChannel unavailable)
|
|
202
|
+
|
|
203
|
+
When `BroadcastChannel` is unavailable (some WebViews, older browsers), the cluster normally degrades to local-only operation: the transport works, but tabs do not coordinate ownership.
|
|
204
|
+
|
|
205
|
+
`createBrowserEnvironment({ channelFallback: 'storage-event' })` opts into a fallback `ClusterChannel` backed by localStorage `storage` events, preserving cross-tab coordination. It is opt-in because of a security trade-off: BroadcastChannel messages live in memory only, while the fallback writes coordination payloads (which carry plaintext topic names) to localStorage under the `cross-tab-worker-databus:channel:` key namespace — at least transiently, and indefinitely after a tab crash. The key is removed when the channel closes.
|
|
206
|
+
|
|
149
207
|
## SharedWorker Session Reaper
|
|
150
208
|
|
|
151
209
|
A `MessagePort` has no `close` event, so the SharedWorker cannot know when a tab has crashed or been closed without sending a `STOP` message. To avoid leaking a `CentrifugeSession` (and its WebSocket) for a dead tab, the transport sends a periodic **PING heartbeat** to the SharedWorker, and the SharedWorker runs a **reaper** that closes any session whose port has been silent for longer than its timeout.
|