cross-tab-worker-databus 0.21.12 → 0.21.13

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/centrifuge-protocol.d.ts.map +1 -1
  3. package/dist/centrifuge-session.d.ts +10 -3
  4. package/dist/centrifuge-session.d.ts.map +1 -1
  5. package/dist/centrifuge.d.ts +10 -4
  6. package/dist/centrifuge.d.ts.map +1 -1
  7. package/dist/centrifuge.js +23 -11
  8. package/dist/centrifuge.js.map +2 -2
  9. package/dist/centrifuge.shared.worker.js +28 -11
  10. package/dist/centrifuge.shared.worker.js.map +2 -2
  11. package/dist/centrifuge.worker.js +10 -3
  12. package/dist/centrifuge.worker.js.map +2 -2
  13. package/dist/{chunk-TSTV3SDC.js → chunk-4HW6PYIR.js} +67 -33
  14. package/dist/chunk-4HW6PYIR.js.map +7 -0
  15. package/dist/{chunk-2TTJ7LGZ.js → chunk-NPFTD7YE.js} +1 -1
  16. package/dist/chunk-NPFTD7YE.js.map +7 -0
  17. package/dist/cjs/centrifuge.cjs +86 -40
  18. package/dist/cjs/centrifuge.cjs.map +2 -2
  19. package/dist/cjs/hooks.cjs.map +2 -2
  20. package/dist/cjs/index.cjs +80 -36
  21. package/dist/cjs/index.cjs.map +2 -2
  22. package/dist/cjs/vue.cjs.map +2 -2
  23. package/dist/core/cluster.d.ts +33 -16
  24. package/dist/core/cluster.d.ts.map +1 -1
  25. package/dist/core/data-bus.d.ts +19 -5
  26. package/dist/core/data-bus.d.ts.map +1 -1
  27. package/dist/core/dedup-manager.d.ts +6 -3
  28. package/dist/core/dedup-manager.d.ts.map +1 -1
  29. package/dist/core/environment.d.ts +14 -5
  30. package/dist/core/environment.d.ts.map +1 -1
  31. package/dist/core/hash.d.ts +10 -4
  32. package/dist/core/hash.d.ts.map +1 -1
  33. package/dist/core/replay-manager.d.ts +2 -1
  34. package/dist/core/replay-manager.d.ts.map +1 -1
  35. package/dist/core/routing.d.ts +18 -9
  36. package/dist/core/routing.d.ts.map +1 -1
  37. package/dist/core/storage-batch.d.ts +2 -2
  38. package/dist/core/storage-batch.d.ts.map +1 -1
  39. package/dist/core/trace.d.ts +21 -9
  40. package/dist/core/trace.d.ts.map +1 -1
  41. package/dist/core/types.d.ts +13 -5
  42. package/dist/core/types.d.ts.map +1 -1
  43. package/dist/hooks.d.ts.map +1 -1
  44. package/dist/hooks.js +1 -1
  45. package/dist/hooks.js.map +2 -2
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +17 -7
  49. package/dist/index.js.map +2 -2
  50. package/dist/utils/constants.d.ts +7 -4
  51. package/dist/utils/constants.d.ts.map +1 -1
  52. package/dist/utils/validation.d.ts +12 -4
  53. package/dist/utils/validation.d.ts.map +1 -1
  54. package/dist/vue.js +1 -1
  55. package/dist/websocket.d.ts +24 -6
  56. package/dist/websocket.d.ts.map +1 -1
  57. package/dist/workers/port-reaper.d.ts +18 -8
  58. package/dist/workers/port-reaper.d.ts.map +1 -1
  59. package/docs/api.md +6 -5
  60. package/docs/benchmarks.md +9 -9
  61. package/docs/release-checklist.md +3 -3
  62. package/docs/roadmap.md +31 -1
  63. package/docs/transports.md +3 -2
  64. package/docs/zh/api.md +6 -5
  65. package/docs/zh/benchmarks.md +9 -9
  66. package/docs/zh/release-checklist.md +3 -3
  67. package/docs/zh/roadmap.md +22 -1
  68. package/docs/zh/transports.md +2 -1
  69. package/package.json +2 -1
  70. package/dist/chunk-2TTJ7LGZ.js.map +0 -7
  71. package/dist/chunk-TSTV3SDC.js.map +0 -7
@@ -6,11 +6,19 @@
6
6
  * same cross-tab clustering stack (owner dedup, sticky routes, EVENT fan-out)
7
7
  * that the Centrifuge backend uses.
8
8
  *
9
- * Wire protocol (JSON text frames):
9
+ * Wire protocol — two frame formats, both over text/binary WebSocket messages:
10
10
  * - client → server: `{"op":"subscribe"|"unsubscribe"|"publish","topic":...,"data":...}`
11
11
  * - client → server (batched): `{"op":"publishBatch","topic":...,"items":[{data,...}]}`
12
12
  * - server → client: `{"topic":...,"data":...}` for publications; anything
13
13
  * without a string `topic` field is ignored (forward-compatible).
14
+ * - binary publications use a *separate* tagged frame, not JSON:
15
+ * `0xc7 | uint16 topicLength | topic UTF-8 | raw payload`, written by
16
+ * `sendBinaryFrame` when `publish()` receives an `ArrayBuffer` with no
17
+ * `messageId`/`timestamp` (with metadata it degrades to a JSON PUBLISH whose
18
+ * `data` is a number array, so an id is never lost to the compact shape), and
19
+ * parsed by the matching reader. A server that implements only the JSON arm
20
+ * above silently drops those — see `scripts/demo-ws-server.mjs`, which
21
+ * implements both directions.
14
22
  */
15
23
  import { CrossTabDataBus } from './core/data-bus';
16
24
  import type { CrossTabDataBusOptions } from './core/data-bus';
@@ -80,7 +88,8 @@ export declare class WebSocketTransport<TData = unknown> implements DataBusTrans
80
88
  /** Idempotent: re-subscribing an active topic re-sends the frame but does
81
89
  * not duplicate the local tracking entry. */
82
90
  subscribe(topic: string): MaybePromise<void>;
83
- /** Idempotent: unsubscribing an unknown topic is a no-op. */
91
+ /** Idempotent for local tracking: an unknown topic has nothing to delete, but
92
+ * the UNSUBSCRIBE frame is still sent, exactly as SUBSCRIBE is re-sent above. */
84
93
  unsubscribe(topic: string): MaybePromise<void>;
85
94
  /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
86
95
  publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void>;
@@ -101,13 +110,22 @@ export declare class WebSocketTransport<TData = unknown> implements DataBusTrans
101
110
  private abortSocket;
102
111
  private clearConnectTimer;
103
112
  /** Send one JSON frame. Frames are dropped with an `onError` report when
104
- * the socket is not open — subscribe frames are re-sent on open, so the
105
- * only real loss is a publish during a disconnect window. */
113
+ * the socket is not open. A dropped SUBSCRIBE is recovered — `onopen`
114
+ * re-asserts every topic in `subscribedTopics` — but the other two are not:
115
+ * a dropped PUBLISH is the documented at-most-once loss, and a dropped
116
+ * UNSUBSCRIBE is the quieter one, because `unsubscribe()` has already deleted
117
+ * the topic from that set, so no later re-assert resends it and the server
118
+ * keeps streaming a channel the app left. Through the DataBus neither of the
119
+ * two is reachable while closed — `runTransport()` queues both operations
120
+ * behind recovery — so this reads as a direct-transport-contract note. */
106
121
  private sendFrame;
107
122
  private sendBinaryFrame;
108
123
  /** Parse a server frame. Only objects carrying a string `topic` are
109
- * publications; malformed JSON and unknown shapes are ignored so a chatty
110
- * server cannot crash the message path. */
124
+ * publications, and unknown shapes are dropped silently so a chatty server
125
+ * cannot crash the message path. Malformed JSON is *not* dropped silently: it
126
+ * goes to `handlers.onError` as "WebSocket server sent a non-JSON frame.",
127
+ * which also records it in the bus's last-failure diagnostics. Neither case
128
+ * throws out of the socket listener. */
111
129
  private handleMessage;
112
130
  }
