cross-tab-worker-databus 0.21.10 → 0.21.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/core/cluster.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,kBAAkB,EAAe,MAAM,eAAe,CAAC;AASrF,OAAO,KAAK,EACV,0BAA0B,EAC1B,oBAAoB,EAGpB,mBAAmB,EACnB,YAAY,EAEZ,WAAW,EACX,YAAY,EAEb,MAAM,SAAS,CAAC;AAEjB,OAAO,EAIL,qBAAqB,EAItB,MAAM,oBAAoB,CAAC;AAK5B,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,SAAS,EAAE,CACT,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,OAAO,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,KACf,IAAI,CAAC;IACV;;yEAEqE;IACrE,cAAc,CAAC,EAAE,CACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,KAC5E,IAAI,CAAC;IACV;;;2DAGuD;IACvD,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,OAAO,EAAE,CACP,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,KACjB,IAAI,CAAC;IACV,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,qGAAqG;IACrG,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QACrB,SAAS,EAAE,CAAC,OAAO,qBAAqB,CAAC,SAAS,GAAG,OAAO,qBAAqB,CAAC,eAAe,GAAG,OAAO,qBAAqB,CAAC,wBAAwB,CAAC,CAAC;QAC3J,KAAK,EAAE,MAAM,CAAC;KACf,KAAK,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,oBAAoB;IACnC;gFAC4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,qBAAqB,CAAC;IAChC,2EAA2E;IAC3E,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;sEACkE;IAClE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;kFAG8E;IAC9E,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACtC;AAED,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACpD,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,YAAY,CAAC;IAC5B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,qFAAqF;IACrF,MAAM,EAAE,KAAK,CAAC,WAAW,GAAG;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACtD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,WAAW,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,eAAe,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/E;AAUD;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,gFAAgF;IAChF,OAAO,CAAC,gBAAgB,CAItB;IAEF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IAItD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA+D;IAC/F,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAoC;IAMzE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,kBAAkB,CAAS;IACnC;4EACwE;IACxE,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,aAAa,CAAe;gBAExB,OAAO,EAAE,oBAAoB;IAsCzC,sFAAsF;IACtF,KAAK,IAAI,IAAI;IAiBb;;;;;;OAMG;IACH,IAAI,IAAI,IAAI;IAcZ;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAgDhB;;;OAGG;IACH,OAAO,CAAC,KAAK;IAsDb,oEAAoE;IACpE,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAMrC;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAoCjC;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAOhC;;yEAEqE;IACrE,OAAO,CAAC,mBAAmB;IAa3B,qGAAqG;IACrG,OAAO,CAAC,qBAAqB;IAsC7B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO;IAClE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,0BAA0B,GAAG,OAAO;IAuCrF;;;;;;OAMG;IACH,YAAY,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAC9E,OAAO;IAgDV;sFACkF;IAClF,OAAO,CAAC,oBAAoB;IAmB5B,iEAAiE;IACjE,OAAO,CAAC,oBAAoB;IAa5B;6EACyE;IACzE,OAAO,CAAC,yBAAyB;IAYjC;;;mEAG+D;IAC/D,OAAO,CAAC,gBAAgB;IAIxB;;iFAE6E;IAC7E,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAS/E;2CACuC;IACvC,OAAO,CAAC,aAAa;IAMrB;;+DAE2D;IAC3D,OAAO,CAAC,gBAAgB;IAsBxB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAsBlC,4EAA4E;IAC5E,cAAc,IAAI,OAAO;IAIzB;8EAC0E;IAC1E,OAAO,CAAC,aAAa;IAMrB;kEAC8D;IAC9D,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAQ1C,iEAAiE;IACjE,sBAAsB,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAEpE,8EAA8E;IAC9E,WAAW,IAAI,qBAAqB;IAuBpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IAErD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAU7B;IAEF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAQrC;IAEF,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,wBAAwB;IAQhC,qFAAqF;IACrF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAsB5B;IAEF,mFAAmF;IACnF,OAAO,CAAC,oBAAoB;IA6E5B;;;;;OAKG;IACH,OAAO,CAAC,2BAA2B;IAYnC;kEAC8D;IAC9D,OAAO,CAAC,iBAAiB;IAgBzB;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IAoBlC;;;;oCAIgC;IAChC,OAAO,CAAC,mBAAmB;IAW3B;;;;oEAIgE;IAChE,OAAO,CAAC,cAAc;IAItB,8EAA8E;IAC9E,OAAO,CAAC,SAAS;IASjB;;iFAE6E;IAC7E,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAsF9B,wEAAwE;IACxE,OAAO,CAAC,uBAAuB;IAa/B;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA6CnB,oFAAoF;IACpF,OAAO,CAAC,IAAI;IAUZ,qFAAqF;IACrF,OAAO,CAAC,WAAW;IAenB,0EAA0E;IAC1E,OAAO,CAAC,oBAAoB;IAsB5B,0FAA0F;IAC1F,OAAO,CAAC,SAAS;IAKjB;;;yCAGqC;IACrC,OAAO,CAAC,eAAe;IAavB,2EAA2E;IAC3E,OAAO,CAAC,UAAU;IAUlB;;qCAEiC;IACjC,OAAO,CAAC,gBAAgB;IAgBxB,yFAAyF;IACzF,OAAO,CAAC,YAAY;IAYpB,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB;IAU7B,oEAAoE;IACpE,OAAO,CAAC,0BAA0B;IAQlC,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAQvB;;;;;0CAKsC;IACtC,OAAO,CAAC,WAAW;IAYnB,4EAA4E;IAC5E,OAAO,CAAC,cAAc;IAItB,8GAA8G;IAC9G,OAAO,CAAC,WAAW;IAOnB,kGAAkG;IAClG,OAAO,CAAC,UAAU;IAqBlB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,aAAa;IAQrB,8EAA8E;IAC9E,OAAO,CAAC,YAAY;CAGrB"}
1
+ {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/core/cluster.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  selectRebalanceTarget,
21
21
  selectWorkerBackend,
22
22
  topicMatchesPattern
23
- } from "./chunk-L5P66THO.js";
23
+ } from "./chunk-TSTV3SDC.js";
24
24
  import {
25
25
  DEFAULT_STORAGE_PREFIX,
26
26
  PRUNE_STRATEGY,
@@ -39,9 +39,13 @@ export declare class PortReaper {
39
39
  * setTimeout() overrides it. register() always calls schedule() so the
40
40
  * reaper interval starts as soon as the first port connects. */
41
41
  register(port: MessagePort, target: ReapTarget): void;
42
- /** Record activity on a port (any incoming message). No-op for untracked
43
- * ports — a late PING from a port that was already removed/STOP'd must not
44
- * resurrect it in the reaper's tracking maps. */
42
+ /** Record activity on a port (any incoming message). The early return is what
43
+ * keeps a late PING from a removed port out of `lastSeenAt`: `reap()` iterates
44
+ * `targets`, so a row here would be unreachable rather than dangerous — measured,
45
+ * deleting the guard passes every test in the suite, because the only difference is
46
+ * one stale map entry per dead port. It stays as the cheap invariant that the three
47
+ * maps hold exactly the tracked ports, which is what `reap()`'s `??` fallbacks and
48
+ * this file's tracking contract both lean on. */
45
49
  touch(port: MessagePort): void;
46
50
  /** Override a port's session timeout from its INIT heartbeat config.
47
51
  * `Infinity` is the documented way to disable the PING heartbeat, and it means
@@ -71,8 +75,13 @@ export declare class PortReaper {
71
75
  * be unit-tested in isolation from the timer plumbing. */
72
76
  private computeMinHeartbeat;
73
77
  /** Close sessions whose port has been silent longer than its timeout.
74
- * Iterates a snapshot so closing a target (which mutates `targets`) during
75
- * the loop cannot skip a subsequent entry or visit one twice. */
78
+ * Iterates a snapshot. That is defensive rather than load-bearing: `Map`
79
+ * iteration in JS is deletion-safe, so removing the current entry cannot skip the
80
+ * next one, and the only case a snapshot changes is a re-entrant `register()` from
81
+ * inside a target's own teardown — which a fresh port would survive anyway, because
82
+ * `register()` stamps `lastSeenAt` with the current time and a zero-age port is
83
+ * never past its timeout. Measured: iterating the live map instead passes every
84
+ * test, so do not read this line as pinned. */
76
85
  private reap;
77
86
  }
78
87
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;+DAE+D;AAC/D,MAAM,WAAW,UAAU;IACzB,MAAM,IAAI,IAAI,CAAC;IACf,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;;;oEAGgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD;;qDAEiD;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B;;;;;;;;;gDAS4C;IAC5C,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI;IAWhE;;4EAEwE;IACxE,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAQ/B;2EACuE;IACvE,OAAO,IAAI,IAAI;IAgBf;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ;IAahB;;8DAE0D;IAC1D,OAAO,CAAC,mBAAmB;IAQ3B;;qEAEiE;IACjE,OAAO,CAAC,IAAI;CAoDb"}
1
+ {"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;+DAE+D;AAC/D,MAAM,WAAW,UAAU;IACzB,MAAM,IAAI,IAAI,CAAC;IACf,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;;;oEAGgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD;;;;;;qDAMiD;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B;;;;;;;;;gDAS4C;IAC5C,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI;IAWhE;;4EAEwE;IACxE,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAQ/B;2EACuE;IACvE,OAAO,IAAI,IAAI;IAgBf;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ;IAahB;;8DAE0D;IAC1D,OAAO,CAAC,mBAAmB;IAQ3B;;;;;;;mDAO+C;IAC/C,OAAO,CAAC,IAAI;CAoDb"}
package/docs/api.md CHANGED
@@ -376,7 +376,7 @@ createStorageEventChannel(options: {
376
376
  }): ClusterChannel | null
377
377
  ```
378
378
 
379
- 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).
379
+ 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); the payload envelope carries a per-channel sender nonce **and** a monotonic sequence, which is what keeps consecutive identical messages deliverable — including two tabs whose first frame would otherwise store the same `seq=1` value and be silently suppressed. Opt in via `createBrowserEnvironment({ channelFallback: 'storage-event' })`; see the security note in [configuration.md](./configuration.md#coordination-channel-fallback-broadcastchannel-unavailable).
380
380
 
381
381
  ## WebSocket Transport Backend
382
382
 
@@ -85,7 +85,7 @@ Terms are explained in plain language; the code and the rest of this document us
85
85
  | Term | Short name in code | Plain-language meaning |
86
86
  |---|---|---|
87
87
  | **Topic** | `topic` | A named channel (e.g. `price.feed`) that applications subscribe to or publish on. |
88
- | **Topic key** | `topicKey` | An opaque 128-bit hash of the Topic name. The Topic name itself is never persisted in coordination storage. |
88
+ | **Topic key** | `topicKey` | An opaque 128-bit hash of the Topic name. The Topic name never appears in the worker, route or subscriber records; the two opt-in exceptions that do write plaintext are `replay.persistence` (IndexedDB) and `channelFallback: 'storage-event'`. |
89
89
  | **Tab** | `tabId` | One browser page instance. `tabId` survives refresh so a tab keeps its identity across the page lifecycle. |
90
90
  | **Worker** | `workerId` | One runtime instance inside a Tab. Each Worker publishes its own heartbeat and can own Topics. A Tab can briefly run two Workers during a restart/handoff. |
91
91
  | **Topic owner** | — | The Worker responsible for the real transport subscription of a Topic. "Owner" is a hat a Worker wears, not a permanent role: it receives the Topic's publications from the server and fans them out to other Tabs. |
@@ -117,9 +117,13 @@ Unlike the old single-JSON route table, subscribers use per-Tab independent keys
117
117
 
118
118
  ```ts
119
119
  interface WorkerRecord {
120
+ /** Cluster protocol version advertised by this worker; absent from a legacy peer. */
121
+ protocolVersion?: number;
120
122
  workerId: string;
121
123
  tabId: string;
122
124
  load: number;
125
+ /** Rolling traffic sample, present only when `loadWeighting` is enabled. */
126
+ throughput?: WorkerThroughputSample;
123
127
  role: 'active' | 'standby';
124
128
  status: 'connecting' | 'connected' | 'disconnected' | 'error';
125
129
  visibilityState: 'visible' | 'hidden';
@@ -146,7 +150,7 @@ interface WorkerRoute {
146
150
 
147
151
  `generation` increments on every re-assignment and must match across the handoff handshake; `handoffFromWorkerId` records the previous owner during a graceful handoff. The interface above matches the current protocol — see [Failover](#failover) for how these two fields drive takeover.
148
152
 
149
- Routes do not store the original topic string or payload. When the actual owner receives `CONTROL/SUBSCRIBE`, the original topic string is only passed through the BroadcastChannel in-memory message. The receiver accepts that control frame only when the durable route currently names it; a delayed frame from an earlier assignment round is dropped, and a route awaiting `ROUTE_RELEASED` can be confirmed only by the matching handoff ACK. `confirmedAt` is written after the owner processes the control message; before the route is confirmed, the subscriber Runtime holding the original topic string will resend `SUBSCRIBE` to recover from BroadcastChannel message loss that results in "a route without a real subscription".
153
+ Routes do not store the original topic string or payload. When the actual owner receives `CONTROL/SUBSCRIBE`, the topic plaintext reached it only through the coordination channel (in-memory on BroadcastChannel; part of the stored frame under `channelFallback: 'storage-event'`). The receiver drops that control frame when the durable route names a *different* worker, so a delayed frame from an earlier assignment round cannot make a non-owner subscribe, and a route awaiting `ROUTE_RELEASED` can be confirmed only by the matching handoff ACK. A topic whose route cannot be read is accepted rather than dropped — `confirmRoute` writes nothing without a route to stamp, so the frame mints no durable ownership, and the next reconcile withdraws the assignment; the tolerance exists so coordination still works when storage is absent or the record has expired. `confirmedAt` is stamped as the frame is accepted, **before** the control action is handed to the transport, so it records "this Worker took the assignment", not "the server subscription is live" — which is exactly why an unconfirmed route is re-sent: it is how "a route without a real subscription" is recovered from BroadcastChannel message loss.
150
154
 
151
155
  ### Receiver-side frame validation
152
156
 
@@ -166,7 +170,10 @@ The second row is what makes ownership authorization meaningful. `topicKey` is a
166
170
  substitution, not a variant — and because ownership is authorized by the route stored under `topicKey`
167
171
  while the transport subscription is named by `topic`, an unchecked mismatch lets a frame borrow one
168
172
  channel's authorization to name another. The durable route record stores only `topicKey`, never the
169
- plaintext, so the key → name mapping exists only in memory and cannot be injected through localStorage.
173
+ plaintext, so the key → name mapping exists only in memory and cannot be injected through the
174
+ coordination records. The storage-event fallback channel is the one path where a frame — and with it
175
+ a key/plaintext pair — does arrive through localStorage; the pairing rule is what makes a frame
176
+ written by any other tenant of that storage just as invalid as one posted into the channel.
170
177
 
171
178
  The third row exists because the batch path iterates its payload: a value with a `length` but no
172
179
  iterator threw out of the message listener, and an iterable non-array (a string) walked the batch as
@@ -195,6 +202,7 @@ These identifiers represent different layers:
195
202
  | `tabId` | Stable identity of a browser Tab | Identifies which Tab subscribes to a `topicKey` | Yes, in subscriber keys |
196
203
  | `workerId` | Identity of the current Runtime/Worker instance | Identifies the Worker that owns the transport subscription | Yes, in worker/route records |
197
204
  | `BroadcastChannel` | Same-origin, in-memory real-time channel | Carries control actions, publication events, and reconciliation signals | No |
205
+ | storage-event channel | The same channel interface over localStorage `storage` events, used only when `channelFallback: 'storage-event'` is set and BroadcastChannel is unavailable | Carries exactly the frames above | **Yes** — each frame is written whole under `cross-tab-worker-databus:channel:*`, so plaintext topics and `PUBLISH` payloads persist until the channel closes (indefinitely if the tab dies first) |
198
206
 
199
207
  ```text
200
208
  topic
@@ -211,7 +219,7 @@ BroadcastChannel CONTROL
211
219
 
212
220
  ### In-memory topic key cache (`knownTopics`)
213
221
 
214
- Each Runtime maintains a `Map<topicKey, topic>` called `knownTopics` that serves as the reverse-lookup cache from opaque key to plaintext topic. It is populated by `rememberTopic()`, which is called on every `subscribe`, `publish`, `unsubscribe`, and inbound `CONTROL` message.
222
+ Each Runtime maintains a `Map<topicKey, topic>` called `knownTopics` that serves as the reverse-lookup cache from opaque key to plaintext topic. It is populated by `rememberTopic()`, which is called on every `subscribe`, `publish`, and `unsubscribe`, and on every inbound `CONTROL` message that survives the `topicKey` pairing check — a frame dropped for disagreeing about that pair is dropped before its plaintext is ever read, so invalid pairs cannot grow this map. Legitimate calls still can, through any API a same-origin script may reach on the page's own bus, which is what the cap below bounds.
215
223
 
216
224
  The cache exists for two reasons:
217
225
 
@@ -302,7 +310,10 @@ This decentralized structure is a trade-off for concurrency correctness, not for
302
310
  | Single large JSON for Worker/route/subscriber | High | Only whole read/write | Multiple Tabs doing read-modify-write concurrently can easily overwrite each other, losing subscribers |
303
311
  | Independent key per entity | Low | Can clean up per Worker, per route, per Topic+Tab precisely | More keys, requires TTL-based garbage collection |
304
312
 
305
- The SDK does not rely on `storage` events to drive coordination; control notifications use BroadcastChannel. Although Worker heartbeats update their own independent key, this does not trigger repeated business callbacks or message dispatch within the SDK. The core benefit of separate keys is that different Tabs write different records, avoiding overwrite contention on a shared large object.
313
+ By default the SDK does not rely on `storage` events to drive coordination; control notifications use
314
+ BroadcastChannel. That is a default, not a guarantee: `channelFallback: 'storage-event'` selects a
315
+ channel implemented entirely on `storage` events, which is the one configuration where coordination
316
+ notifications do arrive that way. Although Worker heartbeats update their own independent key, this does not trigger repeated business callbacks or message dispatch within the SDK. The core benefit of separate keys is that different Tabs write different records, avoiding overwrite contention on a shared large object.
306
317
 
307
318
  Under normal conditions, the number of keys is approximately: `Number of Workers + Number of Topic routes + Number of Topic/Tab subscription relationships`. The Runtime cleans up timed-out Workers, orphaned subscribers without active Tabs, and orphaned routes that have exceeded the Worker TTL and no longer have subscribers. Other naming structures left over from older versions do not belong to the current SDK protocol and do not participate in current route resolution.
308
319
 
@@ -348,7 +359,11 @@ The two `topicKey → topic` maps (`assignedTopics`, `knownTopics`) deliberately
348
359
 
349
360
  ## BroadcastChannel Protocol
350
361
 
351
- All real-time coordination flows through one BroadcastChannel per cluster, whose name is derived from `clusterKey`. Messages on it exist only in memory: they never touch localStorage and never pass through the transport server. Four message types are exchanged:
362
+ All real-time coordination flows through one channel per cluster, whose name is derived from
363
+ `clusterKey`. BroadcastChannel — the implementation unless `channelFallback` is set — keeps its
364
+ messages in memory only; they never touch localStorage. The storage-event fallback is the exception
365
+ by construction: it *is* localStorage, so every frame on it persists as long as the key does. Neither
366
+ implementation passes through the transport server. Four message types are exchanged:
352
367
 
353
368
  | Type | Direction | Purpose |
354
369
  |---|---|---|
@@ -513,7 +528,12 @@ sequenceDiagram
513
528
 
514
529
  BroadcastChannel does not echo to its sender, so the owner receives no `EVENT` back for its own broadcast — its local dispatch is the only local delivery.
515
530
 
516
- Publications are not written to localStorage. Message data and publication metadata only exist in the BroadcastChannel in-memory event and within the transport; batch writes only cover coordination metadata.
531
+ Publications are not written to localStorage by the coordination layer or the transport; message data
532
+ and publication metadata exist in the BroadcastChannel in-memory event and in the transport, and batch
533
+ writes cover coordination metadata only. Two opt-ins do put payloads in browser storage, and each is
534
+ named where it is documented: the `channelFallback: 'storage-event'` channel writes every frame it
535
+ carries — including `PUBLISH` payloads — under its channel key, and `replay.persistence` stores the
536
+ publications themselves in IndexedDB, keyed by the plaintext topic.
517
537
 
518
538
  ### Service Worker boundary
519
539
 
@@ -588,7 +608,7 @@ These invariants are pinned by regression tests (see `tests/stability.test.ts` a
588
608
  - **Last-subscriber release.** An owning Worker learns a topic is unserved from the last subscriber's `CONTROL/UNSUBSCRIBE`: it drops the assignment *and* dispatches the release to its transport. Consuming the message inside the handoff short-circuit alone would leave the server subscription — and its inbound fan-out — alive after every tab has left.
589
609
  - **Assignment drift repair.** Every reconcile tick drops any topic this Worker holds in `assignedTopics` once the durable route no longer names it, releasing the transport subscription as it goes. Ownership thus follows the storage record rather than whichever message arrived last: a stale `CONTROL/SUBSCRIBE`, a lost route write, or a tab that claimed a route it does not own all converge back to exactly one owner within a heartbeat. `tests/coordination-invariants.test.ts` fuzzes three tabs through those corruptions and asserts the quiescent result (pinned by regression: emptying this sweep makes the fuzzed end state fail).
590
610
  - **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.
591
- - **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.
611
+ - **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 makes every write a distinct value — a per-channel sender nonce *and* a monotonic sequence in the envelope — because a browser suppresses the `storage` event when `setItem` leaves the stored value unchanged, and the nonce is what separates two tabs whose first identical frame would each be `seq=1`; a dropped dispatch still recovers through the same reconcile loop.
592
612
  - **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()`.
593
613
 
594
614
  ## Transport Reconnection
@@ -22,7 +22,7 @@ Status Legend: `✅ Implemented` means the current version has code and test cov
22
22
  | Degradation | Runs locally when localStorage or BroadcastChannel is unavailable | ✅ Implemented | Preserves the current Tab's connection and subscription capabilities |
23
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 |
24
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 |
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
+ | Security Boundary | localStorage uses opaque keys derived from connection and Topic; BroadcastChannel coordination messages carry plaintext topic names | ✅ Implemented | The coordination records themselves persist no URLs, credentials, raw Topic names, or publication payloads, and BroadcastChannel coordination messages are in-memory only (they carry plaintext topic names but are never written). Two opt-ins persist what the default path does not, and both are documented trade-offs: `replay.persistence` (IndexedDB) keys its object store by the plaintext topic and stores publication payloads in it, and `channelFallback: 'storage-event'` writes every coordination frame — plaintext Topic names, and payloads on `PUBLISH` frames — to localStorage under the channel key until that channel closes. |
26
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
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()` |
28
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 |
@@ -35,7 +35,8 @@ Status Legend: `✅ Implemented` means the current version has code and test cov
35
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 |
36
36
  | Runtime Model | SharedWorker / Dedicated Worker transport | ✅ Implemented | `workerMode` supports `dedicated`, `shared`, and `auto`, defaulting to `dedicated` |
37
37
  | Runtime Model | Service Worker transport | Not Implemented | Deliberately deferred; lifetime and long-lived connection constraints are documented in `docs/architecture.md` |
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 |
38
+ | Durable Messages | Replaying published payloads to a tab that loads later | ✅ Implemented | Opt-in: `createIndexedDbReplayPersistence` stores the publications themselves — business payloads included, under the plaintext topic as the object store's key — and they are re-hydrated on the next load, so history survives a reload or a page close. This is inbound replay for subscribers, not a delivery guarantee |
39
+ | Durable Messages | Re-sending outbound publish commands after a page closes | Not Implemented | No publish command is persisted; a publication that was already dropped (a transport disconnect window, or the local-mode degradation) is the one documented unrecoverable loss |
39
40
 
40
41
  ## Verified Guarantees (pinned by the regression suite)
41
42
 
@@ -46,10 +47,10 @@ and regression-locked:
46
47
 
47
48
  | Invariant | Area | Regressed guarantee |
48
49
  |---|---|---|
49
- | SUBSCRIBE route binding | coordination | An inbound `CONTROL/SUBSCRIBE` is honored only when the durable route currently names the receiver and is not awaiting `ROUTE_RELEASED`; delayed frames from earlier assignment rounds cannot create ownership, subscribe the transport, or confirm a pending handoff |
50
+ | SUBSCRIBE route binding | coordination | An inbound `CONTROL/SUBSCRIBE` is dropped when the durable route names a different worker, and a route awaiting `ROUTE_RELEASED` is confirmed only by the matching handoff ACK — so delayed frames from earlier assignment rounds cannot create lasting ownership, subscribe the transport, or confirm a pending handoff. A topic with no readable route is accepted rather than dropped, and that tolerance is bounded: `confirmRoute` writes nothing without a route to stamp, and the next reconcile takes the assignment back. It is what keeps coordination working when storage is unavailable or the record has expired |
50
51
  | 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 exactly matches the stored route — delayed ACKs from another a↔b ping-pong round are dropped |
51
52
  | 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, and `suspend()`/`stop()` discard the queued batch); cleared or stopped-session history is never re-appended by an in-flight flush |
52
- | 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 |
53
+ | 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`); the pass stops at the first failure, so keys queued behind it wait for that key's retry (four backoff passes, 50+100+200+400 ms) and then go through the same pass that drops the poisoned key — delayed, never permanently blocked; backoff resets once the queue drains or `clear()` cancels the retries |
53
54
  | 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 |
54
55
  | Recovery waiter lifetime | lifecycle | A recovery operation parked behind the transport gate captures the recovery cancellation generation; `stop()` / page-hide invalidates it, so an immediate explicit `start()` cannot be followed by a stale duplicate operation after the replacement transport re-establishes subscriptions. By contrast, an explicit `start()` that supersedes the automatic recovery timer preserves the gate and its parked operations if the manual open also fails, allowing a later automatic or demand-driven reopen to flush them |
55
56
  | 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,7 +54,7 @@ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced dur
54
54
 
55
55
  `replay.retentionSweepMs` optionally schedules the same cleanup on a periodic interval. It is useful for quiet topics whose old durable records should still expire; it requires `retentionMs` and a persistence adapter with `clearBefore()`. The timer follows visibility and lifecycle transitions and is disabled by default.
56
56
 
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. A batch flush still queued in a microtask when the bus suspends or stops is discarded with its lifecycle generation rather than starting a durable append after teardown.
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 are capped only where they grow: after a failed attempt the delay doubles and the doubled value is capped at 1600 ms, while `backoffMs` itself is waited verbatim as the first delay — so `backoffMs: 5000` waits 5000 ms, then 1600 ms, rather than being clamped to the ceiling; final failures retain the existing `onError` and reliability behavior. A batch flush still queued in a microtask when the bus suspends or stops is discarded with its lifecycle generation rather than starting a durable append after teardown.
58
58
 
59
59
  ### Replay Options
60
60
 
@@ -65,7 +65,7 @@ When `replay.retentionMs` is enabled, automatic durable cleanup is coalesced dur
65
65
  | `retentionMs` | `number` | — | Producer-timestamp retention window; history older than the cutoff is pruned through the adapter's `clearBefore` |
66
66
  | `pruneStrategy` | `'count' \| 'age' \| 'both'` | `'count'` | `count` caps each topic at `maxPerTopic`; `age` prunes timestamped history by `retentionMs` and caps timestamp-less legacy entries by `maxPerTopic`; `both` applies both. `age` without `retentionMs` has nothing to prune by and falls back to the count cap |
67
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 |
68
+ | `persistenceRetry` | `{ maxAttempts, backoffMs }` | `1` / `50` | Bounded retry for transient persistence failures; the delay doubles after each attempt and the doubled value is capped at 1600 ms, `backoffMs` being waited verbatim first |
69
69
 
70
70
  ### Deduplication Options
71
71
 
@@ -14,7 +14,8 @@ The package provides the following entry points:
14
14
  - `cross-tab-worker-databus/centrifuge`: the built-in Centrifuge Worker transport
15
15
  - `cross-tab-worker-databus/centrifuge.worker`: the Dedicated Worker build artifact, loaded by default by the built-in factory; typically no need to reference it directly
16
16
  - `cross-tab-worker-databus/centrifuge.shared.worker`: the SharedWorker build artifact, loaded by default by the built-in factory; typically no need to reference it directly
17
- - `cross-tab-worker-databus/hooks`: optional React hooks adapter (`useCrossTabDataBus`, `useCrossTabSubscription`, `useCrossTabStatus`); React (>= 18) is an optional peer dependency
17
+ - `cross-tab-worker-databus/hooks`: optional React hooks adapter (`useCrossTabDataBus`, `useCrossTabSubscription`, `useCrossTabStatus`, `useCrossTabHealth`); React (>= 18) is an optional peer dependency
18
+ - `cross-tab-worker-databus/vue`: optional Vue composables with the same four names (`useCrossTabDataBus`, `useCrossTabSubscription`, `useCrossTabStatus`, `useCrossTabHealth`); Vue (>= 3.3) is an optional peer dependency
18
19
  - `cross-tab-worker-databus` also exports a zero-dependency native `WebSocketTransport` / `createWebSocketDataBus` for servers that speak plain WebSockets
19
20
 
20
21
  The `cross-tab-worker-databus/centrifuge` entry point relies on the optional peer dependency `centrifuge` (^5.5.3). Install it alongside this package when using the built-in Centrifuge transport: `pnpm add centrifuge`.
@@ -46,7 +47,7 @@ export const dataBus = createCentrifugeDataBus<ResourceEvent>({
46
47
 
47
48
  `clusterKey` is derived from the connection URL by default. It is only used for cluster isolation and is converted to an opaque key before entering localStorage and as the BroadcastChannel channel name. Note that topic names and event types sent over the BroadcastChannel coordination channel are transmitted in plaintext; only localStorage metadata is obfuscated via hashing.
48
49
 
49
- By default, a Dedicated Worker is used, one Worker per Tab. To have same-origin Tabs reuse a single connection, set `workerMode: 'shared'` or `'auto'`:
50
+ By default, a Dedicated Worker is used, one Worker per Tab. `workerMode: 'shared'` (or `'auto'`, which tries shared first) puts same-origin Tabs into **one** SharedWorker process — but it does not merge their connections. Each connecting port gets its own `CentrifugeSession` and so its own WebSocket, which is why closing or refreshing one tab leaves the others subscribed: N tabs still mean N server-side connections and N channel subscriptions. Shared mode saves the Worker process, not the socket:
50
51
 
51
52
  ```ts
52
53
  export const dataBus = createCentrifugeDataBus<ResourceEvent>({
@@ -144,7 +145,7 @@ pnpm examples
144
145
 
145
146
  Open `http://localhost:4173/examples/demo/` and open it in multiple browser Tabs at the same time to observe cross-Tab data flow. The page supports:
146
147
 
147
- - Connecting to the public Centrifugo demo address `wss://faye.centrifugal.dev/connection/websocket` by default
148
+ - Connecting to the demo's own local Centrifugo endpoint by default (`<scheme>://<host>/centrifuge/demo/connection/websocket`, seeded into the address box on load); the public `wss://faye.centrifugal.dev/connection/websocket` is one of the selectable presets, not the default
148
149
  - Modifying the WSS address, `workerMode`, Topic, and `transferable` configuration in the page
149
150
  - Switching to "Local Broadcast" mode, which does not depend on an external server and demonstrates multi-Tab collaboration using only BroadcastChannel
150
151
  - Data flow animations, event stream, distribution latency metrics, and cluster Worker routing status
@@ -11,9 +11,9 @@ Use this checklist for each pre-1.0 release. Pushing the version tag *is* the pu
11
11
 
12
12
  ## Automated gates (CI)
13
13
 
14
- The `CI` workflow's `verify` job runs `pnpm check`, `pnpm lint`, `pnpm test:coverage` (the `vitest.config.ts` floors: 98% statements / 96% branches / 98% functions / 99% lines), `pnpm verify:compat`, `pnpm verify:pack`, `pnpm bench`, and `pnpm audit` on every push and pull request; the `browser` job runs the Playwright E2E suite. The `Release` workflow repeats `verify:compat` and `verify:pack` before publishing, then runs the blocking `verify:published` gate. The checkout in both workflows uses `fetch-depth: 0` + `fetch-tags: true` because `verify:compat` resolves its baseline from the most recent release tag.
14
+ The `CI` workflow's `verify` job runs `pnpm check`, `pnpm lint`, `pnpm test:coverage` (the `vitest.config.ts` floors: 98% statements / 96% branches / 98% functions / 99% lines), `pnpm verify:compat`, `pnpm verify:pack`, `pnpm bench`, and `pnpm audit` on pushes and pull requests **that target `main`** — both triggers are filtered to `branches: [main]` in `ci.yml`, so pushing a topic branch runs nothing and CI first covers the work when its PR opens (a PR based on another branch is not covered either); the `browser` job runs the Playwright E2E suite. The `Release` workflow repeats `verify:compat` and `verify:pack` before publishing, then runs the blocking `verify:published` gate. The checkout in both workflows uses `fetch-depth: 0` + `fetch-tags: true` because `verify:compat` resolves its baseline from the most recent release tag.
15
15
 
16
- Only `pnpm bench:browser` / `pnpm bench:compare` stay local-only: shared-runner timing noise makes numeric CI gates unreliable.
16
+ Only `pnpm bench:browser` / `pnpm bench:compare` / `pnpm bench:trend` stay local-only: shared-runner timing noise makes numeric CI gates unreliable, and `bench:trend` rewrites `docs/benchmarks.md` from the local archive, which CI must not do.
17
17
 
18
18
  ## Before tagging
19
19
 
@@ -30,7 +30,7 @@ The repository runs CodeQL (`javascript-typescript`; on push, on pull request, a
30
30
 
31
31
  ## Tagged-release workflow
32
32
 
33
- Pushing a version tag triggers the `Release` GitHub Action: it runs `pnpm check` and `pnpm lint` (a tag can point at a commit that never passed CI's lint step), runs `verify:compat` and `verify:pack`, opens the GitHub release from the `CHANGELOG` section, publishes to npm when the `NPM_TOKEN` secret is set, and then runs the **blocking** published-consumer verification with the same budget as a manual run (`PUBLISHED_VERIFY_ATTEMPTS=48`, `PUBLISHED_VERIFY_DELAY_MS=7500`, a 6-minute ceiling). A release whose published package cannot be imported by a clean consumer fails the workflow — treat every `verify:published` failure as a failed release. For registry propagation or infrastructure failures, rerun the workflow against the unchanged tag; for artifact defects, ship a new patch version. Never move or reuse a published tag. When no token is configured the publish step is skipped, but verification still passes against whatever version is already on npm (e.g. one published manually).
33
+ Pushing a version tag triggers the `Release` GitHub Action: it runs `pnpm check` and `pnpm lint` (a tag can point at a commit that never passed CI's lint step), runs `verify:compat` and `verify:pack`, opens the GitHub release from the `CHANGELOG` section, publishes to npm when the `NPM_TOKEN` secret is set, and then runs the **blocking** published-consumer verification at a budget **twelve times the manual default** — the workflow sets `PUBLISHED_VERIFY_ATTEMPTS=48` and `PUBLISHED_VERIFY_DELAY_MS=7500` (a ~6-minute ceiling) in its own env, while `scripts/verify-published-consumer.mjs` defaults to 6 × 5000 ms = 30 s when run by hand. A release whose published package cannot be imported by a clean consumer fails the workflow — treat every `verify:published` failure as a failed release. For registry propagation or infrastructure failures, rerun the workflow against the unchanged tag; for artifact defects, ship a new patch version. Never move or reuse a published tag. When no token is configured the publish step is skipped, but verification still passes against whatever version is already on npm (e.g. one published manually).
34
34
 
35
35
  ## Publishing
36
36
 
@@ -42,4 +42,4 @@ For a manual release (no `NPM_TOKEN` in the workflow), run `npm publish --access
42
42
  2. Install the published tarball or version in a clean consumer and import the root plus every public subpath.
43
43
  3. Record the result in the release notes. Do not advance to `1.0.0` until the public API and protocol deprecation policy are explicitly frozen.
44
44
 
45
- The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. The workflow's 6-minute ceiling absorbs normal npm CDN propagation lag (the 0.20.89 tag run exhausted the older 2-minute budget after a successful publish); raise `PUBLISHED_VERIFY_ATTEMPTS` / `PUBLISHED_VERIFY_DELAY_MS` further only for unusually slow mirrors.
45
+ The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. The workflow's 6-minute ceiling absorbs normal npm CDN propagation lag (the 0.20.89 tag run exhausted the older 2-minute budget after a successful publish); raise `PUBLISHED_VERIFY_ATTEMPTS` / `PUBLISHED_VERIFY_DELAY_MS` further only for unusually slow mirrors. Note the asymmetry when repeating by hand: `verify-published-consumer.mjs` defaults to `6` attempts × `5_000` ms — 30 seconds, not 6 minutes — because the tag run passes the larger pair as env rather than the script owning it. So a manual repeat can report `ETARGET` on a version whose CI run was green, which reads like a failed publish and is not one; reproduce the CI budget explicitly with `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=48 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`, or judge a manual failure against the 30 s budget you actually ran. Expect ~39 retries of 48 in a *green* tag run: registry propagation, not a symptom.
package/docs/roadmap.md CHANGED
@@ -1,6 +1,64 @@
1
1
  # Roadmap
2
2
 
3
- 0.21.10 was released on September 23, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
3
+ 0.21.12 was released on September 23, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.21.12 delivered scope
6
+
7
+ - The prose sweep finished where it started: the README. `README.md` and `README.zh.md` promised
8
+ "the persistence layer does not store connection addresses, raw Topic text, or message content"
9
+ two bullets under the line advertising the durable replay persistence that does precisely that —
10
+ its IndexedDB store is created with `keyPath: 'topic'` and its rows carry the publication payloads,
11
+ and the factory is a public export. The claim is now scoped to the coordination records it is true
12
+ of, and both opt-ins that persist real content are named beside it. The Chinese list also gained
13
+ the two feature bullets it never had, so the promise no longer stands without its disclosure.
14
+ - `docs/release-checklist.md`, which ships in the tarball, held three traps. The published-consumer
15
+ gate was said to run "with the same budget as a manual run" — the workflow passes 48 × 7500 ms as
16
+ its own env while the script defaults to 6 × 5000 ms, so a hand-run repeat hits `ETARGET` after 30
17
+ seconds on a tag whose CI run was green. Measured on `v0.21.11`: 39 retries of 48 in a successful
18
+ run, and the offline repeat passed first try. Both budgets are now stated, with the command that
19
+ reproduces the CI one. CI was described as running on "every push and pull request" although both
20
+ triggers are filtered to `branches: [main]`, and `bench:trend` was missing from the local-only list
21
+ despite rewriting `docs/benchmarks.md` from the local archive.
22
+ - The `ci.yml` comment naming the coverage floors still said 96/92/96/97 after 0.21.7 raised them to
23
+ 98/96/98/99; the config, the shipped checklist and `AGENTS.md` already agreed, so only the comment
24
+ had drifted. Corrected, and `AGENTS.md` now records how the sweep concluded: an audit of "the option
25
+ surface" covers the tables, while the paragraph under the same heading and the README's summary
26
+ bullets are separate claims — and the sentence shape worth hunting first is an absolute about
27
+ storage, because one opt-in is enough to break it.
28
+ - No behaviour, wire-format, storage-layout or cluster-protocol change, and no `src/` or `tests/`
29
+ file was touched. Coverage stays at 99.01 / 96.90 / 99.26 / 99.69 over **895** tests.
30
+ ## 0.21.11 delivered scope
31
+
32
+ - A prose audit of the four docs files no earlier sweep had covered — `architecture.md`,
33
+ `capabilities.md`, `getting-started.md`, `transports.md`, plus the `api.md` entry they lean on —
34
+ produced roughly fifteen sentences the code does not support, corrected in English and Chinese. The
35
+ load-bearing ones are all of the "never persists" family. `architecture.md` promised in six sentences
36
+ that coordination traffic never touches localStorage, which `channelFallback: 'storage-event'`
37
+ contradicts by construction: the envelope *is* the frame, so plaintext topics and publication
38
+ payloads live under the channel key until the channel closes and indefinitely after a tab crash. The
39
+ matrix said the SDK does not persist business payloads, which its own exported
40
+ `createIndexedDbReplayPersistence` disproves (its object store is keyed by the plaintext topic), and
41
+ it sold `workerMode: 'shared'` as reusing one connection when every port gets its own
42
+ `CentrifugeSession` and its own WebSocket — shared mode saves the process, never the socket.
43
+ - Two instructions written for people implementing a transport pointed at the wrong callback and the
44
+ wrong guard. Auto-recovery is started by `onStatus('error')` and paced by the recovery cooldown;
45
+ `onError` records and notifies and does nothing else, so a backend that reports a dead socket only
46
+ there is never reopened. The generation counter is compared only by the asynchronous credential
47
+ bridge — a superseded Worker cannot reach the transport, because its listeners are removed first.
48
+ The same file's interface block, Worker unions and wire sketch each omitted members that ship
49
+ (`publish`'s metadata argument, optional `publishBatch`, the diagnostics labels, the `TOKEN_*` and
50
+ `SESSION_REAPED` types, the batch frame).
51
+ - One over-absolute claim sat in three places, a `cluster.ts` comment among them: the route check
52
+ drops a `CONTROL/SUBSCRIBE` naming a *different* worker, and a topic whose route cannot be read is
53
+ accepted. The tolerance is bounded — `confirmRoute` writes nothing without a route to stamp and the
54
+ next reconcile withdraws the assignment — and is now pinned by a test. Measured, the stricter rule
55
+ the prose described fails that test *and* two load-weighting tests that already relied on it.
56
+ - Also in this release, from the same "measure the claim rather than re-read it" pass: the
57
+ persistence-retry sentence in `configuration.md` now says which half the 1600 ms ceiling bounds, two
58
+ tests carry the full doubling schedule and the verbatim first wait, and two `PortReaper` comments
59
+ describe what their legs actually protect.
60
+ - No behaviour, wire-format, storage-layout or cluster-protocol change. Whole-tree coverage is
61
+ unchanged at 99.01 / 96.90 / 99.26 / 99.69 over **895** tests.
4
62
 
5
63
  ## 0.21.10 delivered scope
6
64
 
@@ -15,10 +15,20 @@ idempotent — the DataBus may call them repeatedly and replays them on reconnec
15
15
 
16
16
  ```ts
17
17
  interface DataBusTransport<TConfig = unknown, TData = unknown> {
18
+ /** Optional identity/backend labels surfaced through diagnostics. */
19
+ readonly diagnosticsName?: string;
20
+ readonly diagnosticsBackend?: string;
18
21
  start(config: TConfig, handlers: DataBusTransportHandlers<TData>): MaybePromise<void>;
19
22
  subscribe(topic: string): MaybePromise<void>;
20
23
  unsubscribe(topic: string): MaybePromise<void>;
21
- publish(topic: string, data: unknown): MaybePromise<void>;
24
+ /** `options` carries the caller's `{ messageId, timestamp }`; a backend that
25
+ * drops it silently strips dedup identity and producer timestamps from peers. */
26
+ publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void>;
27
+ /** Optional: one frame for a burst. Without it the DataBus loops `publish`.
28
+ * Each item is `{ data, messageId?, timestamp? }` — the same metadata `publish`
29
+ * takes, per entry. (That entry type is internal: name it inline, or widen the
30
+ * `ReadonlyArray` to whatever your backend accepts.) */
31
+ publishBatch?(topic: string, items: ReadonlyArray<{ data: unknown; messageId?: string; timestamp?: number }>): MaybePromise<void>;
22
32
  stop(): MaybePromise<void>;
23
33
  }
24
34
 
@@ -32,8 +42,12 @@ interface DataBusTransportHandlers<TData = unknown> {
32
42
  `start()` receives the user-supplied connection config (untyped `TConfig` — the
33
43
  backend owns its shape) and the three callbacks. Call `onStatus` whenever the
34
44
  connection state changes; call `onMessage` for each inbound publication; call
35
- `onError` for non-fatal errors (the DataBus applies a recovery cooldown so a
36
- flapping connection does not retry-loop).
45
+ `onError` for non-fatal errors. Only `onStatus('error')` starts auto-recovery —
46
+ the DataBus paces that reopen with a recovery cooldown and bounds it with
47
+ `recovery.maxAttempts`, so a flapping connection does not retry-loop. `onError`
48
+ records the failure for `getHealthSummary().lastFailure` and notifies subscribers,
49
+ and nothing else: a backend that reports a dead socket through `onError` alone
50
+ is never reopened.
37
51
 
38
52
  `start()` MUST settle its returned promise only once the backend is connected,
39
53
  and reject it when the attempt fails. The DataBus uses that settlement as its
@@ -65,8 +79,11 @@ transport only owns the I/O path: connect, subscribe, publish, disconnect.
65
79
 
66
80
  Mirror the Centrifuge backend's `centrifuge-protocol.ts`: a discriminated union
67
81
  of messages the main thread sends to the Worker (`INIT` / `SUBSCRIBE` /
68
- `UNSUBSCRIBE` / `PUBLISH` / `PUBLISH_BIN` / `PING` / `STOP`) and a union the
69
- Worker posts back (`STATUS` / `MESSAGE` / `MESSAGE_BIN` / `ERROR`). Keep it
82
+ `UNSUBSCRIBE` / `PUBLISH` / `PUBLISH_BIN` / `PING` / `STOP`, plus
83
+ `TOKEN_RESPONSE` / `TOKEN_ERROR` answering a credential request) and a union the
84
+ Worker posts back (`STATUS` / `MESSAGE` / `MESSAGE_BIN` / `ERROR`, plus
85
+ `TOKEN_REQUEST` for the credential bridge and `SESSION_REAPED` when a SharedWorker
86
+ port is reclaimed). Keep it
70
87
  structured-cloneable (no functions,
71
88
  no class instances — `Error` must be serialised).
72
89
 
@@ -92,9 +109,13 @@ reference shape, including:
92
109
 
93
110
  - **Backend selection**: reuse `selectWorkerBackend` from `worker-mode.ts` so
94
111
  your backend degrades consistently with the rest of the SDK.
95
- - **Generation guard**: bump a monotonic counter when a backend is created;
96
- error handlers check it so late errors from a superseded Worker cannot
97
- corrupt the fresh session.
112
+ - **Generation guard**: bump a monotonic counter when a backend is created or the
113
+ transport stops. Only the asynchronous credential bridge compares it, because a
114
+ provider can settle after the Worker it was answering is gone. Worker error
115
+ handlers read no such counter in the reference implementation — `stop()` and
116
+ `onWorkerFailed()` remove those listeners before the generation moves on, so a
117
+ superseded backend cannot reach the transport at all. If your backend keeps a
118
+ listener alive across a swap, that is when you need the check yourself.
98
119
  - **SharedWorker heartbeat**: if you use a SharedWorker, send periodic PINGs
99
120
  so a `PortReaper` can reclaim dead-tab sessions, and handle the
100
121
  `SESSION_REAPED` message the reaper posts before it closes a port. That
@@ -153,6 +174,7 @@ const bus = createWebSocketDataBus({
153
174
  Wire protocol (JSON text frames):
154
175
 
155
176
  - client → server: `{"op":"subscribe"|"unsubscribe"|"publish","topic":"...","data":...,"messageId"?:"...","timestamp"?:123}`
177
+ - client → server (batched): `{"op":"publishBatch","topic":"...","items":[{data,...,"messageId"?,"timestamp"?}]}` — one frame whose entries the server re-fans out as individual publications. Binary payloads ride inside it as byte arrays. A one-item batch is sent as a plain `publish` frame instead, so a server that implements only `publish` still sees the legacy shape; an empty batch is dropped before it reaches the socket. A batch frame on a closed socket is reported through `handlers.onError` and dropped.
156
178
  - server → client: the canonical publication is `{"op":"publication","publication":{"topic":"...","data":...,"messageId"?:"...","timestamp"?:123}}`. The legacy flat `{"topic":"...","data":...}` frame remains accepted. Frames without a string topic are ignored; malformed JSON is reported through `handlers.onError` without throwing.
157
179
  - A publication that carries its own string `topic` is addressed by **that** value rather than by the channel it arrived on — that is how a server delivering through a wildcard channel (`chat.*`) names the concrete topic. A payload whose top level happens to contain a `topic` string is therefore re-addressed, and dropped when no tab owns the resulting topic. On Centrifuge the channel normally arrives out of band, which is why the rule needs stating here: it is the one transport where "the topic was already in the frame" is not otherwise visible.
158
180
 
package/docs/zh/api.md CHANGED
@@ -375,7 +375,7 @@ createStorageEventChannel(options: {
375
375
  }): ClusterChannel | null
376
376
  ```
377
377
 
378
- 创建以 localStorage `storage` 事件为载体的 `ClusterChannel`——面向无 BroadcastChannel 环境的协调降级通道。storage 或 storage-event 来源缺失时返回 `null`。投递语义与 BroadcastChannel 一致(不回显给发送方、消息可 JSON 序列化、关闭后拒绝再写入);载荷信封内的单调序列号保证连续相同消息仍可投递。通过 `createBrowserEnvironment({ channelFallback: 'storage-event' })` 启用;安全权衡见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
378
+ 创建以 localStorage `storage` 事件为载体的 `ClusterChannel`——面向无 BroadcastChannel 环境的协调降级通道。storage 或 storage-event 来源缺失时返回 `null`。投递语义与 BroadcastChannel 一致(不回显给发送方、消息可 JSON 序列化、关闭后拒绝再写入);载荷信封里同时带一个**每通道 sender nonce** 和一个单调序列号,正是这两者共同保证连续相同的消息仍可投递——包括两个 Tab 各自的第一帧都会写入相同的 `seq=1`、否则会被静默抑制的情况。通过 `createBrowserEnvironment({ channelFallback: 'storage-event' })` 启用;安全权衡见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
379
379
 
380
380
  ## WebSocket 传输后端
381
381