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.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -2
- package/README.zh.md +3 -1
- package/dist/centrifuge.js +1 -1
- package/dist/centrifuge.shared.worker.js +14 -5
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/{chunk-L5P66THO.js → chunk-TSTV3SDC.js} +2 -2
- package/dist/chunk-TSTV3SDC.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +1 -1
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/workers/port-reaper.d.ts +14 -5
- package/dist/workers/port-reaper.d.ts.map +1 -1
- package/docs/api.md +1 -1
- package/docs/architecture.md +28 -8
- package/docs/capabilities.md +5 -4
- package/docs/configuration.md +2 -2
- package/docs/getting-started.md +4 -3
- package/docs/release-checklist.md +4 -4
- package/docs/roadmap.md +59 -1
- package/docs/transports.md +30 -8
- package/docs/zh/api.md +1 -1
- package/docs/zh/architecture.md +13 -8
- package/docs/zh/capabilities.md +4 -3
- package/docs/zh/configuration.md +2 -2
- package/docs/zh/getting-started.md +4 -3
- package/docs/zh/release-checklist.md +4 -4
- package/docs/zh/roadmap.md +43 -1
- package/docs/zh/transports.md +16 -6
- package/package.json +1 -1
- package/dist/chunk-L5P66THO.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
## [0.21.12] - 2026-09-23
|
|
5
|
+
|
|
6
|
+
### Documentation
|
|
7
|
+
- `README.md` and `README.zh.md` promised that "the persistence layer does not store connection addresses, raw Topic text, or message content" — two bullets below the line advertising the durable replay persistence that does exactly that. `replay-persistence.ts` creates its IndexedDB object store with `keyPath: 'topic'` and writes `{ topic, messages }` rows, so the plaintext topic is the store key and the publication payloads are its values, and `createIndexedDbReplayPersistence` is a public export named by the published-consumer verification script. The sentence now scopes itself to what it is true of (the worker/route/subscriber coordination records) and names both opt-ins that persist real content: `replay.persistence`, and `channelFallback: 'storage-event'`, which writes every coordination frame it carries — topic plaintext, and payloads on `PUBLISH` frames — into localStorage until the channel closes. The Chinese README additionally listed neither feature, so its reader received the privacy promise with no disclosure paired to it; both bullets are now present in both languages. This closes the `docs/` + README prose surface that the 0.21.11 sweep had named as unread; the per-file typedoc in `src/**` ships by a different route (bundled into `dist/`) and remains unaudited. Fourth release in this class after 0.21.7, 0.21.8 and 0.21.11 — and the second in a row.
|
|
8
|
+
- `docs/release-checklist.md` (and its Chinese counterpart, which also ships) carried three claims that would misdirect a maintainer at the moment they most need the file to be accurate. **The published-consumer gate was documented as running "with the same budget as a manual run"; it does not** — the workflow passes `PUBLISHED_VERIFY_ATTEMPTS=48` / `PUBLISHED_VERIFY_DELAY_MS=7500` as its own env while `scripts/verify-published-consumer.mjs` defaults to 6 × 5000 ms, so a hand-run repeat reports `ETARGET` after 30 seconds against a tag whose CI run was green, which is indistinguishable from a failed publish. Measured on this very release: `v0.21.11` took 39 retries of 48 in a successful run and the offline repeat passed on its first attempt, so the checklist now states both budgets and how to reproduce the CI one. **CI was said to run "on every push and pull request"**, while both triggers in `ci.yml` are filtered to `branches: [main]` — a topic-branch push runs nothing, and the work is first covered when its PR opens. **And `bench:trend` was missing from the local-only list**, though it rewrites `docs/benchmarks.md` from the local archive and appears in no workflow, so a maintainer could conclude CI refreshes the trend doc.
|
|
9
|
+
- The coverage-floors comment in `ci.yml` still named the pre-0.21.7 values (96/92/96/97) as what `vitest.config.ts` carries; the config, the shipped checklist and `AGENTS.md` all say 98/96/98/99, and only the workflow comment had drifted. Corrected. No behaviour, wire-format, storage-layout or cluster-protocol change, and no coverage movement: this release touches no `src/` or `tests/` file.
|
|
10
|
+
|
|
11
|
+
## [0.21.11] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
- Two comments in `src/workers/port-reaper.ts` described mechanisms the code does not have, and both were corrected against a measurement rather than a re-read. `touch()`'s early return claimed to stop a late PING "resurrecting" a removed port in the reaper's tracking — deleting the guard instead passes the whole suite, because `reap()` iterates `targets`, so the only difference is one stale `lastSeenAt` row per dead port; the comment now says that, and says why the guard is still worth having (it is what keeps the three maps holding exactly the tracked ports, which is the enumeration `reap()`'s `??` fallbacks rest on). And `reap()`'s `Array.from(…)` snapshot claimed to prevent "skipping a subsequent entry or visiting one twice" — `Map` iteration is already deletion-safe, iterating the live map passes every test, and the one case a snapshot changes (a re-entrant `register()` during a teardown) is neutralised by `register()` stamping `lastSeenAt` with the current time. It stays, now labelled defensive rather than load-bearing. No behaviour change; these comments are bundled into `dist/centrifuge.shared.worker.js`, which is why they are recorded rather than edited silently.
|
|
15
|
+
- Recorded so the next pass does not repeat the work: a seeded interleaving harness for this class was written, run, and deleted. Four invariants over register/touch/configure/remove/advance with an injected clock explored 2,000 seeds green in 331 ms (~6,000 seeds/s), then twelve mutants showed it killed none that the 19 existing unit tests do not also kill — nine die to both, `schedule()`'s idempotency guard dies only to the unit tests' `expect(sets).toEqual([])`, and the two survivors are the map-hygiene and snapshot cases above. An interleaving sweep feels like more coverage because it visits more states; the question that decides it is whether any mutant dies to it alone. `AGENTS.md` carries that rule.
|
|
16
|
+
- `docs/configuration.md` and `docs/zh/configuration.md` said the replay persistence retry delays "grow exponentially and are capped". Only the *growth* is capped. `withPersistenceRetry()` seeds its first wait from `replay.persistenceRetry.backoffMs` and applies `Math.min(delay * 2, 1_600)` **after** that wait, so a configured `backoffMs` above the ceiling is waited in full and the schedule then falls to it — with `backoffMs: 5000` the waits are 5000, 1600, 1600, which does not grow at all. `assertPersistenceRetryOptions` accepts any non-negative finite number, so this is a reachable configuration rather than a misuse. The sentence now says which of the two the ceiling bounds and names the measured sequence. Reading the old one the other way — clamping the first wait as well — was considered and rejected: it would silently ignore a value the integrator set, in the opposite direction from what `backoffMs` is documented to mean. No behaviour change.
|
|
17
|
+
- `docs/architecture.md` claimed, in six sentences (each mirrored in Chinese), that coordination traffic "never touch[es] localStorage" and that "publications are not written to localStorage". Under the opt-in `channelFallback: 'storage-event'` the opposite is structural: the envelope *is* the frame, so every `CONTROL` — plaintext topic and `PUBLISH` payload included — is written whole under `cross-tab-worker-databus:channel:*` and stays until the channel closes, which after a tab crash means indefinitely. Four other docs files already stated that trade-off; architecture.md was the outlier, and it is the file a security or data-residency review reads. It also carried a `WorkerRecord` block that omitted `protocolVersion` and `throughput` under a sentence asserting the block "matches the current protocol"; placed `rememberTopic()` on *every* inbound `CONTROL` message (it runs only on those surviving the `topicKey` pairing check, so an invalid pair cannot grow the map); said `confirmedAt` is written "after the owner processes the control message" (it is stamped before the action reaches the transport, which is precisely why an unconfirmed route gets re-sent); and attributed the storage-event envelope's delivery to "a monotonic sequence" when `environment.ts` names the sender nonce as the part that separates two tabs whose first frame is each `seq=1`.
|
|
18
|
+
- The capabilities matrix held the same shape twice more. "Does not persist URLs, raw Topic names, credentials, or publication payloads" is true of the coordination records and false of the exported `createIndexedDbReplayPersistence`, whose object store uses the plaintext topic as its `keyPath` and holds payloads in its rows; and "Durable Messages | Not Implemented | The SDK does not persist business payloads" conflated two capabilities that go opposite ways — outbound publish commands are indeed never persisted, inbound replay history is. That row is now two, each with its own status. The storage-recovery row said a failing key is dropped "without blocking other queued keys", while `flush()` `break`s at the first failure, so everything queued behind one poisoned key waits through four backoff passes (50+100+200+400 ms) before the pass that drops it.
|
|
19
|
+
- `docs/getting-started.md` sold `workerMode: 'shared'` as letting same-origin Tabs "reuse a single connection". Each connecting port gets its own `CentrifugeSession` and therefore its own WebSocket — that per-port isolation is the reason closing one tab leaves the others subscribed — so N Tabs are still N server connections and N channel subscriptions; what shared mode saves is the Worker process. For anyone writing a transport, `docs/transports.md` pointed at the wrong callback and the wrong guard: the recovery cooldown is driven by `onStatus('error')`, while `onError` only records and notifies, so a backend that reports a dead socket through `onError` alone is never reopened; and the generation counter is compared only by the asynchronous credential bridge, because `stop()`/`onWorkerFailed()` remove a superseded backend's listeners before it moves on. The same file's interface block omitted `publish`'s metadata argument, the optional `publishBatch`, and the two diagnostics labels, its Worker unions omitted `TOKEN_REQUEST`/`TOKEN_RESPONSE`/`TOKEN_ERROR`/`SESSION_REAPED`, and its wire sketch omitted the `publishBatch` frame that both the bundled transport and the demo server speak. The demo also defaults to its own local endpoint, not the public Centrifugo URL the page listed as the default (and which its own next paragraph already assumed was local), and the entry-point list omitted the `/vue` export plus the fourth hook each adapter exports. Corrected in both languages.
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
- The one test that claimed to pin the retry ceiling never reached it. `honors the backoff delay between attempts, capped by the retry ceiling` advanced 50 ms then 100 ms from a `backoffMs` of 50, so the ceiling in its name was doing nothing and the sentence above had no witness in either direction. It is now `doubles the backoff delay between attempts`, which is what its assertions show, and two new tests carry the cap: `caps the doubled delay at the retry ceiling and holds it there` walks the whole schedule 50 → 100 → 200 → 400 → 800 → 1600 → 1600 with a 1 ms boundary probe on each step, and `waits the configured backoffMs verbatim even when it exceeds the ceiling` checks that a 5000 ms `backoffMs` is not clamped. Three mutants, each measured: deleting the cap kills **both** new tests (`expected 7 to be 8` on the last row, `expected 2 to be 3` on the third), clamping the initial delay kills only the verbatim test, and deleting the doubling kills only the ceiling test — which is the evidence that the two pin different halves and neither is a spare.
|
|
23
|
+
|
|
24
|
+
- `handleControlMessage`'s route check is `route && route.workerId !== this.workerId`, so a topic whose route cannot be read is **accepted**; three descriptions claimed the stricter "only when the route names the receiver". `accepts a CONTROL/SUBSCRIBE that has no durable route to check, then sweeps it` now pins both halves of what actually makes that safe — `confirmRoute` writes nothing without a route to stamp, so no durable ownership is minted, and the next reconcile withdraws the assignment. Measured: the strict reading fails this test *and two* pre-existing load-weighting tests that already depended on the tolerance, and deleting the sweep's route check fails only the new one.
|
|
25
|
+
- `stores the frame verbatim, which is the trade-off the docs price the fallback at` asserts what storage ends up holding after one `PUBLISH` frame on a storage-event channel — the topic plaintext and the payload, both present, and both gone after `close()`. No delivery test can see this class of claim, which is how six sentences in `architecture.md` stayed wrong while every fallback test in the suite stayed green.
|
|
26
|
+
|
|
3
27
|
## [0.21.10] - 2026-09-23
|
|
4
28
|
|
|
5
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ By default each tab holds its own Dedicated Worker; when configured with `worker
|
|
|
38
38
|
- Opt-in coordination fallback over localStorage storage events when BroadcastChannel is unavailable
|
|
39
39
|
- After a tab exits abnormally, automatic migration happens via heartbeat TTL
|
|
40
40
|
- Automatically degrades to local mode when BroadcastChannel or localStorage is unavailable
|
|
41
|
-
- The
|
|
41
|
+
- The coordination records in localStorage hold opaque keys only — no connection addresses, raw Topic text, credentials, or message content. Two opt-ins persist real content and are documented as trade-offs, not exceptions you can ignore: `replay.persistence` writes published payloads to IndexedDB in a store keyed by the plaintext Topic name, and `channelFallback: 'storage-event'` writes every coordination frame it carries — Topic names, and payloads on publications — into localStorage until that channel closes
|
|
42
42
|
|
|
43
43
|
See the [Capabilities Matrix](./docs/capabilities.md) for the full list of implemented, unimplemented, and planned capabilities.
|
|
44
44
|
|
|
@@ -221,7 +221,7 @@ Only if you use the built-in Centrifuge backend (`cross-tab-worker-databus/centr
|
|
|
221
221
|
|
|
222
222
|
```bash
|
|
223
223
|
pnpm install
|
|
224
|
-
pnpm check # typecheck + unit tests +
|
|
224
|
+
pnpm check # typecheck + build + unit tests + the perf gates (run separately from `pnpm test`, and timing-sensitive)
|
|
225
225
|
pnpm test:e2e # Playwright multi-tab browser tests (requires Google Chrome)
|
|
226
226
|
pnpm pack --pack-destination /tmp
|
|
227
227
|
```
|
package/README.zh.md
CHANGED
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
- Transport 重连自动恢复当前 owner 的 Topic
|
|
35
35
|
- Tab 异常退出后通过心跳 TTL 自动迁移
|
|
36
36
|
- BroadcastChannel 或 localStorage 不可用时自动降级为本地模式
|
|
37
|
-
-
|
|
37
|
+
- 可选的 durable 回放持久化(`replay.persistence`),支持 `appendBatch` 批量写入(IndexedDB 事务合并;清理策略 `count` / `age` / `both`)
|
|
38
|
+
- BroadcastChannel 不可用时,可选的 localStorage storage-event 协调降级通道(`channelFallback: 'storage-event'`)
|
|
39
|
+
- localStorage 中的协调记录只保存不透明 key——不含连接地址、原始 Topic 文本、凭证或消息内容。两个需要显式开启的选项会写入真实内容,且被作为取舍记录在文档中而非可以忽略的例外:`replay.persistence` 把已发布的 payload 写进 IndexedDB,其对象仓以明文 Topic 为 key;`channelFallback: 'storage-event'` 则把它承载的每一帧协调消息——含 Topic 明文与 publication payload——写进 localStorage,直到该 channel 关闭
|
|
38
40
|
|
|
39
41
|
完整能力清单见 [能力矩阵](./docs/zh/capabilities.md)。
|
|
40
42
|
|
package/dist/centrifuge.js
CHANGED
|
@@ -5369,9 +5369,13 @@ var PortReaper = class {
|
|
|
5369
5369
|
this.sessionTimeoutMs.set(port, DEFAULT_SESSION_TIMEOUT_MS);
|
|
5370
5370
|
this.schedule();
|
|
5371
5371
|
}
|
|
5372
|
-
/** Record activity on a port (any incoming message).
|
|
5373
|
-
*
|
|
5374
|
-
*
|
|
5372
|
+
/** Record activity on a port (any incoming message). The early return is what
|
|
5373
|
+
* keeps a late PING from a removed port out of `lastSeenAt`: `reap()` iterates
|
|
5374
|
+
* `targets`, so a row here would be unreachable rather than dangerous — measured,
|
|
5375
|
+
* deleting the guard passes every test in the suite, because the only difference is
|
|
5376
|
+
* one stale map entry per dead port. It stays as the cheap invariant that the three
|
|
5377
|
+
* maps hold exactly the tracked ports, which is what `reap()`'s `??` fallbacks and
|
|
5378
|
+
* this file's tracking contract both lean on. */
|
|
5375
5379
|
touch(port) {
|
|
5376
5380
|
if (!this.targets.has(port)) return;
|
|
5377
5381
|
this.lastSeenAt.set(port, this.now());
|
|
@@ -5445,8 +5449,13 @@ var PortReaper = class {
|
|
|
5445
5449
|
return minHeartbeat;
|
|
5446
5450
|
}
|
|
5447
5451
|
/** Close sessions whose port has been silent longer than its timeout.
|
|
5448
|
-
* Iterates a snapshot
|
|
5449
|
-
*
|
|
5452
|
+
* Iterates a snapshot. That is defensive rather than load-bearing: `Map`
|
|
5453
|
+
* iteration in JS is deletion-safe, so removing the current entry cannot skip the
|
|
5454
|
+
* next one, and the only case a snapshot changes is a re-entrant `register()` from
|
|
5455
|
+
* inside a target's own teardown — which a fresh port would survive anyway, because
|
|
5456
|
+
* `register()` stamps `lastSeenAt` with the current time and a zero-age port is
|
|
5457
|
+
* never past its timeout. Measured: iterating the live map instead passes every
|
|
5458
|
+
* test, so do not read this line as pinned. */
|
|
5450
5459
|
reap() {
|
|
5451
5460
|
const now = this.now();
|
|
5452
5461
|
let reapedAny = false;
|