113
131
  /** Create a CrossTabDataBus backed by a plain WebSocket transport.
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
@@ -51,12 +51,19 @@ export declare class PortReaper {
51
51
  * `Infinity` is the documented way to disable the PING heartbeat, and it means
52
52
  * this port can never be judged silent: `startHeartbeat()` sends nothing at all
53
53
  * once configured that way, so a timeout would be a death sentence with no
54
- * possible appeal. A non-finite-or-non-positive *other* value falls back to the
55
- * default so a bad payload cannot degenerate the reaper into a busy loop or
56
- * silence it — those three (NaN, 0, negative) are rejected by
57
- * `assertHeartbeatInterval` at the transport constructor and so can only arrive
58
- * from a main thread that is not this library. No-op for untracked ports (e.g.
59
- * setTimeout arrives after remove/STOP). */
54
+ * possible appeal. A non-finite-or-non-positive *other* value falls back to
55
+ * the default, which keeps the timeout comparison from degenerating into
56
+ * "always expired" — a NaN timeout reaps every port on the first tick. Read
57
+ * that narrowly, because it is not a busy-loop guard: the busy-loop vector is
58
+ * a tiny *positive* interval, which the ternary passes through untouched
59
+ * (0.0001 → a 0.0003 ms timeout → every tick reaps everything), and the
60
+ * reaper's own cadence is bounded elsewhere — `computeMinHeartbeat` starts
61
+ * from DEFAULT_HEARTBEAT_INTERVAL_MS and only takes a minimum, so a payload
62
+ * can shorten the sweep but never stretch it past 10 s. The three values
63
+ * rejected here (NaN, 0, negative) are also rejected by
64
+ * `assertHeartbeatInterval` at the transport constructor and so can only
65
+ * arrive from a main thread that is not this library. No-op for untracked
66
+ * ports (e.g. setTimeout arrives after remove/STOP). */
60
67
  setTimeout(port: MessagePort, heartbeatIntervalMs: number): void;
61
68
  /** Remove a port (STOP message). The caller is responsible for closing the
62
69
  * port and stopping the session. No-op for untracked ports so a duplicate
@@ -71,8 +78,11 @@ export declare class PortReaper {
71
78
  * each spawning redundant timers when the cadence hasn't changed. */
72
79
  private schedule;
73
80
  /** Smallest heartbeat interval among active ports, derived from each port's
74
- * configured session timeout. Exposed as a method so the cadence logic can
75
- * be unit-tested in isolation from the timer plumbing. */
81
+ * configured session timeout. Extracted because `schedule()` derives its delay
82
+ * from it on every re-arm. Not a test seam: nothing in `tests/` calls it
83
+ * (grep: zero hits), and the cadence is asserted through the injected
84
+ * `setTimer` — i.e. through the same plumbing this note used to claim it was
85
+ * isolated from. */
76
86
  private computeMinHeartbeat;
77
87
  /** Close sessions whose port has been silent longer than its timeout.
78
88
  * Iterates a snapshot. That is defensive rather than load-bearing: `Map`
@@ -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;;;;;;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"}
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;;;;;;;;;;;;;;;;4DAgBwD;IACxD,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;;;;;wBAKoB;IACpB,OAAO,CAAC,mBAAmB;IAQ3B;;;;;;;mDAO+C;IAC/C,OAAO,CAAC,IAAI;CAoDb"}
package/docs/api.md CHANGED
@@ -296,7 +296,7 @@ trace: {
296
296
  }
297
297
  ```
298
298
 
299
- Low-frequency event types include `lifecycle`, `status`, `subscription`, `coordination`, `reliability`, and `error`; high-frequency data is output as `message_metrics` per window, containing receive/dispatch counts, active topic count, and dispatch latency aggregates (`dispatchSamples`, `dispatchAvgMs`, `dispatchP50Ms`, `dispatchP95Ms`, `dispatchMaxMs`), and deduplication outcomes (`dedupAccepted`, `dedupSuppressed`). Route ownership changes surface as `reliability` events: a graceful handoff reports `operation: 'route_migration'`, while a re-election that recovered a stranded unconfirmed handoff (previous owner gone, ACK never arrived) reports `operation: 'route_migration_recovery'`, so trace consumers can tell recoveries apart from routine handoffs. The `coordination` event is emitted after each transport open and carries `coordinated`, `activeWorkers`, `workers` (formatted worker records), and `routes` (`topicKey@workerId|confirmed=…`) reflecting the settled route list. All public events use a fixed structure, and none of them carries a message payload, a connection address, or an error body. Topic plaintext appears in exactly two event types: `subscription`, which reports the transition, and `reliability`, which names the route for the route-scoped operations (`route_ack`, `route_migration`, `route_migration_recovery`) and omits it elsewhere. Every other event is topic-free, and `coordination` identifies routes by their opaque `topicKey` rather than by name. Since those two event types do carry topic names, treat a trace sink as a diagnostic surface and redact sensitive topic conventions before forwarding events to external telemetry (see `configuration.md`). Errors thrown by the sink are isolated and do not interrupt message dispatch, but are output to `console.warn` to facilitate diagnosing configuration issues. The sink should ideally not throw — capture expected error conditions in the event data rather than raising exceptions.
299
+ Low-frequency event types include `lifecycle`, `status`, `subscription`, `coordination`, `reliability`, and `error`; high-frequency data is output as `message_metrics` per window, containing receive/dispatch counts, active topic count, and dispatch latency aggregates (`dispatchSamples`, `dispatchAvgMs`, `dispatchP50Ms`, `dispatchP95Ms`, `dispatchMaxMs`), and deduplication outcomes (`dedupAccepted`, `dedupSuppressed`). Route ownership changes surface as `reliability` events: a graceful handoff reports `operation: 'route_migration'`, while a re-election that recovered a stranded unconfirmed handoff (previous owner gone, ACK never arrived) reports `operation: 'route_migration_recovery'`, so trace consumers can tell recoveries apart from routine handoffs. The `coordination` event is emitted once per successful `start()` — a later automatic or on-demand reopen emits no second snapshot — and carries `coordinated`, `activeWorkers`, `workers` (formatted worker records), and `routes` (`topicKey@workerId|confirmed=…`) reflecting the settled route list. All public events use a fixed structure, and none of them carries a message payload, a connection address, or an error body. Topic plaintext appears in exactly two event types: `subscription`, which reports the transition, and `reliability`, which names the route for the route-scoped operations (`route_ack`, `route_migration`, `route_migration_recovery`) and omits it elsewhere. Every other event is topic-free, and `coordination` identifies routes by their opaque `topicKey` rather than by name. Since those two event types do carry topic names, treat a trace sink as a diagnostic surface and redact sensitive topic conventions before forwarding events to external telemetry (see `configuration.md`). Errors thrown by the sink are isolated and do not interrupt message dispatch, but are output to `console.warn` to facilitate diagnosing configuration issues. The sink should ideally not throw — capture expected error conditions in the event data rather than raising exceptions.
300
300
 
301
301
  **`asyncSink: true` delivery semantics.** By default (`false`) the sink runs synchronously for each emitted event. With `asyncSink: true`, events are queued in memory and delivered in one microtask batch: the first event of a task schedules `queueMicrotask`, and every event emitted before that microtask runs is sent to the sink in FIFO order in a single pass (including the periodic `message_metrics` snapshots). The hot message path therefore never blocks on sink work. Error isolation is unchanged from the synchronous mode — a throwing sink is caught and reported via `console.warn` and never interrupts dispatch or the remaining events in the batch. Ordering boundary: order **within** a batch is guaranteed, but delivery is deferred to the next microtask, so a trace event is no longer observed before your next statement runs, and events emitted after a batch flushed settle into a later batch. Use the default synchronous sink (or consolidate counters yourself) when each event must be visible before the following line executes.
302
302
 
@@ -317,7 +317,8 @@ interface DataBusTransport<TConfig, TData> {
317
317
  unsubscribe(topic): void | Promise<void>;
318
318
  publish(topic, data): void | Promise<void>;
319
319
  /** Optional: one wire frame for many items. The DataBus falls back to
320
- * per-item `publish` calls when this is absent. */
320
+ * per-item `publish` calls when this is absent. Each item is a
321
+ * `DataBusPublicationItem` — `{ data, messageId?, timestamp? }`, exported. */
321
322
  publishBatch?(topic, items): void | Promise<void>;
322
323
  stop(): void | Promise<void>;
323
324
  }
@@ -376,7 +377,7 @@ createStorageEventChannel(options: {
376
377
  }): ClusterChannel | null
