cross-tab-worker-databus 0.20.71 → 0.20.86
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 +216 -2
- package/README.md +8 -0
- package/README.zh.md +4 -0
- package/dist/centrifuge-protocol.d.ts +59 -26
- package/dist/centrifuge-protocol.d.ts.map +1 -1
- package/dist/centrifuge-session.d.ts +9 -0
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.d.ts +12 -2
- package/dist/centrifuge.d.ts.map +1 -1
- package/dist/centrifuge.js +168 -88
- package/dist/centrifuge.js.map +3 -3
- package/dist/centrifuge.shared.worker.js +146 -38
- package/dist/centrifuge.shared.worker.js.map +3 -3
- package/dist/centrifuge.worker.js +140 -33
- package/dist/centrifuge.worker.js.map +3 -3
- package/dist/{chunk-D2SIT473.js → chunk-SDOV3UHG.js} +1304 -515
- package/dist/chunk-SDOV3UHG.js.map +7 -0
- package/dist/chunk-TZ7ZP7YD.js +175 -0
- package/dist/chunk-TZ7ZP7YD.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +1608 -650
- package/dist/cjs/centrifuge.cjs.map +4 -4
- package/dist/cjs/hooks.cjs +37 -2
- package/dist/cjs/hooks.cjs.map +3 -3
- package/dist/cjs/index.cjs +1692 -747
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/vue.cjs +45 -2
- package/dist/cjs/vue.cjs.map +3 -3
- package/dist/core/cluster.d.ts +40 -4
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +134 -77
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/dedup-manager.d.ts +98 -0
- package/dist/core/dedup-manager.d.ts.map +1 -0
- package/dist/core/environment.d.ts +43 -3
- package/dist/core/environment.d.ts.map +1 -1
- package/dist/core/replay-manager.d.ts +129 -0
- package/dist/core/replay-manager.d.ts.map +1 -0
- package/dist/core/replay-persistence.d.ts +2 -1
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/replay-pruning.d.ts +21 -0
- package/dist/core/replay-pruning.d.ts.map +1 -0
- package/dist/core/routing.d.ts +22 -3
- package/dist/core/routing.d.ts.map +1 -1
- package/dist/core/storage-batch.d.ts +0 -4
- package/dist/core/storage-batch.d.ts.map +1 -1
- package/dist/core/trace.d.ts +55 -15
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/types.d.ts +58 -9
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/hooks.d.ts +12 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +28 -2
- package/dist/hooks.js.map +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +263 -166
- package/dist/index.js.map +3 -3
- package/dist/utils/constants.d.ts +170 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/error-utils.d.ts +28 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/metadata.d.ts +16 -0
- package/dist/utils/metadata.d.ts.map +1 -0
- package/dist/utils/storage-utils.d.ts +24 -0
- package/dist/utils/storage-utils.d.ts.map +1 -0
- package/dist/utils/validation.d.ts +76 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/vue.d.ts +13 -1
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +36 -2
- package/dist/vue.js.map +2 -2
- package/dist/websocket.d.ts +6 -1
- package/dist/websocket.d.ts.map +1 -1
- package/dist/worker-mode.d.ts +6 -2
- package/dist/worker-mode.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/api.md +135 -4
- package/docs/architecture.md +92 -1
- package/docs/benchmarks.md +24 -0
- package/docs/capabilities.md +24 -3
- package/docs/configuration.md +58 -0
- package/docs/getting-started.md +35 -0
- package/docs/release-checklist.md +28 -5
- package/docs/roadmap.md +81 -5
- package/docs/zh/README.md +2 -1
- package/docs/zh/api.md +134 -4
- package/docs/zh/architecture.md +61 -1
- package/docs/zh/benchmarks.md +24 -0
- package/docs/zh/capabilities.md +21 -3
- package/docs/zh/configuration.md +53 -0
- package/docs/zh/getting-started.md +35 -0
- package/docs/zh/release-checklist.md +29 -6
- package/docs/zh/roadmap.md +93 -5
- package/package.json +39 -16
- package/dist/chunk-D2SIT473.js.map +0 -7
package/docs/zh/architecture.md
CHANGED
|
@@ -48,10 +48,26 @@ graph TB
|
|
|
48
48
|
| 层 | 入口 | 职责 |
|
|
49
49
|
|---|---|---|
|
|
50
50
|
| DataBus | `CrossTabDataBus` | 本地 handler 引用计数、消息分发、状态与 transport 生命周期 |
|
|
51
|
+
| Replay | `ReplayManager` | 有界的每 Topic 历史环形缓冲、IndexedDB 持久化、保留期清理、重试策略 |
|
|
52
|
+
| Dedup | `DedupManager` | 可选的按 `messageId` 有界去重、自适应 TTL、过期清扫 |
|
|
51
53
|
| 集群协调 | `WorkerClusterRuntime` | Worker 注册、角色、心跳、Topic owner、迁移和广播协议 |
|
|
52
54
|
| Transport | `DataBusTransport` | 在真实 Worker/连接上执行 subscribe、unsubscribe、publish |
|
|
53
55
|
| Centrifuge | `CentrifugeWorkerTransport` | 主线程与内置 Centrifuge Worker 之间的协议适配 |
|
|
54
56
|
|
|
57
|
+
## 源码组织与共享工具
|
|
58
|
+
|
|
59
|
+
`src/` 目录在平台适配器和协调核心之外维护一个轻量工具库 `src/utils/`:
|
|
60
|
+
|
|
61
|
+
| 文件 | 内容 | 使用方 |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `utils/constants.ts` | 全部运行时字符串字面量集中一处——状态、角色、动作、集群/Worker/协议消息类型、trace 事件判别字段、枚举、命名空间前缀。所有由字面量派生的类型(`WorkerStatus`、消息 `type` 判别、trace 的 `action`/`operation` 等)都用 `(typeof X)[keyof typeof X]` 从这些常量派生,值和类型永不脱节。 | 所有模块 |
|
|
64
|
+
| `utils/metadata.ts` | `publicationMetadata(messageId, timestamp)`——只展开已定义字段,此前散落在四个模块各有一份。 | data-bus、cluster、centrifuge、centrifuge-session |
|
|
65
|
+
| `utils/storage-utils.ts` | `readJson` / `writeJson` / `listKeys` / `readAllByPrefix`——容错的存储原语(损坏 JSON 视为不存在;写失败静默)。 | cluster |
|
|
66
|
+
| `utils/validation.ts` | 构造参数/选项校验断言(`assertReplayOptions`、`assertDedupOptions`、`assertRecoveryOptions`、`assertHeartbeatInterval` 等)。可选字段只在显式提供时校验;默认值恒合法。 | data-bus、replay-persistence、centrifuge |
|
|
67
|
+
| `utils/error-utils.ts` | `serializeError` / `deserializeWorkerError` + `SerializedWorkerError`——Error 在 Worker 边界的往返传输。 | centrifuge、centrifuge-session |
|
|
68
|
+
|
|
69
|
+
提取是刻意的选择:这些是无副作用、依赖轻的助手,其重复副本已经开始漂移(例如四份几乎相同的 `publicationMetadata` 实现)。有自身生命周期的有状态横切关注点——回放缓冲与去重——则作为自包含的 `DedupManager` / `ReplayManager` 类,由 DataBus 委托调用;它们持有自己的 map/timer/统计并暴露薄的 start/stop/record/clear 接口。DataBus 生命周期状态机(start/stop/suspend/resume、promise 门、恢复节奏)则刻意保留在 `CrossTabDataBus` 内部:这些标志紧密互锁,拆出去会重新引入这些门原本要防的竞态。
|
|
70
|
+
|
|
55
71
|
## 术语表
|
|
56
72
|
|
|
57
73
|
用通俗语言解释核心术语;代码与本文档其余部分使用简称。
|
|
@@ -310,6 +326,25 @@ owner Worker 对 transport 收到的每条 publication 都先用 `isAssigned(top
|
|
|
310
326
|
6. 只有 Topic 尚无 route,或者原 owner 已退出、心跳 TTL 过期时,才把 Topic 分配给负载最低的候选 Worker。
|
|
311
327
|
7. 新路由在 owner 写入 `confirmedAt` 前视为未确认;subscriber 会自动重发控制消息。
|
|
312
328
|
|
|
329
|
+
### 自适应 owner 加权(`loadWeighting`)
|
|
330
|
+
|
|
331
|
+
默认"负载最低"指拥有最少的 Topic。可选的 `loadWeighting` 增加流量与调度信号,同样只作用于新路由或孤儿路由——已有 route 保持 sticky,绝不迁移:
|
|
332
|
+
|
|
333
|
+
- 每个 Worker 在心跳之间采样自身的 fan-out 活动,并把 `WorkerThroughputSample`(`windowMs`、`messageCount`、`byteCount`、`overrunMs`、`sampledAt`)随 worker 记录发布。
|
|
334
|
+
- `overrunMs` 是采样窗口超出名义心跳间隔的正向余量。事件循环饥饿(浏览器可观测的 CPU 饱和代理)会让心跳延迟、窗口拉长,因此这是一个廉价的原生信号。
|
|
335
|
+
- `effectiveWorkerLoad`(`routing.ts` 中的纯函数)把 Worker 打分记为 `load + messageRateWeight × 条/秒 + byteRateWeight × 字节/秒 + scheduleLagWeight × (overrunMs ÷ windowMs)`。所有权重默认 `0`,保持 legacy 纯 Topic 数打分字节级不变。
|
|
336
|
+
- 评分确定性且读取所有 Tab 相同的持久化记录,因此集群各处路由一致;调度滞后的 Worker 即使携带更少的 Topic,对新路由的吸引力也会下降。
|
|
337
|
+
|
|
338
|
+
### 异步凭证刷新桥(`credentialProvider`)
|
|
339
|
+
|
|
340
|
+
Centrifuge 客户端选项会 structured-clone 进 Worker,因此函数型 `getToken` / `getChannelToken` 无法随配置传输。`createCentrifugeDataBus` 的可选 `credentialProvider`(`{ getToken, getChannelToken }`)改为在主线程运行:
|
|
341
|
+
|
|
342
|
+
1. 配置了 provider 时,INIT 携带 `tokenBridge: true`,Worker 内 `CentrifugeSession` 把 `getToken` / `getChannelToken` 接到一次 `TOKEN_REQUEST` 输出(`requestId`、`kind`、可选 `channel`)。
|
|
343
|
+
2. transport 在主线程解析:`resolveTokenRequest` 调用 provider,并把 `TOKEN_RESPONSE`(拒绝或空 token 时为 `TOKEN_ERROR`)按 `requestId` 回发。
|
|
344
|
+
3. session 按 `requestId` 结算挂起的 promise;`STOP` 会拒绝所有在途请求,已停止的 Worker 不会永远等待响应。
|
|
345
|
+
4. 未配置 provider 时 INIT 不带 `tokenBridge`,配置保持与 legacy 字节级一致——服务端从不请求 token 就永远不会触发请求。
|
|
346
|
+
5. Dedicated、SharedWorker 与本地后端共用 `CentrifugeSession`,因此桥对三者都生效;token 从不以函数形式跨越 Worker 边界,只作为已解析的字符串返回。
|
|
347
|
+
|
|
313
348
|
## 订阅流程
|
|
314
349
|
|
|
315
350
|
```mermaid
|
|
@@ -466,6 +501,20 @@ Transport 消息 → isAssigned(topic)? → 是 → broadcastEvent(EVENT)
|
|
|
466
501
|
|
|
467
502
|
该过程在正常 owner 交接时避免重复订阅,同时在故障恢复时保持可用;不保证 exactly-once。
|
|
468
503
|
|
|
504
|
+
## 稳定性不变量
|
|
505
|
+
|
|
506
|
+
以下不变量由回归测试固化(见 `tests/stability.test.ts` 与 `tests/replay-persistence.test.ts`),后续重构必须继续保持:
|
|
507
|
+
|
|
508
|
+
- **Handoff ACK 有效性。** `ROUTE_RELEASED` 只有在 route 仍指向接收方、释放来自记录的 `handoffFromWorkerId`、且 ACK generation 不小于存储 route 的 generation 时才被接受。来自更早交接轮次的重复 ACK(如 a↔b 反复交接)携带更旧的 generation,会被丢弃。
|
|
509
|
+
- **Replay 持久化清理顺序。** 排队在当前任务之后的批量持久化 flush 会与竞速的清理操作对账:`unsubscribe` 与 `clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore` 丢弃早于截止时间的条目。已清理的历史不会被在途 flush 复活。
|
|
510
|
+
- **存储写失败恢复。** 合并写入按指数退避重试(50 ms → 1.6 s 封顶)。结构性失败的关键在 5 次尝试后被丢弃(伴随 `console.warn`),且不会永久阻塞其他排队 key;队列完全清空或 `clear()` 取消重试后,退避延迟重置。
|
|
511
|
+
- **Transport 恢复预算。** 自动恢复由冷却时间限速、由 `recovery.maxAttempts` 限量,预算耗尽后标记 `exhausted`。成功的重开会重置尝试计数与 exhausted 标记;transport 宕机时显式 `subscribe` 仍可手动恢复。
|
|
512
|
+
- **BFCache 挂起。** Tab 隐藏时停止 transport、递增持久化重试 generation(取消在途重试且不对外报错)并门控分发;pageshow 时重开 transport,每轮循环只重建一次订阅。
|
|
513
|
+
- **交接通道关闭顺序。** `pause()` 将物理 `channel.close()` 推迟一个任务。同步关闭会丢弃仍在排队等待投递的消息(包括交接的 `ROUTE_RELEASED`),使交接目标持有未确认路由。
|
|
514
|
+
- **悬挂交接恢复。** 若前任 owner 已消失而其 `ROUTE_RELEASED` 始终未到达(高负载下通道消息丢失,或 route 写入与 ACK 发送之间崩溃),reconcile 循环会在该未确认交接悬挂超过一个 worker TTL(默认 10 秒)后重新选举存活 owner:路由以全新 generation 重写并清除交接标记,使常规确认路径得以完成(已有回归固化)。年龄门限很关键——刚写入的未确认路由可能只是在等确认落盘,不能误判为悬挂;而只要前任 owner 仍然存活,新 owner 会继续等待,因此严格交接的无重叠保证不受影响。
|
|
515
|
+
- **丢失与恢复矩阵。** 每类协调消息都有有界恢复路径:丢失的 `CONTROL/SUBSCRIBE` 由心跳 reconcile 对未确认路由重发;丢失的 `REGISTRY` 通知最多损失一个心跳间隔(默认 3 秒),因为每次 tick 都会 reconcile;丢失的 `ROUTE_RELEASED` 由 reconcile 在前任 owner 消失且交接悬挂超过一个 worker TTL 后重新选举恢复(见上文悬挂交接不变量,已有回归固化);transport 断连窗口内被丢弃的 publication 是唯一文档化的不可恢复丢失(transport 契约)。storage-event 降级通道通过信封内的单调序列号保证变值投递,丢失的派发由同一 reconcile 循环恢复。
|
|
516
|
+
- **恢复诊断。** `getHealthSummary()` 从生命周期标志推导单一就绪判定(`stopped` / `starting` / `healthy` / `recovering` / `suspended` / `degraded`);统一的 `lastFailure` 账本与持久化计数在每次显式 `start()` 后重置。
|
|
517
|
+
|
|
469
518
|
## Transport 重连
|
|
470
519
|
|
|
471
520
|
DataBus 将"业务订阅意图"与"transport 当前订阅状态"分离。transport 上报 `disconnected` / `error` 时,只清除底层订阅标志,不清除业务 handler;重新进入 `connected` 时,DataBus 自动重放当前 Worker 负责的 Topic。
|
|
@@ -485,7 +534,12 @@ DataBus 将"业务订阅意图"与"transport 当前订阅状态"分离。transpo
|
|
|
485
534
|
| `suspended` | `boolean` | Tab 已隐藏;transport 被有意暂停 |
|
|
486
535
|
| `transportReady` | `boolean` | transport 已上报 `connected`,可接受操作 |
|
|
487
536
|
| `startPromise` | `Promise \| null` | 并发 `start()` 调用的 gate;操作完成后清除 |
|
|
537
|
+
| `stopPromise` | `Promise \| null` | 显式 `stop()` 及其后排队的 restart 共享的 gate |
|
|
538
|
+
| `queuedStart` | `Promise \| null` | 等待进行中的显式 stop 完成后执行的一次全新 start |
|
|
539
|
+
| `queuedStartToken` | `number` | 每次排队 restart 获得的单调令牌,避免取消被误认为更晚的 restart |
|
|
540
|
+
| `canceledQueuedStartToken` | `number` | 被 `stop()` 取消的最高 queued-restart 令牌;令牌不高于它的续体只 resolve,不打开 transport |
|
|
488
541
|
| `pendingStop` | `Promise \| null` | 异步 `transport.stop()` 的 gate;由 suspend 和故障路径共享 |
|
|
542
|
+
| `lifecycleEpoch` | `number` | 单调所有权令牌;使被取代 open 的回调与清理失效 |
|
|
489
543
|
|
|
490
544
|
### 状态转换
|
|
491
545
|
|
|
@@ -515,8 +569,14 @@ DataBus 将"业务订阅意图"与"transport 当前订阅状态"分离。transpo
|
|
|
515
569
|
|
|
516
570
|
**关键行为:**
|
|
517
571
|
|
|
518
|
-
- **并发 start
|
|
572
|
+
- **并发 start**:真实 transport open 在飞行中时,第二次调用 `start()` 返回同一个 promise,任何时候只有一个 transport open 在飞行中。pagehide 产生的 stop 也可能占用 `startPromise`;`start()` 会识别 `startPromise === pendingStop`,把 reopen 排在该 stop 之后,而不是把清理 promise 当作成功启动返回。
|
|
573
|
+
- **显式 stop 期间 start**:`stop()` 用共享的 `stopPromise` 服务并发调用者。若 `start()` 在该 stop settle 期间到达,只保存一个 `queuedStart`;stop 的 `finally` 清理生命周期状态后,排队的 start 使用新配置开启全新生命周期。此窗口内的重复调用共享 stop 和 queued-start promise。
|
|
574
|
+
- **stop 取消排队 restart**:排队续体已经挂在 stop promise 上、无法撤销调度,因此在它执行前再次 `stop()` 会改为使其失效。每个排队 restart 携带单调令牌;`stop()` 记录当前令牌并释放唯一的队列槽位,续体发现自己的令牌不再是最新时只 resolve、不打开 transport。排队 `start()` Promise 保留这一「取消即 resolve」契约,而单独的 readiness 视图会让 `ready()` 对被取消的意图 reject。由于后到的 `start()` 会签发更高令牌,`stop → start → stop → start` 仍以运行态结束,而 `stop → start → stop` 以停止态结束且不会多打开一次 transport。
|
|
575
|
+
- **停止期间发布拒绝**:`stop()` 设置 `stopping` 后,新发起的 `publish()` 与非空 `publishBatch()` 无法到达 transport;它们通过 `onError` 上报错误,而不是让 `runTransport()` 静默返回;空 batch 仍为 no-op。已排队在飞行中 open 之后的发布会被 stop 取消(最新生命周期意图优先),而页面隐藏挂起仍保持文档所述的「不延迟、直接丢弃」语义。
|
|
576
|
+
- **停止期间生命周期操作拒绝**:`stopping` gate 同样覆盖 `subscribe()` 与 `ready()`。迟到的 `subscribe()` 会通过 `onError` 上报并返回 no-op 释放函数,避免 handler 被 `topicHandlers.clear()` 清掉,或订阅漂移进下一次 restart 却没有对应 handler。`ready()` 会 reject,而不是对正在停止的 transport 报告 ready。若 `start()` 已在该 stop 之后排队重启,`ready()` 仍返回 queued-start promise,因为这是最新生命周期意图。
|
|
577
|
+
- **排队重启失败保留**:排队重启若在 transport 启动阶段失败,会清除 `started`,但为后续 `ready()` 调用保留真实错误。未提供 `initialConfig` 时,这些调用会以启动失败 reject,而不是返回通用的配置错误;显式 `start(config)` 仍以全新失败账本执行干净的手动重试。
|
|
519
578
|
- **启动期间隐藏**:`pagehide` 在 `openTransport` 飞行中触发时,`suspendTransport()` 设置 `suspended = true`,并在飞行中的 start 之后链式执行 `transport.stop()`。`openTransport` 的 catch 路径检测到 `suspended` 后放弃本次 open,不视为失败。
|
|
579
|
+
- **被取代 open 失效**:每次全新 start、reopen、suspend 和 stop 都会推进 `lifecycleEpoch`。open 会捕获自己的 epoch;一旦更新的转换接管生命周期,旧 open 的 status/message/error 回调会被忽略,也不会再把 transport 标记为 ready 或执行失败清理。因此 `stop()` 会等待未完成的 open/reopen,并阻止被取代的 open 在 stop 完成后变为 ready。
|
|
520
580
|
- **恢复冷却**:transport 上报 `error` 且 `started` 为 true、`stopping` 为 false 时,`updateStatus` 在 `RECOVERY_COOLDOWN_MS`(1000 ms)后调度自动 `reopenTransport()`。冷却窗口内的第二次错误被抑制,防止紧循环重试。
|
|
521
581
|
- **暂停期间停止**:`stop()` 设置 `stopping = true`,阻止 `suspendTransport()` 执行。清理过程会 await `startPromise` 和 `pendingStop`,确保任何飞行中的 open 或 stop 完成后才执行最终的 `transport.stop()`。
|
|
522
582
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- 由 scripts/bench-trend.mjs 生成 —— 整个文件均为机器生成,请修改脚本而非本文档。 -->
|
|
2
|
+
|
|
3
|
+
# 浏览器基准趋势
|
|
4
|
+
|
|
5
|
+
> 数据截至 2026-09-12,基于 14 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
|
|
6
|
+
|
|
7
|
+
发布门禁的对比基线是最近两份报告之间的 `pnpm bench:compare --fail-above-pct 50`(50% 上限用于吸收共享 runner 的噪声)。本文记录长期趋势:数值为逐指标延迟,越低越好;历史最优为本机观察到的最健康一次运行。
|
|
8
|
+
|
|
9
|
+
<!-- BENCH-TREND:BEGIN (machine-generated table) -->
|
|
10
|
+
| 指标 | 上次 (ms) | 本次 (ms) | Δ | 历史最优 (ms) |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| publish per-message (ms, lower is better) — dedicated | 61.1276 | 58.888 | -2.24 | 40.8227 |
|
|
13
|
+
| publish per-message (ms, lower is better) — shared | 38.5784 | 45.0451 | +6.47 | 33.8206 |
|
|
14
|
+
| wildcard dispatch ×1000 (ms, lower is better) | 6.5 | 7.5 | +1.00 | 0.1 |
|
|
15
|
+
| publishBatch ×1000 (ms, lower is better) | 4.5 | 5.1 | +0.60 | 0.4 |
|
|
16
|
+
| dedup ×1000 (ms, lower is better) | 13.4 | 14.5 | +1.10 | 0 |
|
|
17
|
+
| trace + publish ×1000 (ms, lower is better) | 5.3 | 5.2 | -0.10 | 4.8 |
|
|
18
|
+
| first-packet cold dispatch (ms, lower is better) | 0.1 | 0 | -0.10 | 0 |
|
|
19
|
+
<!-- BENCH-TREND:END -->
|
|
20
|
+
|
|
21
|
+
说明:
|
|
22
|
+
|
|
23
|
+
- publish 行测量完整演示链路(发布点击 → transport → 演示服务器 → EVENT 扇出 → 接收方指标),包含真实浏览器与服务器延迟;databus 行是页内热路径微基准。
|
|
24
|
+
- 回归信号是多轮持续上移,而不是单次离群值。打 tag 发布前,应排查对应窗口内的热路径改动。
|
package/docs/zh/capabilities.md
CHANGED
|
@@ -20,20 +20,38 @@
|
|
|
20
20
|
| 异常恢复 | Worker TTL、过期 owner 迁移和协调缓存清理 | ✅ 已实现 | 回收死 Worker、孤儿 subscriber 和无 subscriber 的过期路由 |
|
|
21
21
|
| Transport | 重连后重放 owner Topic | ✅ 已实现 | 断开连接不清除业务 handler 和订阅意图 |
|
|
22
22
|
| 降级 | localStorage 或 BroadcastChannel 不可用时本地运行 | ✅ 已实现 | 保留当前 Tab 的连接和订阅能力 |
|
|
23
|
+
| 降级 | BroadcastChannel 不可用时 opt-in 启用 localStorage storage-event 协调通道 | ✅ 已实现 | `createBrowserEnvironment({ channelFallback: 'storage-event' })`;协调载荷(明文 Topic 名称)会写入 localStorage,已在文档中标注权衡 |
|
|
23
24
|
| Centrifuge | 内置 Dedicated / Shared Worker transport | ✅ 已实现 | 支持 subscribe、unsubscribe、publish、连接状态和错误上报;`auto` 从 SharedWorker → Dedicated Worker → 主线程 WebSocket 降级 |
|
|
24
25
|
| 安全边界 | localStorage 使用连接和 Topic 派生不透明 key;BroadcastChannel 协调消息以明文传输 Topic 名称 | ✅ 已实现 | 不持久化 URL、原始 Topic 名称、凭证或 publication payload。BroadcastChannel 协调消息仅存在于内存中,以明文传输 Topic 名称——不会被持久化。 |
|
|
25
|
-
| 诊断 | 聚合生命周期、吞吐量、分发延迟、去重结果、恢复重试、路由确认和迁移 | ✅ 已实现 | 默认关闭;默认每 5 秒输出指标,并以有界 reliability
|
|
26
|
+
| 诊断 | 聚合生命周期、吞吐量、分发延迟、去重结果、恢复重试、路由确认和迁移 | ✅ 已实现 | 默认关闭;默认每 5 秒输出指标,并以有界 reliability 事件记录恢复和路由协调。按需 `getMetrics()` / `getDiagnostics().metrics` 无需 sink 即可同步暴露当前窗口 |
|
|
27
|
+
| 诊断 | `getHealthSummary()` 单对象健康判定与统一失败账本 | ✅ 已实现 | `healthy`/`state` 覆盖未启动、启动中、恢复中、BFCache 挂起与恢复耗尽降级;`lastFailure` 统一 transport/persistence/dispatch 来源,`start()` 时重置 |
|
|
26
28
|
| 性能 | 协调元数据批量写入 + 退避重试 | ✅ 已实现 | 心跳、路由和 subscriber 写入合并后在微任务中 flush;失败时指数退避;`pagehide` / `stop()` 同步 flush |
|
|
27
29
|
| 性能 | 可选 ArrayBuffer Transferable 传输 | ✅ 已实现 | 开启 `transferable: true` 后,二进制 publish/receive 跳过 structured clone 复制;对象消息 API 不变 |
|
|
30
|
+
| 性能 | 可选 `DataBusTransport.publishBatch` 单帧突发发布 | ✅ 已实现 | 内置 WebSocket transport 将多条消息合并为一帧(`publishBatch` op,demo server 已支持);无批量能力的 transport 回退逐条 `publish` |
|
|
28
31
|
| 消息语义 | exactly-once 投递 | 未实现 | 正常交接会避免重叠,但异常恢复和 transport/服务端行为仍不提供 exactly-once 保证 |
|
|
29
32
|
| 消息语义 | 可插拔的 publication 去重 | ✅ 已实现 | 按 `DataBusMessage.messageId` 做可选有界入站抑制;默认关闭,ID 仍由调用方/服务端控制 |
|
|
30
|
-
| 认证 | Worker 内异步凭证刷新桥接 |
|
|
31
|
-
| 负载策略 |
|
|
33
|
+
| 认证 | Worker 内异步凭证刷新桥接 | ✅ 已实现 | `createCentrifugeDataBus` 的可选 `credentialProvider`(`getToken` / `getChannelToken`)。Worker 配置保持结构化可克隆;Worker 通过 TOKEN_REQUEST / TOKEN_RESPONSE 交换向主线程请求每个新凭证,由 provider 从应用上下文提供 |
|
|
34
|
+
| 负载策略 | 按消息速率、字节数或调度滞后自适应加权 | ✅ 已实现 | 可选的 `loadWeighting`(`messageRateWeight`、`byteRateWeight`、`scheduleLagWeight`):Workers 按窗口采样自身 fan-out 流量与心跳调度超时并随记录发布;新 route 的 owner 选择在 Topic 数之上加入归一化速率与滞后比率。默认(未设置)保持纯按 Topic 数;已有 route 保持 sticky |
|
|
32
35
|
| 可观测性 | owner 确认、迁移和恢复尝试的指标/事件 | ✅ 已实现 | `DataBusReliabilityTraceEvent` 记录有界的 route ack/migration 与 transport recovery;服务端最终确认仍由 transport 决定 |
|
|
33
36
|
| 运行时模型 | SharedWorker / Dedicated Worker transport | ✅ 已实现 | `workerMode` 支持 `dedicated`、`shared` 和 `auto`,默认 `dedicated` |
|
|
34
37
|
| 运行时模型 | Service Worker transport | 未实现 | 刻意延后;生命周期与长连接约束见 `docs/zh/architecture.md` |
|
|
35
38
|
| 持久消息 | 跨页面关闭持久化 publication 或发布命令 | 未实现 | SDK 不持久化业务 payload,也不在恢复后重放发布命令 |
|
|
36
39
|
|
|
40
|
+
## 已验证保证(由回归套件钉住)
|
|
41
|
+
|
|
42
|
+
以下不变量统一收自 `docs/zh/architecture.md` 的「稳定性不变量」一节(由 `tests/stability.test.ts` 与 `tests/replay-persistence.test.ts` 固化的回归),保证每条保证既成文又被回归锁定:
|
|
43
|
+
|
|
44
|
+
| 不变量 | 领域 | 被回归固化的保证 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| 交接 ACK 有效性 | 协调 | 仅当 route 仍指向接收方、释放方匹配 `handoffFromWorkerId`、且 ACK 代数 ≥ 存储 route 代数时才接受 `ROUTE_RELEASED`——更早交接轮次的过期 ACK(如 a↔b 乒乓)会被丢弃 |
|
|
47
|
+
| 回放持久化清理顺序 | 持久性 | 排队中的批量 flush 会按先到清理过滤(`unsubscribe`/`clearReplayTopic` 丢弃该 topic 的待写条目,`clearReplayBefore` 丢弃早于截止时间的条目);已清历史不会被进行中的 flush 重新追加 |
|
|
48
|
+
| 存储写入恢复 | 协调 | 合并写以指数退避重试(50 ms → 1.6 s 上限);结构性失败键在 5 次后丢弃(并 `console.warn`)而不阻塞其他排队键;队列清空或 `clear()` 取消后退避重置 |
|
|
49
|
+
| 传输恢复预算 | 生命周期 | 自动恢复由冷却间隔节流、以 `recovery.maxAttempts` 为界,预算耗尽时报 `exhausted`;成功重开后重置尝试计数与 exhausted,断线传输上的显式 `subscribe` 仍可手动恢复 |
|
|
50
|
+
| BFCache 挂起 | 生命周期 | 隐藏页面停掉传输并静默取消进行中的持久化重试;pageshow 重开传输并每个周期恰好一次重建订阅 |
|
|
51
|
+
| 交接通道关闭顺序 | 协调 | `pause()` 将物理 `channel.close()` 推迟一个任务,确保排队中的交接帧(含 `ROUTE_RELEASED`)先冲刷再关闭 |
|
|
52
|
+
| 丢失与恢复矩阵 | 协调 | 每条协调消息都有有界的恢复路径(未确认 SUBSCRIBE 的重发、REGISTRY 催促、丢失 ACK 的 TTL 清理 + 重新选举);传输断线窗口内丢失的发布是唯一有文档佐证的不恢复损失 |
|
|
53
|
+
| 恢复诊断 | 可观测性 | `getHealthSummary()` 得出单一就绪判定,统一的 `lastFailure` 账本跨传输失败持续保留,并在每次显式 `start()` 时重置 |
|
|
54
|
+
|
|
37
55
|
## 验收标准
|
|
38
56
|
|
|
39
57
|
- "已实现"不代表每个浏览器环境都能提供跨 Tab 能力;缺少 localStorage 或 BroadcastChannel 时,按设计降级为本地运行。
|
package/docs/zh/configuration.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
| `transport` | `DataBusTransport<TConfig, TData>` | 必填 | 真实连接和订阅实现 |
|
|
13
13
|
| `initialConfig` | `TConfig` | 无 | 自动启动时传给 transport |
|
|
14
14
|
| `autoStart` | `boolean` | 传入 `initialConfig` 时为 `true` | 是否在创建实例后自动启动 |
|
|
15
|
+
| `recovery.cooldownMs` | `number` | `1000` | 出错后自动重开 transport 前的最小延迟 |
|
|
16
|
+
| `recovery.maxAttempts` | `number` | `Infinity` | 每轮恢复序列的最大自动重开次数;显式订阅需求仍可继续重试 |
|
|
15
17
|
| `storagePrefix` | `string` | `cross-tab-worker-databus` | storage key 和 BroadcastChannel 的命名空间 |
|
|
16
18
|
| `maxActiveWorkers` | `number` | `3` | 可作为 Topic owner 的最大 Worker 数 |
|
|
17
19
|
| `heartbeatIntervalMs` | `number` | `3000` | Worker 心跳间隔 |
|
|
@@ -54,6 +56,27 @@ const bus = new CrossTabDataBus({
|
|
|
54
56
|
|
|
55
57
|
`replay.persistenceRetry` 可选地控制瞬时持久化失败的恢复。`maxAttempts` 是总尝试次数(默认 `1`),`backoffMs` 是首次重试前的延迟(默认 `50`);延迟会指数增长并封顶。最终失败仍沿用现有 `onError` 和 reliability 行为。
|
|
56
58
|
|
|
59
|
+
### Replay 选项
|
|
60
|
+
|
|
61
|
+
| 配置 | 类型 | 默认值 | 说明 |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `maxPerTopic` | `number` | `100` | `count`/`both` 下每个 topic 最多缓冲的 publication 数;超出时先淘汰最旧条目。`age` 下带时间戳条目按 retention 窗口有界,无时间戳的 legacy 条目仍受该值限制(正安全整数) |
|
|
64
|
+
| `persistence` | `DataBusReplayPersistence` | — | 可选持久化后端(`createIndexedDbReplayPersistence`);省略则历史仅存内存 |
|
|
65
|
+
| `retentionMs` | `number` | — | 生产者时间戳保留窗口;早于 cutoff 的历史通过适配器的 `clearBefore` 清理 |
|
|
66
|
+
| `pruneStrategy` | `'count' \| 'age' \| 'both'` | `'count'` | `count` 按 `maxPerTopic` 截断;`age` 按 `retentionMs` 清理带时间戳历史,并以 `maxPerTopic` 限制无时间戳的 legacy 条目;`both` 两者都应用。`age` 未配置 `retentionMs` 时无 age 可依,回退为数量上限 |
|
|
67
|
+
| `retentionSweepMs` | `number` | — | 面向安静 topic 的周期性 durable retention sweep;需要 `retentionMs` 与实现 `clearBefore` 的适配器 |
|
|
68
|
+
| `persistenceRetry` | `{ maxAttempts, backoffMs }` | `1` / `50` | 瞬时持久化失败的有界重试;延迟指数增长并封顶 |
|
|
69
|
+
|
|
70
|
+
### 去重选项
|
|
71
|
+
|
|
72
|
+
| 配置 | 类型 | 默认值 | 说明 |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `maxEntries` | `number` | `1000` | 记忆的 message ID 上限,超出时先淘汰最旧(FIFO)条目(正安全整数) |
|
|
75
|
+
| `ttlMs` | `number` | `60000` | 记忆 ID 抑制重复的时长(正有限值) |
|
|
76
|
+
| `sweepMs` | `number` | — | 可选的周期性清理,使安静 ID 过期;默认关闭 |
|
|
77
|
+
| `now` | `() => number` | `Date.now` | 可注入时钟,便于确定性 TTL 测试与非墙钟宿主 |
|
|
78
|
+
| `adaptiveTtl` | `{ minMs, maxMs }` | — | 可选有界自适应 TTL:近期消息速率高时窗口向 `minMs` 收紧,安静时放宽到 `maxMs`(两者必须同时提供,且 `minMs <= maxMs`) |
|
|
79
|
+
|
|
57
80
|
启用 trace 后,每次在最终尝试之前发生的重试都会发出有界 `reliability` 事件:`operation: persistence_retry`,并包含 `persistenceOperation`(`load`、`append`、`clear`、`clearTopic` 或 `clearBefore`)和失败的尝试次数。不包含 payload、URL、凭证或错误正文。
|
|
58
81
|
|
|
59
82
|
WebSocket 二进制帧可能以 `ArrayBuffer` 或浏览器 `Blob` 到达;两者使用相同的紧凑二进制 publication 格式。Blob 转换是异步的,转换失败会通过 transport error handler 报告。
|
|
@@ -98,6 +121,29 @@ active 集合只在 Topic 需要选择新 owner 时使用。已有 owner Worker
|
|
|
98
121
|
- `2-3`:在资源复用和故障恢复之间取得平衡。
|
|
99
122
|
- 更大值:适合 Topic 很多且单连接存在服务端限制的场景。
|
|
100
123
|
|
|
124
|
+
### 自适应 owner 加权
|
|
125
|
+
|
|
126
|
+
默认情况下新 Topic 分配给拥有最少 Topic 的 owner。可选的 `loadWeighting` 加入流量与调度信号,使新 route 偏向更空闲、更健康的 Worker——已有 route 保持 sticky,永不被迁移。
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const bus = createCentrifugeDataBus({
|
|
130
|
+
connection: { url: getConnectionUrl() },
|
|
131
|
+
loadWeighting: {
|
|
132
|
+
messageRateWeight: 0.5, // 每条消息/秒的权重
|
|
133
|
+
byteRateWeight: 0.001, // 每字节/秒的权重
|
|
134
|
+
scheduleLagWeight: 2 // 心跳调度滞后比率的权重
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| 选项 | 默认值 | 作用 |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `messageRateWeight` | `0` | 每个 Worker 按心跳窗口采样自身的 fan-out 消息速率,归一化速率计入有效负载。 |
|
|
142
|
+
| `byteRateWeight` | `0` | 同上,针对近似 payload 字节数/秒。 |
|
|
143
|
+
| `scheduleLagWeight` | `0` | 加权调度滞后比率(`overrunMs ÷ windowMs`)。事件循环饥饿会让心跳延迟,从而引导新 route 远离被节流的 Worker。 |
|
|
144
|
+
|
|
145
|
+
所有权重默认 `0`,保持纯按 Topic 数的 legacy 行为字节级不变。每个权重必须是非负有限数;负数或非有限值会在构造时抛出 `TypeError`,因为负权重会把新 route 导向**最繁忙**的 Worker。每个 Worker 的采样(`WorkerThroughputSample`)随心跳发布,含 `windowMs`、`messageCount`、`byteCount`、`overrunMs`、`sampledAt`。
|
|
146
|
+
|
|
101
147
|
## Centrifuge 配置
|
|
102
148
|
|
|
103
149
|
`createCentrifugeDataBus<TData>(options)` 的主要配置:
|
|
@@ -112,6 +158,7 @@ active 集合只在 Topic 需要选择新 owner 时使用。已有 owner Worker
|
|
|
112
158
|
| `heartbeatIntervalMs` | `number` | `10000` | SharedWorker PING 心跳间隔(见下方 SharedWorker 会话回收);传 `Infinity` 完全禁用心跳。与 Core 集群心跳(默认 3000 ms,通过 localStorage 跟踪 worker 存活)相互独立 |
|
|
113
159
|
| `workerFactory` | `() => Worker` | 内置 Worker | 测试或自定义 Worker 加载方式 |
|
|
114
160
|
| `sharedWorkerFactory` | `() => SharedWorker` | 内置 SharedWorker | 测试或自定义 SharedWorker 加载方式 |
|
|
161
|
+
| `credentialProvider` | `{ getToken?, getChannelToken? }` | `undefined` | 异步凭证刷新桥:Worker 向主线程请求每个新 token(`getToken` / `getChannelToken`),由该 provider 从应用上下文提供。必要原因:函数型 Centrifuge 选项无法 structured-clone 进 Worker |
|
|
115
162
|
| 其他 Core 配置 | 对应类型 | Core 默认值 | `storagePrefix`、心跳、TTL 等 |
|
|
116
163
|
|
|
117
164
|
```ts
|
|
@@ -144,6 +191,12 @@ const bus = createCentrifugeDataBus({
|
|
|
144
191
|
|
|
145
192
|
`sharedWorkerFactory` 和 `workerFactory` 都未提供时,transport 在启动时检测全局 `SharedWorker` / `Worker` 能力。提供自定义 factory 时,对应后端视为可用,避免在 Node、SSR 或嵌入环境中被全局能力检测误判。各模式都会执行相同的结构化克隆校验,配置和 `publish` 数据必须可结构化克隆。
|
|
146
193
|
|
|
194
|
+
## 协调通道降级(BroadcastChannel 不可用)
|
|
195
|
+
|
|
196
|
+
当 `BroadcastChannel` 不可用(部分 WebView、旧浏览器)时,集群通常降级为本地模式:transport 可用,但 Tab 之间不协调 owner。
|
|
197
|
+
|
|
198
|
+
`createBrowserEnvironment({ channelFallback: 'storage-event' })` 可选择启用基于 localStorage `storage` 事件的降级 `ClusterChannel`,保留跨 Tab 协调能力。该能力为 opt-in,原因是安全权衡:BroadcastChannel 消息仅存在于内存,而降级通道会把协调载荷(含明文 Topic 名称)写入 localStorage 的 `cross-tab-worker-databus:channel:` 键空间——至少短暂落盘,Tab 崩溃后可能长期留存。通道关闭时会清除该键。
|
|
199
|
+
|
|
147
200
|
## SharedWorker 会话回收
|
|
148
201
|
|
|
149
202
|
`MessagePort` 没有 `close` 事件,因此 SharedWorker 无法在 Tab 崩溃或关闭时获知(除非收到 `STOP` 消息)。为避免泄漏已死 Tab 的 `CentrifugeSession`(及其 WebSocket),transport 定期向 SharedWorker 发送 **PING 心跳**,SharedWorker 运行一个**回收器**来关闭超过静默超时的端口会话。
|
|
@@ -168,3 +168,38 @@ const config = await loadTransportConfig();
|
|
|
168
168
|
await bus.start(config);
|
|
169
169
|
bus.subscribe('resource.changed', handleResourceEvent);
|
|
170
170
|
```
|
|
171
|
+
|
|
172
|
+
## 10. 健康摘要与协调降级
|
|
173
|
+
|
|
174
|
+
就绪探针与仪表盘可直接消费单对象健康判定,无需自行拼装诊断:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const health = bus.getHealthSummary();
|
|
178
|
+
// { healthy: true, state: 'healthy', recovery: { attempt, … }, lastFailure: null, … }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
React 与 Vue 适配器提供 `useCrossTabHealth`,在状态变化、错误与轮询间隔时刷新。
|
|
182
|
+
|
|
183
|
+
当 `BroadcastChannel` 不可用时,各 Tab 通常退化为本地模式。可选择启用基于 localStorage storage 事件的降级通道:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { createBrowserEnvironment } from 'cross-tab-worker-databus';
|
|
187
|
+
|
|
188
|
+
const bus = new CrossTabDataBus({
|
|
189
|
+
/* … */
|
|
190
|
+
environment: createBrowserEnvironment({ channelFallback: 'storage-event' })
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
该能力为 opt-in:启用后协调载荷(明文 Topic 名称)会写入 localStorage,详见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
|
|
195
|
+
|
|
196
|
+
## 11. 升级与弃用
|
|
197
|
+
|
|
198
|
+
在 `1.0.0` 之前,SDK 允许增量新增,并在明确的弃用周期后才移除 API。根导出面由回归套件与 tag 间兼容性门禁钉住,意外删除会令 CI 失败,而不是静默破坏既有消费者。
|
|
199
|
+
|
|
200
|
+
升级时请注意:
|
|
201
|
+
|
|
202
|
+
- 逐版阅读当前版本到目标版本之间每个版本的 CHANGELOG。每次移除都会在其中明确指出,且弃用周期只在小版本间顺延。
|
|
203
|
+
- 若运行时 `console.warn` 提到某弃用别名,请在下一个小版本前迁移——该别名在告警首次出现后至少再存活一个小版本。
|
|
204
|
+
- 混版本 Tab 仍可协调:集群在诊断中携带协议版本(`getDiagnostics().protocol`),协议变更后 legacy 消息帧至少再解析一个小版本。
|
|
205
|
+
- 1.0 前,请优先使用 [api.md](./api.md) 中的文档化入口与 [capabilities.md](./capabilities.md) 的能力矩阵。每次发布都会对 ESM 与 CommonJS 消费者做打包验证。
|
|
@@ -2,16 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
每个 1.0.0 之前的版本都按此清单执行。仓库不由助手执行发布;完成打包验证并人工审阅后,再手动运行 npm 命令。
|
|
4
4
|
|
|
5
|
+
## 公共 API 稳定性与弃用策略(1.0 之前)
|
|
6
|
+
|
|
7
|
+
- 根导出面由 `tests/dual-format.test.ts`(冻结测试)与 `scripts/verify-version-compat.mjs`(tag 间兼容门禁)钉住。新增或删除导出是经过评审的刻意变更;同一提交中必须同步更新 `docs/api.md` 与 CHANGELOG。
|
|
8
|
+
- 1.0 前的破坏性变更仅在走完弃用周期后才允许:legacy 别名至少保留一个小版本,首次使用时 `console.warn` 提示,且只在 CHANGELOG 明确标注移除的小版本中删除。
|
|
9
|
+
- 协议别名(worker/cluster/transport 消息形态)遵循同一规则:弃用后至少一个小版本继续解析 legacy 帧,保证混版本 peer 兼容(见 `getDiagnostics().protocol` 的协议版本诊断)。
|
|
10
|
+
- 晋升 `1.0.0` 要求公共 API 与协议弃用策略正式冻结,并发布迁移指南(见 `docs/roadmap.md` 的 0.13.0 candidates)。
|
|
11
|
+
|
|
12
|
+
## 自动化门禁(CI)
|
|
13
|
+
|
|
14
|
+
`CI` 工作流的 `verify` job 在每次 push 与 pull request 上运行 `pnpm check`、`pnpm lint`、`pnpm test:coverage`(`vitest.config.ts` 中的下限:语句 85% / 分支 80% / 函数 90% / 行 85%)、`pnpm verify:compat`、`pnpm verify:pack`、`pnpm bench` 与 `pnpm audit`;`browser` job 运行 Playwright E2E 套件。`Release` 工作流在发布前重跑 `verify:compat` 与 `verify:pack`,随后执行阻塞式 `verify:published` 门禁。两个工作流的 checkout 均使用 `fetch-depth: 0` + `fetch-tags: true`,因为 `verify:compat` 需要从最近的发布 tag 解析基线。
|
|
15
|
+
|
|
16
|
+
只有 `pnpm bench:browser` / `pnpm bench:compare` 保持仅本地执行:共享 runner 的计时噪声会让数值型 CI 门禁不可靠。
|
|
17
|
+
|
|
5
18
|
## 打 tag 前
|
|
6
19
|
|
|
7
20
|
1. 更新 `package.json`、`CHANGELOG.md` 和中英文 roadmap。
|
|
8
|
-
2. 运行 `pnpm check`、`pnpm lint`、`pnpm bench`、`pnpm test:e2e`、`pnpm bench:browser`、`pnpm verify:pack` 以及 `git diff --check
|
|
9
|
-
3.
|
|
10
|
-
4.
|
|
21
|
+
2. 运行 `pnpm check`、`pnpm lint`、`pnpm test:coverage`、`pnpm bench`、`pnpm test:e2e`、`pnpm bench:browser`、`pnpm verify:pack`、`pnpm verify:compat` 以及 `git diff --check`(`verify:compat` 断言 `COMPAT_BASE_TAG` 基线中的 package `exports` 子路径与类型字段仍然存在;`verify:pack` 从打包产物冒烟导入完整根公共面与全部子路径的 ESM/CJS。`verify:compat` 从最近的发布 tag 解析基线,因此浅克隆或缺少 tag 的克隆需先执行 `git fetch --tags`,否则会以 "no version tag found" 失败)。
|
|
22
|
+
3. 依赖安全门禁:`pnpm audit --registry=https://registry.npmjs.org`(配置的镜像 registry 缺少 audit 端点;CI 在 verify job 中于公共 registry 运行)。任一已知漏洞公告即视为发布失败;`pnpm-workspace.yaml` overrides 钉住补丁版本。
|
|
23
|
+
4. 浏览器基准回归门禁:运行两次 `pnpm bench:browser` 后执行 `pnpm bench:compare --fail-above-pct 50`(50% 上限用于吸收共享 runner 的无关噪声,参见已知的共享 runner 抖动说明);基线迁移(例如某指标从空操作变为真实路径)属预期内的一次性失败。用 `pnpm bench:trend` 刷新长期趋势文档,表格变化时一并提交。
|
|
24
|
+
5. 用 `npm pack --dry-run --json` 确认发布包只包含预期文件。
|
|
25
|
+
6. 提交、给精确版本打 tag,并推送 `main --tags`。
|
|
26
|
+
|
|
27
|
+
## 安全与依赖扫描
|
|
28
|
+
|
|
29
|
+
仓库配置了 CodeQL(`javascript-typescript`,push/PR/每周)与 Dependabot(npm + GitHub Actions 每周更新)。CodeQL 告警会作为 PR check 暴露;Dependabot PR 在合并前必须通过其 verify(全量 check)与 CodeQL 检查,浏览器 E2E 的已知共享 runner 抖动按既有处理方式重跑确认。
|
|
30
|
+
|
|
31
|
+
## 打 tag 的发布工作流
|
|
32
|
+
|
|
33
|
+
推送版本 tag 会触发 `Release` GitHub Action:先跑 `pnpm check` 与 `pnpm lint`(tag 可能指向从未通过 CI lint 步骤的提交),再跑 `verify:compat` 与 `verify:pack`,从 `CHANGELOG` 对应章节生成 GitHub release,配置了 `NPM_TOKEN` 时自动发布到 npm,然后运行与手动执行相同预算的**阻塞式**消费者验证(`PUBLISHED_VERIFY_ATTEMPTS=24`、`PUBLISHED_VERIFY_DELAY_MS=5000`)。已发布包若无法被干净消费者导入,工作流即失败——任何 `verify:published` 失败都应视为发布失败,修复后重新发布该 tag。未配置 token 时跳过发布步骤,但验证仍会针对 npm 上已有的版本(例如手动发布的)通过。
|
|
11
34
|
|
|
12
|
-
##
|
|
35
|
+
## 发布(手动场景)
|
|
13
36
|
|
|
14
|
-
|
|
37
|
+
未在工作流配置 `NPM_TOKEN` 时,从对应 tag 的工作树手动运行 `npm publish --access public`。已经存在于 npm 的版本不能重复发布;npm 缺失的历史版本必须从对应 git tag 重建并逐个审阅,不能把当前工作树伪装成旧版本发布。
|
|
15
38
|
|
|
16
39
|
## 发布后
|
|
17
40
|
|
|
@@ -19,4 +42,4 @@
|
|
|
19
42
|
2. 在干净消费者中安装已发布版本或 tarball,并导入主入口及所有公开子路径。
|
|
20
43
|
3. 将结果记录到发布说明。在公开 API 和协议弃用策略明确冻结前,不进入 `1.0.0`。
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
打 tag 的发布工作流已自动执行上述消费者验证;仅在需要离线复验时才手动运行 `PUBLISHED_VERSION=<version> pnpm verify:published`。只有遇到异常慢的镜像才需要调整 `PUBLISHED_VERIFY_ATTEMPTS` 和 `PUBLISHED_VERIFY_DELAY_MS`。
|
package/docs/zh/roadmap.md
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
0.20.
|
|
3
|
+
0.20.86 正在推进。项目会先持续完成可靠性与协议兼容性的中版本迭代,再进入 1.0.0 稳定性冻结。
|
|
4
|
+
|
|
5
|
+
## 0.20.86 已完成范围
|
|
6
|
+
|
|
7
|
+
- 加固显式 stop/start 边界的生命周期:排队重启与进行中的 stop 串行化,并会被更新的 stop 取消,可通过 `ready()` 观测;被取代的异步开启不再拆除更新的 suspend/resume 转换;stop 期间调用的 `subscribe()` 与非空 `publish()`/`publishBatch()` 改为通过 `onError` 上报,不再修改 teardown 状态或被静默丢弃。
|
|
8
|
+
- 显式 `start()` 在自动恢复预算耗尽后,现在会执行文档所述的手动恢复,同时保留 cluster 状态、订阅与回放历史。
|
|
9
|
+
- IndexedDB replay 持久化在事务 abort(包括连接丢失导致的 abort)时结算全部 mutation,串行队列不再永久阻塞;replay AGE 裁剪改为内存与持久化历史共用同一套位置无关策略。
|
|
10
|
+
- 配置参考在中英文中完整记录 replay/dedup 公共选项,并新增从声明派生的文档守卫;固定种子属性不变量覆盖 active-worker 选择与 rebalance target。
|
|
11
|
+
|
|
12
|
+
## 0.20.85 已完成范围
|
|
13
|
+
|
|
14
|
+
- 新增固定种子的属性测试套件(`tests/property.test.ts`):针对纯热路径函数与有状态管理器,覆盖有限性/全函数性、与顺序无关的选择、循环安全的大小估算、publication topic/元数据有效性、`serializeError` 可克隆性,以及长时间随机操作序列下的 dedup/replay 上界。
|
|
15
|
+
- `effectiveWorkerLoad` 对损坏的存储基础负载保持全函数性——非有限值(JSON `1e999` → `Infinity`)不再泄漏进 owner 选择并重新引入数组顺序依赖。
|
|
16
|
+
- `approximatePayloadBytes` 增加深度上界,循环 payload(structured clone 会保留循环)不再使回放字节占用或自适应负载采样栈溢出。
|
|
17
|
+
- `serializeError` 始终产出可结构化克隆的结果;不可克隆的 context(函数/Symbol)会被丢弃,而不是让错误上报本身抛出 `DataCloneError`。
|
|
18
|
+
- 当 `pruneStrategy: 'age'` 未配置 `retentionMs` 时,回放历史重新受上界约束:内存环与 IndexedDB 记录均应用数量上限。
|
|
19
|
+
|
|
20
|
+
## 0.20.84 已完成范围
|
|
21
|
+
|
|
22
|
+
- 发布/CI 门禁由“文档约定”变为强制执行:`pnpm test:coverage`、`pnpm verify:compat`、`pnpm verify:pack` 进入 CI verify job,Release 工作流在发布前重跑 lint + compat + pack,两个 checkout 均拉取完整历史与 tag 以便解析 compat 基线。
|
|
23
|
+
- 协调恢复加固:交接 ACK 丢失、owner 崩溃或前任 owner 退出后,均由 worker-TTL 门禁的重新选举恢复,采用单写者与投影负载分摊;每次路由确认 / 迁移 / 恢复都会发出有界的 `reliability` trace 事件。
|
|
24
|
+
- 三个真实正确性修复:Vue `useCrossTabDataBus` 卸载泄漏(pending start 可能创建无人拥有的 bus)、自适应 dedup TTL 未在热路径生效,以及 `effectiveWorkerLoad` 会把损坏的存储 load 造成的非有限评分泄漏回 owner 选择。
|
|
25
|
+
- 产品 demo 可观测性:事件流渲染 reliability / subscription / coordination trace 事件,混沌开关在真实浏览器中演练丢 ACK 与崩溃恢复路径,配置面板显示当前混沌模式。
|
|
26
|
+
- 覆盖与工具链:`ReplayManager` / `DedupManager` 直接测试套件、transport 错误隔离与部分元数据覆盖、vitest 5(基准 API 已迁移)与 eslint 10 lint 配置;TypeScript 7 因 typescript-eslint 未支持而继续递延。
|
|
27
|
+
|
|
28
|
+
## 0.20.83 已完成范围
|
|
29
|
+
|
|
30
|
+
- 健康钩子的适配器边界用例、可归档对比的浏览器基准、与当前能力对齐的 README 特性清单。
|
|
31
|
+
- 一次大规模内部清理:全部运行时字符串字面量集中到 `utils/constants.ts` 并由之派生字面量类型,回放与去重从 `CrossTabDataBus` 拆分为自包含的 `ReplayManager` / `DedupManager`。
|
|
32
|
+
- demo 的「批量 10」publishBatch 按钮与 `/debug/wsstats` 帧计数及单帧 E2E,`asyncSink: true` 投递语义文档,以及与阻塞式发布消费者校验对齐的发布检查清单。
|
|
33
|
+
|
|
34
|
+
## 0.20.82 已完成范围
|
|
35
|
+
|
|
36
|
+
- E2E 默认断言上限提升至 20 秒;结构化克隆拒收(Symbol)与 cause 保留覆盖;共享模式会话生命周期经示例服务的连接数端点端到端验证。
|
|
37
|
+
|
|
38
|
+
## 0.20.81 已完成范围
|
|
39
|
+
|
|
40
|
+
- 浏览器基准新增 data-bus 热路径矩阵,健康摘要纳入 E2E 端到端断言,storage-event 通道与传输层批量进入 API 文档与能力矩阵。
|
|
41
|
+
|
|
42
|
+
## 0.20.80 已完成范围
|
|
43
|
+
|
|
44
|
+
- E2E 可靠性治理:失败 trace/视频与更长保留期、reload 类用例的先收敛后发布模式、符合文档保证的错峰突发模式,以及架构文档中的丢失与恢复矩阵。
|
|
45
|
+
|
|
46
|
+
## 0.20.79 已完成范围
|
|
47
|
+
|
|
48
|
+
- 已发布包消费自检成为 release 阻塞门禁(重试预算提升至 24 × 5 秒);丢失 ACK 交接的恢复链(TTL 清理 + 恢复后重选举)由回归固化。
|
|
49
|
+
|
|
50
|
+
## 0.20.78 已完成范围
|
|
51
|
+
|
|
52
|
+
- E2E 逐 Tab 断言真实 transport 后端;延迟关闭不变量由回归测试固化并写入架构文档;getting-started 覆盖健康摘要与协调降级。
|
|
53
|
+
|
|
54
|
+
## 0.20.77 已完成范围
|
|
55
|
+
|
|
56
|
+
- 修复无 factory 时静默降级本地会话的缺陷(打包的 Worker 现在真正被使用);补充默认后端与通道丢失恢复覆盖;demo 展示协调通道诊断与降级开关。
|
|
57
|
+
|
|
58
|
+
## 0.20.76 已完成范围
|
|
59
|
+
|
|
60
|
+
- 面向无 BroadcastChannel 环境的 opt-in storage-event 协调降级通道,含降级通道上的 owner 选举集成测试,并同步降级文档与能力矩阵。
|
|
61
|
+
|
|
62
|
+
## 0.20.75 已完成范围
|
|
63
|
+
|
|
64
|
+
- 单测套件新增热路径性能门禁(宽松阈值防灾难性退化,真实基准仍在 `pnpm bench`);IndexedDB replay 持久化新增脚本化故障注入,覆盖 invalidate 与恢复错误路径;审计确认 Release workflow 已集成已发布包消费自检。
|
|
65
|
+
|
|
66
|
+
## 0.20.74 已完成范围
|
|
67
|
+
|
|
68
|
+
- 可选的 `DataBusTransport.publishBatch`:WebSocket transport 单帧批量发送,demo server 支持批量帧,无批量能力的 transport 自动回退逐条发送;React/Vue 新增 `useCrossTabHealth` 绑定;健康判定纳入 transport 实时状态。
|
|
69
|
+
|
|
70
|
+
## 0.20.73 已完成范围
|
|
71
|
+
|
|
72
|
+
- IndexedDB replay 持久化适配器纳入单测(基于 `fake-indexeddb`):覆盖裁剪策略、批量分组、并发串行化、清理语义与瞬时打开失败恢复。
|
|
73
|
+
- 真实浏览器 E2E 新增三 Tab 并发发布突发与整连接重构建(stop/start)重入集群两个场景;architecture 文档新增稳定性不变量参考(中英文)。
|
|
74
|
+
|
|
75
|
+
## 0.20.72 已完成范围
|
|
76
|
+
|
|
77
|
+
- 扩展基准矩阵,覆盖 `publishBatch`、wildcard routing、dedup、replay prune、批量持久化与异步 trace sink。
|
|
78
|
+
- 长时稳定性加固:补齐 handoff ACK 世代校验、BFCache 往返、恢复耗尽重置、存储写退避恢复与 replay 持久化清理竞态的回归测试;修复反向的 stale-ACK 世代比较与批量 flush 复活清理历史两处缺陷。
|
|
79
|
+
- 生产能力:`getHealthSummary()` 就绪判定、`getPersistenceStats()`、diagnostics 中 transport 状态细化(status/suspended),以及构建时注入的 SDK 版本。
|
|
4
80
|
|
|
5
81
|
## 0.20.71 已完成范围
|
|
6
82
|
|
|
@@ -385,19 +461,31 @@
|
|
|
385
461
|
- publication metadata 做兼容性归一化:只接受非空 ID 与有限 timestamp。
|
|
386
462
|
- 补充 legacy、嵌套、fallback topic 和坏 metadata 协议夹具测试。
|
|
387
463
|
|
|
464
|
+
## 0.11.0 已完成范围
|
|
465
|
+
|
|
466
|
+
- 当持久化适配器支持 `clearBefore` 时,通过 `replay.retentionMs` 自动执行持久化回放留存。
|
|
467
|
+
- 周期性 trace 指标中加入去重接受/抑制计数。
|
|
468
|
+
- 覆盖 WebSocket、Centrifuge、Worker 边界与浏览器 E2E 的 publication metadata 兼容性测试。
|
|
469
|
+
- Service Worker transport 决策:在目标浏览器具备稳定的连接生命周期契约前,刻意保持不实现。
|
|
470
|
+
|
|
388
471
|
## 0.20.68 已交付
|
|
389
472
|
|
|
390
473
|
- 为集群帧和 worker snapshot 增加协议版本元数据,并保持旧版本缺失字段时的兼容处理。
|
|
391
474
|
|
|
392
475
|
## 0.20.69 候选
|
|
393
476
|
|
|
394
|
-
1.
|
|
395
|
-
2.
|
|
396
|
-
3.
|
|
397
|
-
4.
|
|
477
|
+
1. ~~增加 peer 能力矩阵,并在 diagnostics 暴露 SDK、后端与 transport 身份。~~ 已交付:`getDiagnostics()` 携带协议版本、未知消息统计、peer 协议版本与 transport 身份;`getHealthSummary()`/`getMetrics()` 现在也会随单一对象输出实时 trace 指标与 sink 状态。
|
|
478
|
+
2. ~~统一 replay、dedup、trace、recovery 与 cluster 健康指标。~~ 已交付:`getDiagnostics()` + `getMetrics()` + `getHealthSummary()` 在单一快照中覆盖生命周期、恢复、dedup、replay、持久化、协议、transport、cluster、trace 指标与 sink 背压。
|
|
479
|
+
3. ~~优化 IndexedDB 并发 append 与清理路径。~~ 已交付:相邻 `appendBatch` 变更合并为单事务;`clear`/`clearTopic`/`clearBefore` 顺序保持。
|
|
480
|
+
4. ~~增加 adaptive dedup、async trace、prune 和长时多 Tab 性能基线。~~ 部分交付:bench 覆盖负载加权评分、`getMetrics`、publishBatch 批次敏感性,以及既有 dedup/async-sink/persistence 用例。
|
|
398
481
|
|
|
399
482
|
## 0.13.0 候选
|
|
400
483
|
|
|
484
|
+
1. 冻结公共导出面与 transport 无关的 publication 信封。
|
|
485
|
+
2. 精确记录 at-least-once 投递与去重保证。
|
|
486
|
+
3. 增加长时浏览器浸泡覆盖:replay 留存、重连、BFCache 与 owner 迁移。
|
|
487
|
+
4. 为 1.0 前的协议别名发布迁移指南与弃用策略。
|
|
488
|
+
|
|
401
489
|
## 更长期候选
|
|
402
490
|
|
|
403
491
|
1. **Replay 生命周期与留存**:增加持久化 `clear`/`clearTopic`,退订/替换时清理旧历史,并通过 trace 与 error handler 暴露持久化失败;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cross-tab-worker-databus",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.86",
|
|
4
4
|
"description": "Framework-agnostic cross-tab data bus with Dedicated/Shared Worker clustering and Centrifuge support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,9 +24,29 @@
|
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"docs",
|
|
27
|
+
"docs/api.md",
|
|
28
|
+
"docs/architecture.md",
|
|
29
|
+
"docs/benchmarks.md",
|
|
30
|
+
"docs/capabilities.md",
|
|
31
|
+
"docs/configuration.md",
|
|
32
|
+
"docs/getting-started.md",
|
|
33
|
+
"docs/release-checklist.md",
|
|
34
|
+
"docs/roadmap.md",
|
|
35
|
+
"docs/transports.md",
|
|
36
|
+
"docs/README.md",
|
|
37
|
+
"docs/zh/api.md",
|
|
38
|
+
"docs/zh/architecture.md",
|
|
39
|
+
"docs/zh/benchmarks.md",
|
|
40
|
+
"docs/zh/capabilities.md",
|
|
41
|
+
"docs/zh/configuration.md",
|
|
42
|
+
"docs/zh/getting-started.md",
|
|
43
|
+
"docs/zh/release-checklist.md",
|
|
44
|
+
"docs/zh/roadmap.md",
|
|
45
|
+
"docs/zh/transports.md",
|
|
46
|
+
"docs/zh/README.md",
|
|
28
47
|
"CHANGELOG.md",
|
|
29
48
|
"README.md",
|
|
49
|
+
"README.zh.md",
|
|
30
50
|
"LICENSE"
|
|
31
51
|
],
|
|
32
52
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -77,6 +97,8 @@
|
|
|
77
97
|
"test:coverage": "vitest run --coverage",
|
|
78
98
|
"bench": "vitest bench --run",
|
|
79
99
|
"bench:browser": "pnpm build && node scripts/bench-browser.mjs",
|
|
100
|
+
"bench:compare": "node scripts/bench-compare.mjs",
|
|
101
|
+
"bench:trend": "node scripts/bench-trend.mjs",
|
|
80
102
|
"verify:pack": "node scripts/verify-packed-consumer.mjs",
|
|
81
103
|
"verify:compat": "node scripts/verify-version-compat.mjs",
|
|
82
104
|
"verify:published": "node scripts/verify-published-consumer.mjs",
|
|
@@ -101,21 +123,22 @@
|
|
|
101
123
|
}
|
|
102
124
|
},
|
|
103
125
|
"devDependencies": {
|
|
104
|
-
"@eslint/js": "^
|
|
105
|
-
"@playwright/test": "^1.
|
|
126
|
+
"@eslint/js": "^10.0.1",
|
|
127
|
+
"@playwright/test": "^1.63.0",
|
|
106
128
|
"@testing-library/react": "^16.3.3",
|
|
107
|
-
"@types/node": "^
|
|
108
|
-
"@types/react": "^
|
|
109
|
-
"@vitest/coverage-v8": "^
|
|
110
|
-
"esbuild": "^0.
|
|
111
|
-
"eslint": "^
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"react
|
|
116
|
-
"
|
|
117
|
-
"typescript
|
|
118
|
-
"
|
|
129
|
+
"@types/node": "^26.5.1",
|
|
130
|
+
"@types/react": "^19.3.0",
|
|
131
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
132
|
+
"esbuild": "^0.28.2",
|
|
133
|
+
"eslint": "^10.10.0",
|
|
134
|
+
"fake-indexeddb": "^6.2.5",
|
|
135
|
+
"globals": "^17.12.0",
|
|
136
|
+
"jsdom": "^30.0.1",
|
|
137
|
+
"react": "^19.3.0",
|
|
138
|
+
"react-dom": "^19.3.0",
|
|
139
|
+
"typescript": "^6.0.3",
|
|
140
|
+
"typescript-eslint": "^8.70.0",
|
|
141
|
+
"vitest": "^5.0.0",
|
|
119
142
|
"vue": "^3.5.42"
|
|
120
143
|
},
|
|
121
144
|
"engines": {
|