cross-tab-worker-databus 0.20.87 → 0.20.89
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 +22 -0
- package/dist/centrifuge-session.d.ts +1 -0
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.d.ts.map +1 -1
- package/dist/centrifuge.js +55 -8
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js +38 -6
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js +38 -6
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-ZNHJ5OMY.js → chunk-77BQELW4.js} +69 -29
- package/dist/{chunk-ZNHJ5OMY.js.map → chunk-77BQELW4.js.map} +3 -3
- package/dist/cjs/centrifuge.cjs +122 -35
- package/dist/cjs/centrifuge.cjs.map +3 -3
- package/dist/cjs/index.cjs +76 -30
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/core/data-bus.d.ts +6 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/replay-manager.d.ts.map +1 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/api.md +4 -4
- package/docs/architecture.md +8 -6
- package/docs/capabilities.md +1 -1
- package/docs/configuration.md +1 -1
- package/docs/roadmap.md +18 -1
- package/docs/zh/api.md +4 -4
- package/docs/zh/architecture.md +8 -6
- package/docs/zh/capabilities.md +1 -1
- package/docs/zh/configuration.md +1 -1
- package/docs/zh/roadmap.md +18 -1
- package/package.json +1 -1
package/docs/zh/architecture.md
CHANGED
|
@@ -506,11 +506,11 @@ Transport 消息 → isAssigned(topic)? → 是 → broadcastEvent(EVENT)
|
|
|
506
506
|
以下不变量由回归测试固化(见 `tests/stability.test.ts` 与 `tests/replay-persistence.test.ts`),后续重构必须继续保持:
|
|
507
507
|
|
|
508
508
|
- **Handoff ACK 有效性。** `ROUTE_RELEASED` 只有在 route 仍指向接收方、释放来自记录的 `handoffFromWorkerId`、且 ACK generation 不小于存储 route 的 generation 时才被接受。来自更早交接轮次的重复 ACK(如 a↔b 反复交接)携带更旧的 generation,会被丢弃。
|
|
509
|
-
- **Replay 持久化清理顺序。** 排队在当前任务之后的批量持久化 flush 会与竞速的清理操作对账:`unsubscribe` 与 `clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore`
|
|
509
|
+
- **Replay 持久化清理顺序。** 排队在当前任务之后的批量持久化 flush 会与竞速的清理操作对账:`unsubscribe` 与 `clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore` 丢弃早于截止时间的条目,`suspend()`/`stop()` 则丢弃整个待写批次,避免它在新生命周期代际下启动。已清理或属于已停止会话的历史不会被在途 flush 复活。
|
|
510
510
|
- **存储写失败恢复。** 合并写入按指数退避重试(50 ms → 1.6 s 封顶)。结构性失败的关键在 5 次尝试后被丢弃(伴随 `console.warn`),且不会永久阻塞其他排队 key;队列完全清空或 `clear()` 取消重试后,退避延迟重置。
|
|
511
511
|
- **Transport 恢复预算。** 自动恢复由冷却时间限速、由 `recovery.maxAttempts` 限量,预算耗尽后标记 `exhausted`。成功的重开会重置尝试计数与 exhausted 标记;transport 宕机时显式 `subscribe` 仍可手动恢复。自动调度本身不会重开连接:后端必须先释放失效连接,重试才能创建或重开 socket。`WebSocketTransport` 只在 socket `open` 后 resolve `start()`;握手前的 `error`/`close` 或 `connectTimeoutMs` 超时都会 reject。它仅在 socket 有效期间将其标记为 active;`error`/`close` 会立即失效,下一次 `start()` 在调用工厂前清除旧引用,因此被取代 socket 的迟到回调会被忽略。
|
|
512
|
-
- **BFCache 挂起。** Tab 隐藏时停止 transport、递增持久化重试 generation
|
|
513
|
-
- **挂起态就绪判定。** 挂起中的 bus 会把 `startPromise` 复用为 `pendingStop`(即 chained `transport.stop()` 的 gate),该 Promise 只能证明清理完成,不能证明可以承载数据。`ready()` 在 `stopping` 门之后检查 `suspended`,以挂起态错误 reject,而不是返回 stop gate;`pageshow`/`reopenTransport()` 与显式 `start()` 会清除标记并安装真正的重开 Promise,使 `ready()` 跟随最新生命周期意图。`getHealthSummary()` 原本就报告 `{ healthy: false, state: 'suspended' }`,reject 让 `ready()`
|
|
512
|
+
- **BFCache 挂起。** Tab 隐藏时停止 transport、递增持久化重试 generation(取消在途重试且不对外报错),同时暂停 trace metrics 与 dedup/replay 周期清理并门控分发;`pageshow` 或显式 `start()` 会重开 transport、恢复这些周期资源,并且每轮循环只重建一次订阅。
|
|
513
|
+
- **挂起态就绪判定。** 挂起中的 bus 会把 `startPromise` 复用为 `pendingStop`(即 chained `transport.stop()` 的 gate),该 Promise 只能证明清理完成,不能证明可以承载数据。`ready()` 在 `stopping` 门之后检查 `suspended`,以挂起态错误 reject,而不是返回 stop gate;`pageshow`/`reopenTransport()` 与显式 `start()` 会清除标记并安装真正的重开 Promise,使 `ready()` 跟随最新生命周期意图。`getHealthSummary()` 原本就报告 `{ healthy: false, state: 'suspended' }`,reject 让 `ready()` 与该判定保持一致。由于 `pagehide` 会独立于 transport 暂停 cluster,显式 `start()` 在解除挂起时也必须一并恢复 cluster;否则 bus 会报告 transport 健康,而 channel listener、heartbeat 与 route 分配仍保持休眠直到下一次 `pageshow`,所有入站 publication 都会因 `isAssigned()` 对照已清空的分配表而被丢弃。
|
|
514
514
|
- **交接通道关闭顺序。** `pause()` 将物理 `channel.close()` 推迟一个任务。同步关闭会丢弃仍在排队等待投递的消息(包括交接的 `ROUTE_RELEASED`),使交接目标持有未确认路由。
|
|
515
515
|
- **悬挂交接恢复。** 若前任 owner 已消失而其 `ROUTE_RELEASED` 始终未到达(高负载下通道消息丢失,或 route 写入与 ACK 发送之间崩溃),reconcile 循环会在该未确认交接悬挂超过一个 worker TTL(默认 10 秒)后重新选举存活 owner:路由以全新 generation 重写并清除交接标记,使常规确认路径得以完成(已有回归固化)。年龄门限很关键——刚写入的未确认路由可能只是在等确认落盘,不能误判为悬挂;而只要前任 owner 仍然存活,新 owner 会继续等待,因此严格交接的无重叠保证不受影响。
|
|
516
516
|
- **丢失与恢复矩阵。** 每类协调消息都有有界恢复路径:丢失的 `CONTROL/SUBSCRIBE` 由心跳 reconcile 对未确认路由重发;丢失的 `REGISTRY` 通知最多损失一个心跳间隔(默认 3 秒),因为每次 tick 都会 reconcile;丢失的 `ROUTE_RELEASED` 由 reconcile 在前任 owner 消失且交接悬挂超过一个 worker TTL 后重新选举恢复(见上文悬挂交接不变量,已有回归固化);transport 断连窗口内被丢弃的 publication 是唯一文档化的不可恢复丢失(transport 契约)。storage-event 降级通道通过信封内的单调序列号保证变值投递,丢失的派发由同一 reconcile 循环恢复。
|
|
@@ -537,7 +537,7 @@ DataBus 将"业务订阅意图"与"transport 当前订阅状态"分离。transpo
|
|
|
537
537
|
| `suspended` | `boolean` | Tab 已隐藏;transport 被有意暂停 |
|
|
538
538
|
| `transportReady` | `boolean` | 本次会话中 transport 已成功打开;运行期 `error` 后会保留,使 `ready()` 继续跟随已安装的 transport(待执行操作由恢复门而非该标记控制) |
|
|
539
539
|
| `startPromise` | `Promise \| null` | 并发 `start()` 调用的 gate;操作完成后清除 |
|
|
540
|
-
| `stopPromise` | `Promise \| null` | 显式 `stop()` 及其后排队的 restart 共享的 gate |
|
|
540
|
+
| `stopPromise` | `Promise \| null` | 显式 `stop()` 及其后排队的 restart 共享的 gate;仅在 `stopping` 为 true 时复用,因为它会在 `performStop()` settle 后再过一个微任务才被清空 |
|
|
541
541
|
| `queuedStart` | `Promise \| null` | 等待进行中的显式 stop 完成后执行的一次全新 start |
|
|
542
542
|
| `queuedStartToken` | `number` | 每次排队 restart 获得的单调令牌,避免取消被误认为更晚的 restart |
|
|
543
543
|
| `canceledQueuedStartToken` | `number` | 被 `stop()` 取消的最高 queued-restart 令牌;令牌不高于它的续体只 resolve,不打开 transport |
|
|
@@ -573,12 +573,14 @@ DataBus 将"业务订阅意图"与"transport 当前订阅状态"分离。transpo
|
|
|
573
573
|
**关键行为:**
|
|
574
574
|
|
|
575
575
|
- **并发 start**:真实 transport open 在飞行中时,第二次调用 `start()` 返回同一个 promise,任何时候只有一个 transport open 在飞行中。pagehide 产生的 stop 也可能占用 `startPromise`;`start()` 会识别 `startPromise === pendingStop`,把 reopen 排在该 stop 之后,而不是把清理 promise 当作成功启动返回。
|
|
576
|
-
-
|
|
576
|
+
- **失败通知顺序**:transport 可能在 `start()` 仍在飞行时同步上报 `error`。`openTransport()` 会立即更新内部状态,但会延迟用户可见的 `onStatus('error')` 通知,直到它清除 `transportReady`、拆掉初始启动的 cluster、安装失败 transport 的 stop gate、记录失败并清除 `startPromise`;启动失败的 `onError` 通知也在这些清理之后发送。因此在任一回调中同步调用 `start()` 都会在 stop gate 之后开启全新尝试,而不是共享刚刚 reject 的 promise。重试会重置失败账本,被取代 opening 的 rejection 由其自身生命周期清理消费,不会在重试成功后再次写回。真实 open 仍在飞行时的普通并发 start 仍共享同一个 promise。
|
|
577
|
+
- **显式 stop 期间 start**:`stop()` 用共享的 `stopPromise` 服务并发调用者。若 `start()` 在该 stop settle 期间到达,只保存一个 `queuedStart`;stop 的 `finally` 清理生命周期状态后,排队的 start 使用新配置开启全新生命周期。此窗口内的重复调用共享 stop 和 queued-start promise。该共享 gate 只在 `stopping` 为 true 时复用:`performStop()` 在 `finally` 中把 `stopping` 翻回 false,而 `stopPromise` 要再过一个微任务才清空,因此落在这个缝隙里的 `start()`/`stop()` 必须 fall through 到一次全新 teardown,而不是对已 settle 的 gate resolve 并放任重启后的 bus 继续运行。
|
|
577
578
|
- **stop 取消排队 restart**:排队续体已经挂在 stop promise 上、无法撤销调度,因此在它执行前再次 `stop()` 会改为使其失效。每个排队 restart 携带单调令牌;`stop()` 记录当前令牌并释放唯一的队列槽位,续体发现自己的令牌不再是最新时只 resolve、不打开 transport。排队 `start()` Promise 保留这一「取消即 resolve」契约,而单独的 readiness 视图会让 `ready()` 对被取消的意图 reject。由于后到的 `start()` 会签发更高令牌,`stop → start → stop → start` 仍以运行态结束,而 `stop → start → stop` 以停止态结束且不会多打开一次 transport。
|
|
578
579
|
- **停止期间发布拒绝**:`stop()` 设置 `stopping` 后,新发起的 `publish()` 与非空 `publishBatch()` 无法到达 transport;它们通过 `onError` 上报错误,而不是让 `runTransport()` 静默返回;空 batch 仍为 no-op。已排队在飞行中 open 之后的发布会被 stop 取消(最新生命周期意图优先),而页面隐藏挂起仍保持文档所述的「不延迟、直接丢弃」语义。
|
|
579
580
|
- **停止期间生命周期操作拒绝**:`stopping` gate 同样覆盖 `subscribe()` 与 `ready()`。迟到的 `subscribe()` 会通过 `onError` 上报并返回 no-op 释放函数,避免 handler 被 `topicHandlers.clear()` 清掉,或订阅漂移进下一次 restart 却没有对应 handler。`ready()` 会 reject,而不是对正在停止的 transport 报告 ready。若 `start()` 已在该 stop 之后排队重启,`ready()` 仍返回 queued-start promise,因为这是最新生命周期意图。
|
|
581
|
+
- **停止期间健康判定**:`getHealthSummary()` 在整个 teardown 期间都报告 `state: 'stopped'` / `healthy: false`,而不只是在 `started` 翻回 false 之后。transport 的异步 `stop()` 尚未 settle 时可能仍上报 `connected`,仅依据实时状态推导健康会得到自相矛盾的快照:一边宣称 bus 可用,一边 `publish()`、`subscribe()`、`ready()` 已经全部拒绝。`started` 仍反映真实生命周期标志,`transport` 仍把实时 status/`ready` 作为诊断信息输出;排在 stop 之后的 restart 在真正接管生命周期后报告为 `starting`。
|
|
580
582
|
- **排队重启失败保留**:排队重启若在 transport 启动阶段失败,会清除 `started`,但为后续 `ready()` 调用保留真实错误。未提供 `initialConfig` 时,这些调用会以启动失败 reject,而不是返回通用的配置错误;显式 `start(config)` 仍以全新失败账本执行干净的手动重试。
|
|
581
|
-
- **启动期间隐藏**:`pagehide` 在 `openTransport` 飞行中触发时,`suspendTransport()` 设置 `suspended = true`,并在飞行中的 start 之后链式执行 `transport.stop()`。`openTransport` 的 catch 路径检测到 `suspended` 后放弃本次 open
|
|
583
|
+
- **启动期间隐藏**:`pagehide` 在 `openTransport` 飞行中触发时,`suspendTransport()` 设置 `suspended = true`,并在飞行中的 start 之后链式执行 `transport.stop()`。`openTransport` 的 catch 路径检测到 `suspended` 后放弃本次 open,不视为失败。当重复的 hide/show 让排队的 resume opening 与更早的 stop gate 交错时,挂起会安装新的串行 stop gate 并恢复 `startPromise === pendingStop` 不变量,使下一次 `pageshow` 真正重开,而不是复用已被淘汰的 opening 并永久停留在挂起状态。
|
|
582
584
|
- **被取代 open 失效**:每次全新 start、reopen、suspend 和 stop 都会推进 `lifecycleEpoch`。open 会捕获自己的 epoch;一旦更新的转换接管生命周期,旧 open 的 status/message/error 回调会被忽略,也不会再把 transport 标记为 ready 或执行失败清理。因此 `stop()` 会等待未完成的 open/reopen,并阻止被取代的 open 在 stop 完成后变为 ready。
|
|
583
585
|
- **恢复冷却**:transport 上报 `error` 且 `started` 为 true、`stopping` 为 false 时,`updateStatus` 在 `RECOVERY_COOLDOWN_MS`(1000 ms)后调度自动 `reopenTransport()`。冷却窗口内的第二次错误被抑制,防止紧循环重试。
|
|
584
586
|
- **传输恢复门**:调度重开的同时会抬起恢复门,使冷却期间发起的 `subscribe` / `publish` 无法到达失效连接,等重开成功后才释放。自动尝试失败后门刻意保持关闭:下一次显式操作会立即触发按需重开,而不是等待下一个限速尝试,挂起的操作则在该次成功后一并 flush。`recovery.maxAttempts` 耗尽,或被 `stop()` / `suspendTransport()` 取代时释放门,从而保留显式重试路径与挂起丢弃语义。运行期 `error` 不会清除 `transportReady`——清除它会让调用方流量绕过冷却重开,并在连接尚未承载数据时报告 ready。
|
package/docs/zh/capabilities.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
| 不变量 | 领域 | 被回归固化的保证 |
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| 交接 ACK 有效性 | 协调 | 仅当 route 仍指向接收方、释放方匹配 `handoffFromWorkerId`、且 ACK 代数 ≥ 存储 route 代数时才接受 `ROUTE_RELEASED`——更早交接轮次的过期 ACK(如 a↔b 乒乓)会被丢弃 |
|
|
47
|
-
| 回放持久化清理顺序 | 持久性 | 排队中的批量 flush 会按先到清理过滤(`unsubscribe`/`clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore`
|
|
47
|
+
| 回放持久化清理顺序 | 持久性 | 排队中的批量 flush 会按先到清理过滤(`unsubscribe`/`clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore` 丢弃早于截止时间的条目,`suspend()`/`stop()` 丢弃整个待写批次);已清或已停止会话的历史不会被进行中的 flush 重新追加 |
|
|
48
48
|
| 存储写入恢复 | 协调 | 合并写以指数退避重试(50 ms → 1.6 s 上限);结构性失败键在 5 次后丢弃(并 `console.warn`)而不阻塞其他排队键;队列清空或 `clear()` 取消后退避重置 |
|
|
49
49
|
| 传输恢复预算 | 生命周期 | 自动恢复由冷却间隔节流、以 `recovery.maxAttempts` 为界,预算耗尽时报 `exhausted`;成功重开后重置尝试计数与 exhausted,断线传输上的显式 `subscribe` 仍可手动恢复 |
|
|
50
50
|
| BFCache 挂起 | 生命周期 | 隐藏页面停掉传输并静默取消进行中的持久化重试;pageshow 重开传输并每个周期恰好一次重建订阅 |
|
package/docs/zh/configuration.md
CHANGED
|
@@ -54,7 +54,7 @@ const bus = new CrossTabDataBus({
|
|
|
54
54
|
|
|
55
55
|
`replay.retentionSweepMs` 可选地按周期触发同一清理逻辑。它适合安静 topic 的 durable 旧记录也需要过期的场景;需要同时配置 `retentionMs` 和实现 `clearBefore()` 的持久化适配器。定时器遵循页面可见性和生命周期切换,默认关闭。
|
|
56
56
|
|
|
57
|
-
`replay.persistenceRetry` 可选地控制瞬时持久化失败的恢复。`maxAttempts` 是总尝试次数(默认 `1`),`backoffMs` 是首次重试前的延迟(默认 `50`);延迟会指数增长并封顶。最终失败仍沿用现有 `onError` 和 reliability 行为。
|
|
57
|
+
`replay.persistenceRetry` 可选地控制瞬时持久化失败的恢复。`maxAttempts` 是总尝试次数(默认 `1`),`backoffMs` 是首次重试前的延迟(默认 `50`);延迟会指数增长并封顶。最终失败仍沿用现有 `onError` 和 reliability 行为。bus 挂起或停止时仍排队在微任务中的批量 flush 会随其生命周期代际一起丢弃,不会在 teardown 后启动 durable append。
|
|
58
58
|
|
|
59
59
|
### Replay 选项
|
|
60
60
|
|
package/docs/zh/roadmap.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
0.20.
|
|
3
|
+
0.20.89 已发布。项目会先持续完成生命周期/就绪与适配器 parity 审计,再进入 1.0.0 稳定性冻结。
|
|
4
|
+
|
|
5
|
+
## 0.20.89 已完成范围
|
|
6
|
+
|
|
7
|
+
本开发线延续异步回调隔离审计:以下每一项修复都把回调、排队微任务或 Promise 续体绑定到创建它的生命周期 generation,使被取代的会话无法写入其替代者。
|
|
8
|
+
|
|
9
|
+
- 异步 teardown 与重启边界:已 settle 的 `stop()` gate 不再吞掉后续 teardown(`stop → start → stop` 现在以停止态结束);`getHealthSummary()` 对正在停止的 bus 报告 `state: 'stopped'`,不再与其已经发出的 `publish()` / `subscribe()` / `ready()` 拒绝语义自相矛盾。
|
|
10
|
+
- replay 持久化隔离:微任务排队的 batch flush 与被排队的 retention cleanup 会在 `suspend()` / `stop()` 取代其 generation 后被丢弃,已停止会话的历史无法再写入 durable store。
|
|
11
|
+
- durable hydration 取消:在 `suspend()` 或 `stop()` 之后才 resolve 的 `load()` 不再向 teardown 已清空的缓冲区追加数据,并按生命周期取消上报,而不是记为持久化失败。
|
|
12
|
+
- trace 会话隔离:已停止的 trace reporter 保持惰性——旧会话排队中的 `asyncSink` 事件被丢弃,显式重启会清除停止标记,使其重新发出生命周期 `start`。
|
|
13
|
+
- transport 与 Worker 回调隔离:Centrifuge credential-provider 结果绑定到发起请求的确切 Worker/port/session;`CentrifugeSession` 的异步 client/subscription 回调在 `STOP` 或重新初始化后被忽略;来自已替换 WebSocket 连接的 `Blob` 二进制帧不再被当作新连接的帧派发。
|
|
14
|
+
- 回归安全网:seeded lifecycle fuzzer 现在覆盖 1_500 种交织,并断言最后一次显式意图为 `stop()` 的序列会以 `state: 'stopped'` 且无 live transport 结束。
|
|
15
|
+
|
|
16
|
+
## 0.20.88 已完成范围
|
|
17
|
+
|
|
18
|
+
- 启动失败恢复现在可重入:transport 在初始 `openTransport()` 尚未结算时同步上报 `error`,调用方可以从 `onStatus('error')` 或 `onError` 回调立即重试。失败 open 会先完成清理,重试建立新的生命周期,旧 rejection 不会重新污染已重置的失败账本。
|
|
19
|
+
- 初始 transport open 尚在飞行时反复发生 BFCache `pagehide`/`pageshow`,不再让 bus 永久停留在挂起状态。suspend 只在旧 stop gate 仍代表当前生命周期时复用;否则安装新的串行 stop,使下一次 resume 真正重开 transport。
|
|
20
|
+
- 显式 `start()` 现在是完整的 BFCache 恢复路径:除 transport 外还会恢复跨 Tab 协调,并重新启动 `pagehide` 暂停的 trace metrics、dedup 过期清扫与 replay retention 定时工作。
|
|
4
21
|
|
|
5
22
|
## 0.20.87 已完成范围
|
|
6
23
|
|
package/package.json
CHANGED