377
378
  ```
378
379
 
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
+ 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 follow BroadcastChannel where cross-tab coordination needs them — no echo to the sending tab, JSON-serializable messages, closed channels refuse further posts — with one deliberate divergence: `postMessage` also never dispatches to the channel's own listeners, so two channels created in the **same** document do not see each other, which a real BroadcastChannel does. Only cross-tab delivery matters to this fallback, and the divergence is pinned by the sibling-channel case in `tests/storage-channel.test.ts`; 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
381
 
381
382
  ## WebSocket Transport Backend
382
383
 
@@ -502,7 +503,7 @@ Selects the actual backend based on `WorkerMode` and capability detection, retur
502
503
  - `shared` / `auto`: SharedWorker -> Dedicated Worker -> local mode
503
504
  - `dedicated` (default): Dedicated Worker -> SharedWorker -> local mode
504
505
 
505
- `availability` can explicitly pass `worker` / `sharedWorker` capability flags, for use in SSR, testing, or embedded environments, avoiding access to non-existent global objects.
506
+ `availability` (`WorkerAvailability`, exported) can explicitly pass `worker` / `sharedWorker` capability flags, for use in SSR, testing, or embedded environments, avoiding access to non-existent global objects.
506
507
 
507
508
  ### `effectiveWorkerLoad(worker, options?)`
508
509
 
@@ -512,7 +513,7 @@ The score is always finite. A Worker with no throughput sample, an unset (all-ze
512
513
 
513
514
  ### `approximatePayloadBytes(payload)`
514
515
 
515
- Cheap, allocation-free estimate of a payload's wire size, used for the byte side of an adaptive load sample. It only runs when adaptive routing is enabled, so approximate sizes are fine — the goal is a stable cross-Worker comparison, not an exact byte count.
516
+ Cheap estimate of a payload's wire size, used for the byte side of an adaptive load sample and — because `getDiagnostics().replay` reports a `bytes` figure computed on demand — to size the retained replay buffers, so it is not gated on adaptive routing. It is not allocation-free either: an object node materialises `Object.values`. Approximate sizes are fine either way; the goal is a stable cross-Worker comparison, not an exact byte count.
516
517
 
517
518
  Sizes: `null`/`undefined` and symbols/functions `0`, booleans `4`, numbers and bigints `8`, strings their length, `ArrayBuffer`s and typed-array views their `byteLength`, arrays an 8-byte header plus their elements, and plain objects the sum of their values.
518
519
 
@@ -2,20 +2,20 @@
2
2
 
3
3
  # Browser Benchmark Trend
4
4
 
5
- > Data through 2026-09-23, from the 71 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
5
+ > Data through 2026-09-23, from the 87 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
6
6
 
7
- The comparison baseline for release gating is `pnpm bench:compare --fail-above-pct 50`, which compares the newest report against the median of the same metric in the preceding reports (50% ceiling absorbs shared-runner noise; a single previous report was a fragile baseline because the in-page hot-path metrics alternate between a fast and a slow mode on identical code). This doc records the long-run picture: values are per-metric latencies where lower is better, and the last column is the best run inside the most recent 5 reports — not an all-time record, because the in-page matrix changed measurement semantics in early September 2026 and older reports are not comparable.
7
+ The comparison baseline for release gating is `pnpm bench:compare --fail-above-pct 50`, which compares the newest report against the median of the same metric in the preceding reports *and* requires the new value to exceed the highest of those samples (50% ceiling absorbs shared-runner noise; a single previous report was a fragile baseline because the in-page hot-path metrics alternate between a fast and a slow mode on identical code — and a median over an alternating sample is only stable while the modes are mixed, so a percentage alone can still fail on nothing but a mode shift. Metrics excused by that second leg are listed as "within-baseline, not gated" rather than passing silently). This doc records the long-run picture: values are per-metric latencies where lower is better, and the last column is the best run inside the most recent 5 reports — not an all-time record, because the in-page matrix changed measurement semantics in early September 2026 and older reports are not comparable.
8
8
 
9
9
  <!-- BENCH-TREND:BEGIN (machine-generated table) -->
10
10
  | Metric | Previous (ms) | Latest (ms) | Δ | Best of last 5 runs (ms) |
11
11
  |---|---|---|---|---|
12
- | publish per-message (ms, lower is better) — dedicated | 55.4416 | 50.6133 | -4.83 | 50.6133 |
13
- | publish per-message (ms, lower is better) — shared | 58.2441 | 33.8955 | -24.35 | 33.8955 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 5.2 | 5.7 | +0.50 | 5.2 |
15
- | publishBatch ×1000 (ms, lower is better) | 3.5 | 3.7 | +0.20 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 13 | 11 | -2.00 | 11 |
17
- | trace + publish ×1000 (ms, lower is better) | 4.6 | 4.7 | +0.10 | 4.6 |
18
- | first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
12
+ | publish per-message (ms, lower is better) — dedicated | 38.4709 | 36.5875 | -1.88 | 36.5875 |
13
+ | publish per-message (ms, lower is better) — shared | 33.9845 | 33.7984 | -0.19 | 33.7984 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 5.5 | 5.7 | +0.20 | 5.5 |
15
+ | publishBatch ×1000 (ms, lower is better) | 3.5 | 3.6 | +0.10 | 3.5 |
16
+ | dedup ×1000 (ms, lower is better) | 29.2 | 11 | -18.20 | 11 |
17
+ | trace + publish ×1000 (ms, lower is better) | 7 | 4.6 | -2.40 | 4.6 |
18
+ | first-packet cold dispatch (ms, lower is better) | 0.1 | 0 | -0.10 | 0 |
19
19
  <!-- BENCH-TREND:END -->
20
20
 
21
21
  Notes:
@@ -11,16 +11,16 @@ 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 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.
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:types`, `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`, `verify:types` 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` and `verify:types` resolve their baseline from the most recent release tag.
15
15
 
16
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
 
20
20
  1. Update `package.json`, `CHANGELOG.md`, and both roadmap files.
21
- 2. Run `pnpm check`, `pnpm lint`, `pnpm test:coverage`, `pnpm bench`, `pnpm test:e2e`, `pnpm bench:browser`, `pnpm verify:pack`, `pnpm verify:compat`, and `git diff --check`. (`verify:compat` asserts the package `exports` subpaths and type fields present in the `COMPAT_BASE_TAG` baseline still exist; `verify:pack` steam-imports the full root public surface plus every subpath in ESM and CJS from the packed tarball. `verify:compat` resolves its baseline from the most recent release tag, so a shallow or tag-less clone must first run `git fetch --tags` or it fails with "no version tag found".)
21
+ 2. Run `pnpm check`, `pnpm lint`, `pnpm test:coverage`, `pnpm bench`, `pnpm test:e2e`, `pnpm bench:browser`, `pnpm verify:pack`, `pnpm verify:compat`, `pnpm verify:types`, and `git diff --check`. (`verify:compat` asserts the package `exports` subpaths and type fields present in the `COMPAT_BASE_TAG` baseline still exist; `verify:pack` steam-imports the full root public surface plus every subpath in ESM and CJS from the packed tarball; `verify:types` freezes every public entry's declared export names against the same baseline and asserts no public signature names a type that no entry exports — a type-only export emits no runtime key, so neither the `Object.keys()` freeze in `tests/dual-format.test.ts` nor `verify:compat` can see one change. `verify:compat` and `verify:types` resolve their baseline from the most recent release tag, so a shallow or tag-less clone must first run `git fetch --tags` or they fail with "no version tag found".)
22
22
  3. Gate dependency security: `pnpm audit --registry=https://registry.npmjs.org` (the configured mirror registry lacks the audit endpoint; CI runs it on the public registry in the verify job). Fail the release on any known-vulnerability advisory; `pnpm-workspace.yaml` overrides pin patched ranges.
23
- 4. Gate browser benchmark regressions: run `pnpm bench:browser` (twice at minimum; the gate compares the newest report against the median of the same metric in up to the five preceding archived reports) and then `pnpm bench:compare --fail-above-pct 50`. The median baseline exists because the in-page hot-path metrics alternate between a fast and a slow mode on identical code — two consecutive runs of `dedup1000Ms` measured 12.7 ms then 25.6 ms, and a single noisy previous report could fail the documented ceiling with no code change at all. `publish/dedicated/perMessageMs` is noisy the same way: two consecutive runs of identical code measured 49.9 ms then 71.1 ms, failing the gate at +75.7% while `dedup1000Ms` and `traceAndPublish1000Ms` *improved* in the same report — that single-metric-up-everything-else-down shape is the noise signature, and the next run reported 49.4 ms and passed. Before accepting or acting on a failure, re-run and check the spread; if the change touches a hot path, size it directly (a micro-benchmark of the 0.20.95 bounded-map rewrites measured 80–86 ns/op across all three variants, i.e. inside run-to-run variance). A baseline shift (e.g. a metric becoming real instead of a no-op) is an expected one-time failure. Refresh the long-run trend doc with `pnpm bench:trend` and commit it when the tables change.
23
+ 4. Gate browser benchmark regressions: run `pnpm bench:browser` (twice at minimum; the gate compares the newest report against the median of the same metric in up to the five preceding archived reports, **and additionally requires the new value to exceed the highest of those samples**) and then `pnpm bench:compare --fail-above-pct 50`. The median baseline exists because the in-page hot-path metrics alternate between a fast and a slow mode on identical code — two consecutive runs of `dedup1000Ms` measured 12.7 ms then 25.6 ms, and a single noisy previous report could fail the documented ceiling with no code change at all. The second leg exists because a median over an alternating sample is only stable while the modes stay mixed: when several consecutive reports land in the same mode the median itself jumps, which is how one comment-only tree read +121% against one baseline and −56.7% against another minutes apart. A metric excused by that leg is printed under `within-baseline, not gated` rather than passing silently — measured on the current archive, `publish/dedicated/perMessageMs`, `publish/shared/perMessageMs` and `databus/publishBatch1000Ms` were all inside their own five-report range, so treat that list as the metric's real resolution, not as a clean bill. `publish/dedicated/perMessageMs` is noisy the same way: two consecutive runs of identical code measured 49.9 ms then 71.1 ms, failing the gate at +75.7% while `dedup1000Ms` and `traceAndPublish1000Ms` *improved* in the same report — that single-metric-up-everything-else-down shape is the noise signature, and the next run reported 49.4 ms and passed. Before accepting or acting on a failure, re-run and check the spread; if the change touches a hot path, size it directly (a micro-benchmark of the 0.20.95 bounded-map rewrites measured 80–86 ns/op across all three variants, i.e. inside run-to-run variance). A baseline shift (e.g. a metric becoming real instead of a no-op) is an expected one-time failure. Refresh the long-run trend doc with `pnpm bench:trend` and commit it when the tables change.
24
24
  5. Confirm the package contains only intended files with `npm pack --dry-run --json`.
25
25
  6. Commit on a feature branch, push that branch, and merge its green PR using squash or fast-forward (no merge commit). Fetch the merged commit, tag that exact commit, and push only the specific version tag; never push `main`/`master` directly or force-push. The workflow runs `node scripts/verify-release-version.mjs` to require `RELEASE_TAG` to equal `v` + the package version and to require exactly one non-empty CHANGELOG section.
26
26
 
package/docs/roadmap.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # Roadmap
2
2
 
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.
3
+ 0.21.13 was released on September 24, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.21.13 delivered scope
6
+
7
+ - The prose audit reached the last published surface it had not read: comments *inside function
8
+ bodies* in `src/**`. They ship because esbuild preserves them into `dist/**/*.js` and
9
+ `dist/cjs/*.cjs`, both listed in `package.json` `files`, so the 0.21.11/0.21.12 sweep (docs +
10
+ READMEs) and the declaration-JSDoc sweep had each left this half of the published text unread.
11
+ Thirty-three claims were corrected after each was re-read against the code it describes, and a
12
+ dozen more candidates were cleared on inspection and recorded with the reason.
13
+ - Two of those corrections change how a maintainer reads the code, not just its prose. A proof that
14
+ a chained `.catch` resolves rested on "`reportError` cannot throw", which the same file contradicts
15
+ 500 lines lower and a shipped test pins; and three comments credited themselves with preventing
16
+ something the adjacent code already handles — most consequentially `isAssigned`, whose real reason
17
+ for preferring the in-memory map is a `CONTROL/SUBSCRIBE` accepted with no durable route, the state
18
+ `tests/cluster.test.ts` already pins.
19
+ - Claims about a *dependency* now name it. Against `centrifuge@5.7.4` there is one client-level
20
+ credential hook and no `getChannelToken` in the package at all, so that request kind has never
21
+ fired outside a test fake; and `CentrifugeSession.unsubscribe()` removes the emitter's own no-op
22
+ `error` guard under a comment claiming internals were preserved. The second is recorded as an open
23
+ question with the interleaving it would take to call it a defect — the test double cannot exhibit
24
+ the guard, so no existing test can see it either way.
25
+ - `pnpm bench:compare --fail-above-pct 50` gained the leg its median baseline could not supply: a
26
+ value must now also exceed the highest sample in its own baseline window, because a median over a
27
+ bimodal metric is stable only while the modes stay mixed. What that excuses is printed rather than
28
+ passed silently, and the cost is written down beside it — a sustained shift inside the observed
29
+ range is now invisible to the gate. Both legs were attributed by mutation: removing the new one
30
+ fails exactly one test, suppressing everything fails four including the pre-existing pair path.
31
+ - No behavior, wire-format, storage-layout or cluster-protocol change. The only executable edit in
32
+ the release is inside `scripts/`, which the package does not publish; coverage stays at
33
+ 99.01 / 96.90 / 99.26 / 99.69 over **897** tests.
4
34
 
5
35
  ## 0.21.12 delivered scope
6
36
 
@@ -26,8 +26,9 @@ interface DataBusTransport<TConfig = unknown, TData = unknown> {
26
26
  publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void>;
27
27
  /** Optional: one frame for a burst. Without it the DataBus loops `publish`.
28
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.) */
29
+ * takes, per entry. That entry type is exported as `DataBusPublicationItem`;
30
+ * name it, write it inline, or widen the `ReadonlyArray` to whatever your
31
+ * backend accepts. */
31
32
  publishBatch?(topic: string, items: ReadonlyArray<{ data: unknown; messageId?: string; timestamp?: number }>): MaybePromise<void>;
32
33
  stop(): MaybePromise<void>;
33
34
  }
package/docs/zh/api.md CHANGED
@@ -296,7 +296,7 @@ trace: {
296
296
  }
297
297
  ```
298
298
 
299
- 低频事件类型包括 `lifecycle`、`status`、`subscription`、`coordination`、`reliability` 和 `error`;高频数据按窗口输出 `message_metrics`,包含接收/分发计数、活跃 Topic 数量和分发延迟聚合(`dispatchSamples`、`dispatchAvgMs`、`dispatchP50Ms`、`dispatchP95Ms`、`dispatchMaxMs`),以及去重结果(`dedupAccepted`、`dedupSuppressed`)。路由归属变化通过 `reliability` 事件呈现:优雅交接上报 `operation: 'route_migration'`,而恢复悬挂未确认交接的重选上报 `operation: 'route_migration_recovery'`(前任 owner 已消失且 ACK 始终未到达),便于 trace 消费者区分恢复与常规交接。`coordination` 事件在每次 transport 打开后发出,携带 `coordinated`、`activeWorkers`、`workers`(格式化后的 worker 记录)与 `routes`(`topicKey@workerId|confirmed=…`,反映已收敛的路由列表)。所有公开事件都使用固定结构,其中不会携带消息 payload、连接地址或错误正文。Topic 明文只出现在两种事件里:`subscription` 记录订阅变更,`reliability` 则为涉及具体路由的操作标注该路由(`route_ack`、`route_migration`、`route_migration_recovery`,其余 reliability 操作不带 Topic);其它事件不含 Topic,`coordination` 使用不可逆的 `topicKey` 而非名称来标识路由。既然这两种事件确实携带 Topic 名称,就应把 trace sink 视为诊断出口:在把事件转发给外部监控前,按业务 Topic 约定脱敏,详见 `configuration.md`。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,便于定位诊断配置问题。sink 应尽量避免抛出异常——预期中的错误条件应通过事件数据表达,而不是通过异常上报。
299
+ 低频事件类型包括 `lifecycle`、`status`、`subscription`、`coordination`、`reliability` 和 `error`;高频数据按窗口输出 `message_metrics`,包含接收/分发计数、活跃 Topic 数量和分发延迟聚合(`dispatchSamples`、`dispatchAvgMs`、`dispatchP50Ms`、`dispatchP95Ms`、`dispatchMaxMs`),以及去重结果(`dedupAccepted`、`dedupSuppressed`)。路由归属变化通过 `reliability` 事件呈现:优雅交接上报 `operation: 'route_migration'`,而恢复悬挂未确认交接的重选上报 `operation: 'route_migration_recovery'`(前任 owner 已消失且 ACK 始终未到达),便于 trace 消费者区分恢复与常规交接。`coordination` 事件在每次成功 `start()` 后发出一次(自动或按需的 reopen 不会再发出该快照),携带 `coordinated`、`activeWorkers`、`workers`(格式化后的 worker 记录)与 `routes`(`topicKey@workerId|confirmed=…`,反映已收敛的路由列表)。所有公开事件都使用固定结构,其中不会携带消息 payload、连接地址或错误正文。Topic 明文只出现在两种事件里:`subscription` 记录订阅变更,`reliability` 则为涉及具体路由的操作标注该路由(`route_ack`、`route_migration`、`route_migration_recovery`,其余 reliability 操作不带 Topic);其它事件不含 Topic,`coordination` 使用不可逆的 `topicKey` 而非名称来标识路由。既然这两种事件确实携带 Topic 名称,就应把 trace sink 视为诊断出口:在把事件转发给外部监控前,按业务 Topic 约定脱敏,详见 `configuration.md`。sink 抛错会被隔离,不会中断消息分发,但会向 `console.warn` 输出错误,便于定位诊断配置问题。sink 应尽量避免抛出异常——预期中的错误条件应通过事件数据表达,而不是通过异常上报。
300
300
 
301
301
  **`asyncSink: true` 的投递语义。** 默认(`false`)下,每条事件同步调用 sink。开启 `asyncSink: true` 后,事件先入内存队列,在**一个微任务批次**中统一投递:任务的第一条事件调度 `queueMicrotask`,在该微任务运行前产生的所有事件(含周期性 `message_metrics` 快照)按 FIFO 顺序一次性送往 sink。这样热路径永远不会因 sink 工作而阻塞。错误隔离与同步模式一致——sink 抛错被捕获并记入 `console.warn`,既不会中断分发,也不会中断批次内其余事件。顺序保证边界:**批次内**顺序是确定的,但投递被推迟到下一个微任务,因此事件不再保证在你的下一行语句执行前可见;批次 flush 之后新产生的事件会落入后续批次。当需要"每条事件在下一行代码前可见"时,请使用默认的同步 sink(或自行合并计数)。
302
302
 
@@ -316,7 +316,8 @@ interface DataBusTransport<TConfig, TData> {
316
316
  subscribe(topic): void | Promise<void>;
317
317
  unsubscribe(topic): void | Promise<void>;
318
318
  publish(topic, data): void | Promise<void>;
319
- /** 可选:将多条消息合并为一帧发送。未提供时 DataBus 回退为逐条 `publish`。 */
319
+ /** 可选:将多条消息合并为一帧发送。未提供时 DataBus 回退为逐条 `publish`。
320
+ * 每条是 `DataBusPublicationItem` —— `{ data, messageId?, timestamp? }`,已导出。 */
320
321
  publishBatch?(topic, items): void | Promise<void>;
321
322
  stop(): void | Promise<void>;
322
323
  }
@@ -375,7 +376,7 @@ createStorageEventChannel(options: {
375
376
  }): ClusterChannel | null
376
377
  ```
377
378
 
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
+ 创建以 localStorage `storage` 事件为载体的 `ClusterChannel`——面向无 BroadcastChannel 环境的协调降级通道。storage 或 storage-event 来源缺失时返回 `null`。在跨 Tab 协调需要的维度上,投递语义跟随 BroadcastChannel(不回显给发送方、消息可 JSON 序列化、关闭后拒绝再写入),但有一处刻意差异:`postMessage` 同样不会派发给通道自己的监听器,所以**同一文档**内创建的两个通道互相看不到,而真正的 BroadcastChannel 是可以的。这个降级通道只关心跨 Tab 投递,该差异由 `tests/storage-channel.test.ts` 的 sibling-channel 用固定住;载荷信封里同时带一个**每通道 sender nonce** 和一个单调序列号,正是这两者共同保证连续相同的消息仍可投递——包括两个 Tab 各自的第一帧都会写入相同的 `seq=1`、否则会被静默抑制的情况。通过 `createBrowserEnvironment({ channelFallback: 'storage-event' })` 启用;安全权衡见 [configuration.md](./configuration.md#协调通道降级broadcastchannel-不可用)。
379
380
 
380
381
  ## WebSocket 传输后端
381
382
 
@@ -501,7 +502,7 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
501
502
  - `shared` / `auto`:SharedWorker → Dedicated Worker → 本地模式
502
503
  - `dedicated`(默认):Dedicated Worker → SharedWorker → 本地模式
503
504
 
504
- `availability` 可显式传入 `worker` / `sharedWorker` 能力标记,用于 SSR、测试或嵌入环境,避免访问不存在的全局对象。
505
+ `availability`(`WorkerAvailability`,已导出)可显式传入 `worker` / `sharedWorker` 能力标记,用于 SSR、测试或嵌入环境,避免访问不存在的全局对象。
505
506
 
506
507
  ### `effectiveWorkerLoad(worker, options?)`
507
508
 
@@ -511,7 +512,7 @@ useVueCrossTabSubscription(bus, 'chat.*', message => console.log(message.data));
511
512
 
512
513
  ### `approximatePayloadBytes(payload)`
513
514
 
514
- 低成本、零分配的 payload 线长估算,用于自适应负载采样的字节侧。仅在启用自适应路由时运行,因此近似值足够——目标是跨 Worker 的稳定比较,而非精确字节数。
515
+ 低成本估算 payload 的线长,用于自适应负载采样的字节侧,也用于给保留的 replay 缓冲估算体积(`getDiagnostics().replay` 的 `bytes` 按需计算)——因此它并不只在启用自适应路由时运行。它也不是零分配的:对象节点会物化一份 `Object.values`。两种场景下近似值都够用,目标是跨 Worker 的稳定比较,而非精确字节数。
515
516
 
516
517
  尺寸规则:`null`/`undefined` 与 symbol/function 为 `0`,boolean 为 `4`,number 与 bigint 为 `8`,字符串为长度,`ArrayBuffer` 与 TypedArray 视图为 `byteLength`,数组为 8 字节头部加各元素之和,普通对象为各值之和。
517
518
 
@@ -2,20 +2,20 @@
2
2
 
3
3
  # 浏览器基准趋势
4
4
 
5
- > 数据截至 2026-09-23,基于 71 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
5
+ > 数据截至 2026-09-23,基于 87 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
6
6
 
7
- 发布门禁的对比基线是 `pnpm bench:compare --fail-above-pct 50`:将最新报告与其之前若干报告中同一指标的中位数比较(50% 上限用于吸收共享 runner 的噪声;此前只看"上一份报告"并不稳定,因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳)。本文记录长期趋势:数值为逐指标延迟,越低越好;最后一列是最近 5 份报告内的最优值,而不是历史纪录——页内基准矩阵在 2026 年 9 月初变更过测量语义,更早的报告不可比。
7
+ 发布门禁的对比基线是 `pnpm bench:compare --fail-above-pct 50`:将最新报告与其之前若干报告中同一指标的中位数比较,*并且*要求新值超过这些样本中的最大值(50% 上限用于吸收共享 runner 的噪声;此前只看"上一份报告"并不稳定,因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳;而在两档交替的样本上,中位数只有在这两档混合分布时才稳定,因此单看百分比仍可能纯粹因为档位切换而失败)。被第二个条件放过的指标会显式列在 “within-baseline, not gated” 里,而不是悄悄算作通过。本文记录长期趋势:数值为逐指标延迟,越低越好;最后一列是最近 5 份报告内的最优值,而不是历史纪录——页内基准矩阵在 2026 年 9 月初变更过测量语义,更早的报告不可比。
8
8
 
9
9
  <!-- BENCH-TREND:BEGIN (machine-generated table) -->
10
10
  | 指标 | 上次 (ms) | 本次 (ms) | Δ | 近 5 次最优 (ms) |
11
11
  |---|---|---|---|---|
12
- | publish per-message (ms, lower is better) — dedicated | 55.4416 | 50.6133 | -4.83 | 50.6133 |
13
- | publish per-message (ms, lower is better) — shared | 58.2441 | 33.8955 | -24.35 | 33.8955 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 5.2 | 5.7 | +0.50 | 5.2 |
15
- | publishBatch ×1000 (ms, lower is better) | 3.5 | 3.7 | +0.20 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 13 | 11 | -2.00 | 11 |
17
- | trace + publish ×1000 (ms, lower is better) | 4.6 | 4.7 | +0.10 | 4.6 |
18
- | first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
12
+ | publish per-message (ms, lower is better) — dedicated | 38.4709 | 36.5875 | -1.88 | 36.5875 |
13
+ | publish per-message (ms, lower is better) — shared | 33.9845 | 33.7984 | -0.19 | 33.7984 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 5.5 | 5.7 | +0.20 | 5.5 |
15
+ | publishBatch ×1000 (ms, lower is better) | 3.5 | 3.6 | +0.10 | 3.5 |
16
+ | dedup ×1000 (ms, lower is better) | 29.2 | 11 | -18.20 | 11 |
17
+ | trace + publish ×1000 (ms, lower is better) | 7 | 4.6 | -2.40 | 4.6 |
18
+ | first-packet cold dispatch (ms, lower is better) | 0.1 | 0 | -0.10 | 0 |
19
19
  <!-- BENCH-TREND:END -->
20
20
 
21
21
  说明:
@@ -11,16 +11,16 @@
11
11
 
12
12
  ## 自动化门禁(CI)
13
13
 
14
- `CI` 工作流的 `verify` job 在**以 `main` 为目标分支**的 push 与 pull request 上运行 `pnpm check`、`pnpm lint`、`pnpm test:coverage`(`vitest.config.ts` 中的下限:语句 98% / 分支 96% / 函数 98% / 行 99%)、`pnpm verify:compat`、`pnpm verify:pack`、`pnpm bench` 与 `pnpm audit`——`ci.yml` 里两个触发器都被过滤为 `branches: [main]`,因此推送 topic 分支什么都不会跑,这项工作要到开出 PR 时才第一次进入 CI(base 不是 main 的 PR 同样不在覆盖范围内);`browser` job 运行 Playwright E2E 套件。`Release` 工作流在发布前重跑 `verify:compat` 与 `verify:pack`,随后执行阻塞式 `verify:published` 门禁。两个工作流的 checkout 均使用 `fetch-depth: 0` + `fetch-tags: true`,因为 `verify:compat` 需要从最近的发布 tag 解析基线。
14
+ `CI` 工作流的 `verify` job 在**以 `main` 为目标分支**的 push 与 pull request 上运行 `pnpm check`、`pnpm lint`、`pnpm test:coverage`(`vitest.config.ts` 中的下限:语句 98% / 分支 96% / 函数 98% / 行 99%)、`pnpm verify:compat`、`pnpm verify:types`、`pnpm verify:pack`、`pnpm bench` 与 `pnpm audit`——`ci.yml` 里两个触发器都被过滤为 `branches: [main]`,因此推送 topic 分支什么都不会跑,这项工作要到开出 PR 时才第一次进入 CI(base 不是 main 的 PR 同样不在覆盖范围内);`browser` job 运行 Playwright E2E 套件。`Release` 工作流在发布前重跑 `verify:compat`、`verify:types` 与 `verify:pack`,随后执行阻塞式 `verify:published` 门禁。两个工作流的 checkout 均使用 `fetch-depth: 0` + `fetch-tags: true`,因为 `verify:compat` 与 `verify:types` 需要从最近的发布 tag 解析基线。
15
15
 
16
16
  只有 `pnpm bench:browser` / `pnpm bench:compare` / `pnpm bench:trend` 保持仅本地执行:共享 runner 的计时噪声会让数值型 CI 门禁不可靠,而 `bench:trend` 会依据本地归档重写 `docs/benchmarks.md`,这本就不该由 CI 来做。
17
17
 
18
18
  ## 打 tag 前
19
19
 
20
20
  1. 更新 `package.json`、`CHANGELOG.md` 和中英文 roadmap。
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" 失败)。
21
+ 2. 运行 `pnpm check`、`pnpm lint`、`pnpm test:coverage`、`pnpm bench`、`pnpm test:e2e`、`pnpm bench:browser`、`pnpm verify:pack`、`pnpm verify:compat`、`pnpm verify:types` 以及 `git diff --check`(`verify:compat` 断言 `COMPAT_BASE_TAG` 基线中的 package `exports` 子路径与类型字段仍然存在;`verify:pack` 从打包产物冒烟导入完整根公共面与全部子路径的 ESM/CJS;`verify:types` 用同一基线冻结每个公共入口声明层导出的名字,并断言没有任何公共签名引用了「任何入口都未导出」的类型——纯类型导出不会在运行时产生任何键,因此 `tests/dual-format.test.ts` 的 `Object.keys()` 冻结和 `verify:compat` 都看不到它的变化。`verify:compat` 与 `verify:types` 从最近的发布 tag 解析基线,因此浅克隆或缺少 tag 的克隆需先执行 `git fetch --tags`,否则会以 "no version tag found" 失败)。
22
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`。采用中位数基线是因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳——`dedup1000Ms` 相邻两次实测分别为 12.7 ms 与 25.6 ms,只看"上一份报告"时,一次偶发噪声就能在无代码改动的情况下击穿既定上限。`publish/dedicated/perMessageMs` 同样如此:同一份代码相邻两次实测为 49.9 ms 与 71.1 ms,令门禁以 +75.7% 失败,而同一份报告里 `dedup1000Ms` 与 `traceAndPublish1000Ms` 反而*变快*——"单指标上涨、其余下降"正是噪声的特征,第三次实测为 49.4 ms 并通过。因此在采信或处理失败前先复跑并观察离散度;若改动涉及热路径,就直接量化其开销(0.20.95 有界映射改写的微基准显示三种写法均为 80–86 ns/op,落在运行间波动之内)。基线迁移(例如某指标从空操作变为真实路径)属预期内的一次性失败。用 `pnpm bench:trend` 刷新长期趋势文档,表格变化时一并提交。
23
+ 4. 浏览器基准回归门禁:运行 `pnpm bench:browser`(至少两次;门禁将最新报告与之前最多五份归档报告中同一指标的中位数比较,**并且额外要求新值超过这些样本中的最大值**),随后执行 `pnpm bench:compare --fail-above-pct 50`。采用中位数基线是因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳——`dedup1000Ms` 相邻两次实测分别为 12.7 ms 与 25.6 ms,只看"上一份报告"时,一次偶发噪声就能在无代码改动的情况下击穿既定上限。第二个条件则是因为:在两档交替的样本上,中位数只有当两档保持混合时才稳定——一旦连续几份报告落在同一档,中位数本身就会跳变,这正是同一份纯注释改动在一版基线前读出 +121%、几分钟后在另一版基线前读出 −56.7% 的原因。被该条件放过的指标会打印在 `within-baseline, not gated` 下,而不是静默通过——在当前归档上实测,`publish/dedicated/perMessageMs`、`publish/shared/perMessageMs` 与 `databus/publishBatch1000Ms` 三项都落在各自五份报告的区间内,因此应把这份清单读作该指标的真实分辨率,而不是一张合格证。`publish/dedicated/perMessageMs` 同样如此:同一份代码相邻两次实测为 49.9 ms 与 71.1 ms,令门禁以 +75.7% 失败,而同一份报告里 `dedup1000Ms` 与 `traceAndPublish1000Ms` 反而*变快*——"单指标上涨、其余下降"正是噪声的特征,第三次实测为 49.4 ms 并通过。因此在采信或处理失败前先复跑并观察离散度;若改动涉及热路径,就直接量化其开销(0.20.95 有界映射改写的微基准显示三种写法均为 80–86 ns/op,落在运行间波动之内)。基线迁移(例如某指标从空操作变为真实路径)属预期内的一次性失败。用 `pnpm bench:trend` 刷新长期趋势文档,表格变化时一并提交。
24
24
  5. 用 `npm pack --dry-run --json` 确认发布包只包含预期文件。
25
25
  6. 在功能分支提交并推送该分支,PR 验证通过后使用 squash 或 fast-forward 合入(不创建 merge commit)。获取合入后的精确提交并打 tag,只推送该版本 tag;禁止直接推送 `main`/`master` 或 force-push。工作流运行 `node scripts/verify-release-version.mjs`,要求 `RELEASE_TAG` 等于 `v` 加 package 版本,且 CHANGELOG 中恰好有一个非空的对应版本章节。
26
26
 
@@ -1,6 +1,27 @@
1
1
  # 路线图
2
2
 
3
- 0.21.12 已于 2026 年 9 月 23 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
3
+ 0.21.13 已于 2026 年 9 月 24 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
4
+
5
+ ## 0.21.13 已完成范围
6
+
7
+ - 文案审计读完了它此前唯一没覆盖的公开文本:`src/**` 中**函数体内部**的注释。它们会随包分发,因为
8
+ esbuild 会将其保留进 `dist/**/*.js` 与 `dist/cjs/*.cjs`,而这两者都在 `package.json` 的 `files`
9
+ 列表里;0.21.11/0.21.12(docs + README)与声明层 JSDoc 的那一轮各自都漏掉了这半边文本。共修正 33 条
10
+ 说法——每一条都是先重读它所描述的代码再动手——另有十余条候选经核查后**保留原样**,并连同保留理由一并记录。
11
+ - 其中两条改变的是维护者如何读代码,而不只是读注释。一处"链式 `.catch` 必然 resolve"的证明,依赖的前提是
12
+ "`reportError` 不会抛错",而同一份文件在 500 行之后自相矛盾,且已有测试钉住那个反例;另有三处注释把自己
13
+ 的作用写成"阻止某件邻近代码本就已处理的事"——其中影响最大的是 `isAssigned`:它优先使用内存映射的真正原因,
14
+ 是一条没有持久路由就被接受的 `CONTROL/SUBSCRIBE`,而这个状态正是 `tests/cluster.test.ts` 已经钉住的。
15
+ - 关于*依赖*的说法现在会点出依赖本身。针对 `centrifuge@5.7.4`:客户端级的凭据钩子只有一个,包里根本不存在
16
+ `getChannelToken`,因此那类请求从未在测试替身之外触发过;而 `CentrifugeSession.unsubscribe()` 会移除
17
+ emitter 自己的 no-op `error` 监听,注释却声称"内部监听被保留"。第二项被记为一个**尚未定论的问题**,并写明
18
+ 要构造出怎样的交错才能称其为缺陷——测试替身无法呈现该保护,因此现有测试无论正反都看不见它。
19
+ - `pnpm bench:compare --fail-above-pct 50` 补上了它的中位数基线无法提供的那条判据:新值还必须超过自身基线窗口
20
+ 中的最大样本,因为在中双峰指标上,中位数只有在两档保持混合时才稳定。被这条判据放过的指标会被显式打印出来而不是
21
+ 静默通过,其代价也写在旁边——停留在已观测区间内的持续性劣化从此对门禁不可见。两条判据都由变异测试归属:删掉新增
22
+ 那条只让 1 个测试失败,而让它恒为"放过"则让 4 个失败(包含既有的双报告路径)。
23
+ - 没有行为、线格式、存储布局或集群协议变更。本次发布中唯一的可执行改动位于 `scripts/`,而该目录并不随包分发;
24
+ 覆盖率维持在 99.01 / 96.90 / 99.26 / 99.69,共 **897** 个测试。
4
25
 
5
26
  ## 0.21.12 已完成范围
6
27
 
@@ -25,7 +25,8 @@ interface DataBusTransport<TConfig = unknown, TData = unknown> {
25
25
  publish(topic: string, data: unknown, options?: DataBusPublishOptions): MaybePromise<void>;
26
26
  /** 可选:突发时用一帧发多条。没有它,DataBus 会退化成逐条 `publish`。
27
27
  * 每条是 `{ data, messageId?, timestamp? }`,即 `publish` 那份元数据的逐条版本。
28
- * (这个条目类型是内部的:要么按内联结构写,要么按你的后端放宽 `ReadonlyArray`。) */
28
+ * 这个条目类型已作为 `DataBusPublicationItem` 导出:可以直接引用它、按内联结构写,
29
+ * 或按你的后端放宽 `ReadonlyArray`。 */
29
30
  publishBatch?(topic: string, items: ReadonlyArray<{ data: unknown; messageId?: string; timestamp?: number }>): MaybePromise<void>;
30
31
  stop(): MaybePromise<void>;
31
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-tab-worker-databus",
3
- "version": "0.21.12",
3
+ "version": "0.21.13",
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",
@@ -103,6 +103,7 @@
103
103
  "bench:trend": "node scripts/bench-trend.mjs",
104
104
  "verify:pack": "node scripts/verify-packed-consumer.mjs",
105
105
  "verify:compat": "node scripts/verify-version-compat.mjs",
106
+ "verify:types": "node scripts/verify-public-types.mjs",
106
107
  "verify:published": "node scripts/verify-published-consumer.mjs",
107
108
  "test:e2e": "pnpm build && pnpm build:examples && playwright test",
108
109
  "typecheck": "tsc --noEmit",
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/utils/constants.ts"],
4
- "sourcesContent": ["/**\n * \u516C\u5171\u5B57\u7B26\u4E32\u5E38\u91CF \u2014\u2014 \u751F\u547D\u5468\u671F\u72B6\u6001\u3001\u89D2\u8272\u3001\u63A7\u5236\u52A8\u4F5C\u3001\u534F\u8BAE\u6D88\u606F\u7C7B\u578B\u3001\u679A\u4E3E\u4E0E\n * \u547D\u540D\u7A7A\u95F4\u524D\u7F00\u3002\n *\n * \u6240\u6709\u8FD0\u884C\u65F6\u4F7F\u7528\u7684\u5B57\u7B26\u4E32\u5B57\u9762\u91CF\u96C6\u4E2D\u5728\u6B64\u4E00\u4EFD\u503C\uFF0C\u7C7B\u578B\u5B9A\u4E49\uFF08src/core/types.ts\n * \u7B49\uFF09\u901A\u8FC7 `(typeof X)[keyof typeof X]` \u4ECE\u8FD9\u4E9B\u5E38\u91CF\u6D3E\u751F\uFF0C\u6BD4\u8F83/switch \u5904\u5F15\u7528\n * \u540C\u4E00\u5BF9\u8C61\u6210\u5458\u3002\u8FD9\u6837\u65E2\u80FD\u6D88\u9664\u6563\u843D\u5728\u5404\u6587\u4EF6\u91CC\u7684\u91CD\u590D\u5B57\u7B26\u4E32\uFF08\u907F\u514D\u5927\u5C0F\u5199\u4E0D\u7EDF\u4E00\n * \u6216\u7B14\u8BEF\uFF09\uFF0C\u53C8\u4FDD\u8BC1\u7C7B\u578B\u4E0E\u503C\u6C38\u4E0D\u9519\u4F4D\u3002\n *\n * ## \u4F7F\u7528\u7EA6\u5B9A\n *\n * - **\u65B0\u589E\u6216\u4FEE\u6539\u5B57\u7B26\u4E32\u5B57\u9762\u91CF\u65F6\uFF0C\u5148\u5230\u8FD9\u91CC\u67E5\u627E/\u589E\u8865\uFF0C\u4E0D\u8981\u5728\u4E1A\u52A1\u6587\u4EF6\u91CC\u76F4\u63A5\u5199\n * \u9B54\u6CD5\u5B57\u7B26\u4E32\u3002** \u626B\u63CF\u6B8B\u7559\u5B57\u9762\u91CF\uFF1A`rg \"'(['a-z]+)'\" src/`\u3002\n * - **\u53EA\u7528\u4E8E\u7C7B\u578B\u4F4D\u7F6E\u7684\u5E38\u91CF**\uFF08\u5982 `PERSISTENCE_OPERATION`\uFF09\u5728\u8C03\u7528\u65B9\u7528\n * `import type`\uFF1B**\u5728\u8FD0\u884C\u65F6\u6BD4\u8F83/\u6784\u9020\u4E2D\u4F7F\u7528\u7684**\uFF08\u5982 `WORKER_STATUS.ERROR`\u3001\n * `TRACE_EVENT_TYPE.LIFECYCLE`\uFF09\u5FC5\u987B\u7528\u503C\u5BFC\u5165\uFF0C\u5426\u5219\u4F1A\u62A5 \"cannot be used as\n * a value because it was imported using 'import type'\"\u3002\n * - **\u503C\u6D3E\u751F\u7C7B\u578B**\uFF1A`export type WorkerStatus = (typeof WORKER_STATUS)[keyof\n * typeof WORKER_STATUS]`\u3002\u8FD9\u6837\u65B0\u589E\u679A\u4E3E\u5206\u652F\u65F6\u7C7B\u578B\u81EA\u52A8\u6536\u7A84\uFF0C\u7F16\u8BD1\u5668\u4F1A\u6307\u51FA\n * \u6BCF\u4E2A\u9057\u6F0F\u7684 switch \u5206\u652F\u3002\n * - \u5206\u7EC4\u7684\u987A\u5E8F\uFF08\u72B6\u6001 / \u89D2\u8272 / \u540E\u7AEF / \u6D88\u606F\u7C7B\u578B / \u679A\u4E3E / \u524D\u7F00\uFF09\u4FDD\u6301\u7A33\u5B9A\uFF0C\u65B9\u4FBF\n * \u7EF4\u62A4\u8005\u4E00\u773C\u5B9A\u4F4D\u3002\n */\n\n// ---- \u751F\u547D\u5468\u671F\u72B6\u6001 / \u89D2\u8272 / \u53EF\u89C1\u6027 ----\nexport const WORKER_STATUS = {\n CONNECTING: 'connecting',\n CONNECTED: 'connected',\n DISCONNECTED: 'disconnected',\n ERROR: 'error',\n} as const;\n\nexport const WORKER_ROLE = {\n ACTIVE: 'active',\n STANDBY: 'standby',\n} as const;\n\nexport const TAB_VISIBILITY = {\n VISIBLE: 'visible',\n HIDDEN: 'hidden',\n} as const;\n\n// ---- Worker \u540E\u7AEF\u6A21\u5F0F\u4E0E\u89E3\u6790\u7ED3\u679C\uFF08WorkerMode / WorkerBackend\uFF09----\nexport const WORKER_MODE = {\n DEDICATED: 'dedicated',\n SHARED: 'shared',\n AUTO: 'auto',\n} as const;\n\nexport const WORKER_BACKEND = {\n DEDICATED: 'dedicated',\n SHARED: 'shared',\n LOCAL: 'local',\n} as const;\n\n// ---- \u5B58\u50A8\u4E8B\u4EF6\u901A\u9053\u56DE\u9000 ----\nexport const CHANNEL_FALLBACK = {\n NONE: 'none',\n STORAGE_EVENT: 'storage-event',\n} as const;\n\n// ---- Worker / MessagePort / BroadcastChannel \u7684 EventTarget \u4E8B\u4EF6\u540D\u5224\u522B ----\n// MessageEvent.type\uFF08'message' / 'messageerror'\uFF09\u4E0E Worker \u4E0A\u7684 'error' \u4E8B\u4EF6\u540D\uFF0C\n// \u7528\u4E8E\u73AF\u5883\u9002\u914D\u5668\u63A5\u53E3\u7B7E\u540D\u4E0E\u6D4B\u8BD5\u66FF\u8EAB\u7684 addEventListener \u771F\u5047\u5206\u652F\u3002\nexport const EVENT_TYPE = {\n MESSAGE: 'message',\n MESSAGEERROR: 'messageerror',\n ERROR: 'error',\n} as const;\n\n// ---- \u63A7\u5236\u5E73\u9762\u52A8\u4F5C\uFF08WorkerControlAction\uFF09----\nexport const CONTROL_ACTION = {\n SUBSCRIBE: 'SUBSCRIBE',\n UNSUBSCRIBE: 'UNSUBSCRIBE',\n PUBLISH: 'PUBLISH',\n} as const;\n\n// ---- BroadcastChannel \u96C6\u7FA4\u6D88\u606F\u7C7B\u578B ----\nexport const CLUSTER_MESSAGE_TYPE = {\n CONTROL: 'CONTROL',\n EVENT: 'EVENT',\n REGISTRY: 'REGISTRY',\n ROUTE_RELEASED: 'ROUTE_RELEASED',\n} as const;\n\n// ---- \u547D\u540D\u7A7A\u95F4\u524D\u7F00\u4E0E\u4E8B\u4EF6\u540D ----\nexport const DEFAULT_STORAGE_PREFIX = 'cross-tab-worker-databus';\nexport const STORAGE_CHANNEL_PREFIX = `${DEFAULT_STORAGE_PREFIX}:channel:`;\nexport const TAB_ID_STORAGE_KEY = `${DEFAULT_STORAGE_PREFIX}:tab-id`;\nexport const PUBLICATION_EVENT = 'DATABUS_PUBLICATION';\n\n// ---- \u89C4\u8303 JSON \u4FE1\u5C01\u64CD\u4F5C\u7801 ----\nexport const PUBLICATION_ENVELOPE_OP = 'publication';\n\n// ---- trace \u4E8B\u4EF6\u7C7B\u578B ----\nexport const TRACE_EVENT_TYPE = {\n LIFECYCLE: 'lifecycle',\n STATUS: 'status',\n SUBSCRIPTION: 'subscription',\n COORDINATION: 'coordination',\n ERROR: 'error',\n RELIABILITY: 'reliability',\n MESSAGE_METRICS: 'message_metrics',\n} as const;\n\n// ---- trace \u751F\u547D\u5468\u671F\u52A8\u4F5C ----\nexport const TRACE_LIFECYCLE_ACTION = {\n START: 'start',\n STOP: 'stop',\n SUSPEND: 'suspend',\n RESUME: 'resume',\n} as const;\n\n// ---- \u5185\u90E8 handler \u8C03\u7528\u6807\u7B7E\uFF08\u9694\u79BB\u5F02\u5E38\u65F6\u7684\u5206\u7C7B\u6765\u6E90\uFF09----\nexport const INVOKE_LABEL = {\n DISPATCH: 'dispatch',\n STATUS: 'status',\n ERROR_HANDLER: 'error handler',\n} as const;\n\n// ---- trace \u6A21\u5F0F\uFF08DataBusTraceMode\uFF09----\nexport const TRACE_MODE = {\n EVENTS: 'events',\n METRICS: 'metrics',\n ALL: 'all',\n} as const;\n\n// ---- trace \u9519\u8BEF\u6765\u6E90 ----\nexport const TRACE_ERROR_SOURCE = {\n TRANSPORT: 'transport',\n OPERATION: 'operation',\n} as const;\n\n// ---- reliability \u8BCA\u65AD\u64CD\u4F5C ----\nexport const RELIABILITY_OPERATION = {\n TRANSPORT_RECOVERY: 'transport_recovery',\n ROUTE_ACK: 'route_ack',\n ROUTE_MIGRATION: 'route_migration',\n // A re-election that recovered a stranded unconfirmed handoff (previous\n // owner gone, ACK never arrived). Distinct from ROUTE_MIGRATION so trace\n // consumers can tell recoveries apart from routine graceful handoffs.\n ROUTE_MIGRATION_RECOVERY: 'route_migration_recovery',\n PERSISTENCE_CLEANUP: 'persistence_cleanup',\n PERSISTENCE_RETRY: 'persistence_retry',\n DEDUP_SUPPRESSED: 'dedup_suppressed',\n} as const;\n\n// ---- \u81EA\u52A8\u6062\u590D\u5C1D\u8BD5\u7ED3\u679C ----\nexport const RECOVERY_OUTCOME = {\n SCHEDULED: 'scheduled',\n SUCCEEDED: 'succeeded',\n FAILED: 'failed',\n EXHAUSTED: 'exhausted',\n} as const;\n\n// ---- \u8BA2\u9605\u52A8\u4F5C ----\nexport const SUBSCRIPTION_ACTION = {\n SUBSCRIBE: 'subscribe',\n UNSUBSCRIBE: 'unsubscribe',\n} as const;\n\n// ---- \u56DE\u653E\u4FEE\u526A\u7B56\u7565 ----\nexport const PRUNE_STRATEGY = {\n COUNT: 'count',\n AGE: 'age',\n BOTH: 'both',\n} as const;\n\n// ---- \u6301\u4E45\u5316\u64CD\u4F5C ----\nexport const PERSISTENCE_OPERATION = {\n LOAD: 'load',\n APPEND: 'append',\n CLEAR: 'clear',\n CLEAR_TOPIC: 'clearTopic',\n CLEAR_BEFORE: 'clearBefore',\n} as const;\n\n// ---- \u6545\u969C\u6765\u6E90\uFF08DataBusFailureSource\uFF09----\nexport const FAILURE_SOURCE = {\n TRANSPORT: 'transport',\n PERSISTENCE: 'persistence',\n DISPATCH: 'dispatch',\n} as const;\n\n// ---- \u5065\u5EB7\u5224\u5B9A\u72B6\u6001\uFF08DataBusHealthSummary['state']\uFF09----\nexport const HEALTH_STATE = {\n STOPPED: 'stopped',\n STARTING: 'starting',\n HEALTHY: 'healthy',\n RECOVERING: 'recovering',\n SUSPENDED: 'suspended',\n DEGRADED: 'degraded',\n} as const;\n\n// ---- WebSocket wire \u534F\u8BAE\u64CD\u4F5C\u7801 ----\nexport const WS_OP = {\n SUBSCRIBE: 'subscribe',\n UNSUBSCRIBE: 'unsubscribe',\n PUBLISH: 'publish',\n PUBLISH_BATCH: 'publishBatch',\n} as const;\n\n// ---- Centrifuge Worker \u8F93\u5165\u6D88\u606F\u7C7B\u578B ----\nexport const CENTRIFUGE_INPUT_TYPE = {\n INIT: 'INIT',\n SUBSCRIBE: 'SUBSCRIBE',\n UNSUBSCRIBE: 'UNSUBSCRIBE',\n PUBLISH: 'PUBLISH',\n PUBLISH_BIN: 'PUBLISH_BIN',\n PING: 'PING',\n STOP: 'STOP',\n TOKEN_RESPONSE: 'TOKEN_RESPONSE',\n TOKEN_ERROR: 'TOKEN_ERROR',\n} as const;\n\n// ---- Centrifuge Worker \u8F93\u51FA\u6D88\u606F\u7C7B\u578B ----\nexport const CENTRIFUGE_OUTPUT_TYPE = {\n STATUS: 'STATUS',\n MESSAGE: 'MESSAGE',\n MESSAGE_BIN: 'MESSAGE_BIN',\n ERROR: 'ERROR',\n TOKEN_REQUEST: 'TOKEN_REQUEST',\n SESSION_REAPED: 'SESSION_REAPED',\n} as const;\n"],
5
- "mappings": "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",
6
- "names": []
7
- }