cross-tab-worker-databus 0.21.11 → 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.
- package/CHANGELOG.md +23 -0
- package/README.md +2 -2
- package/README.zh.md +3 -1
- package/dist/centrifuge-protocol.d.ts.map +1 -1
- package/dist/centrifuge-session.d.ts +10 -3
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.d.ts +10 -4
- package/dist/centrifuge.d.ts.map +1 -1
- package/dist/centrifuge.js +23 -11
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js +28 -11
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js +10 -3
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-UO57SLQD.js → chunk-4HW6PYIR.js} +67 -33
- package/dist/chunk-4HW6PYIR.js.map +7 -0
- package/dist/{chunk-2TTJ7LGZ.js → chunk-NPFTD7YE.js} +1 -1
- package/dist/chunk-NPFTD7YE.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +86 -40
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/hooks.cjs.map +2 -2
- package/dist/cjs/index.cjs +80 -36
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/cjs/vue.cjs.map +2 -2
- package/dist/core/cluster.d.ts +33 -16
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +19 -5
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/dedup-manager.d.ts +6 -3
- package/dist/core/dedup-manager.d.ts.map +1 -1
- package/dist/core/environment.d.ts +14 -5
- package/dist/core/environment.d.ts.map +1 -1
- package/dist/core/hash.d.ts +10 -4
- package/dist/core/hash.d.ts.map +1 -1
- package/dist/core/replay-manager.d.ts +2 -1
- package/dist/core/replay-manager.d.ts.map +1 -1
- package/dist/core/routing.d.ts +18 -9
- package/dist/core/routing.d.ts.map +1 -1
- package/dist/core/storage-batch.d.ts +2 -2
- package/dist/core/storage-batch.d.ts.map +1 -1
- package/dist/core/trace.d.ts +21 -9
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/types.d.ts +13 -5
- package/dist/core/types.d.ts.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +1 -1
- package/dist/hooks.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +2 -2
- package/dist/utils/constants.d.ts +7 -4
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +12 -4
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/vue.js +1 -1
- package/dist/websocket.d.ts +24 -6
- package/dist/websocket.d.ts.map +1 -1
- package/dist/workers/port-reaper.d.ts +18 -8
- package/dist/workers/port-reaper.d.ts.map +1 -1
- package/docs/api.md +6 -5
- package/docs/benchmarks.md +9 -9
- package/docs/release-checklist.md +6 -6
- package/docs/roadmap.md +57 -2
- package/docs/transports.md +3 -2
- package/docs/zh/api.md +6 -5
- package/docs/zh/benchmarks.md +9 -9
- package/docs/zh/release-checklist.md +6 -6
- package/docs/zh/roadmap.md +42 -2
- package/docs/zh/transports.md +2 -1
- package/package.json +2 -1
- package/dist/chunk-2TTJ7LGZ.js.map +0 -7
- package/dist/chunk-UO57SLQD.js.map +0 -7
package/dist/websocket.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
105
|
-
*
|
|
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
|
|
110
|
-
*
|
|
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.
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
55
|
-
* default
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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.
|
|
75
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/docs/benchmarks.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
# Browser Benchmark Trend
|
|
4
4
|
|
|
5
|
-
> Data through 2026-09-23, from the
|
|
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 |
|
|
13
|
-
| publish per-message (ms, lower is better) — shared |
|
|
14
|
-
| wildcard dispatch ×1000 (ms, lower is better) | 5.
|
|
15
|
-
| publishBatch ×1000 (ms, lower is better) | 3.5 | 3.
|
|
16
|
-
| dedup ×1000 (ms, lower is better) |
|
|
17
|
-
| trace + publish ×1000 (ms, lower is better) |
|
|
18
|
-
| first-packet cold dispatch (ms, lower is better) | 0 | 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
|
|
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
|
-
Only `pnpm bench:browser` / `pnpm bench:compare` stay local-only: shared-runner timing noise makes numeric CI gates unreliable.
|
|
16
|
+
Only `pnpm bench:browser` / `pnpm bench:compare` / `pnpm bench:trend` stay local-only: shared-runner timing noise makes numeric CI gates unreliable, and `bench:trend` rewrites `docs/benchmarks.md` from the local archive, which CI must not do.
|
|
17
17
|
|
|
18
18
|
## Before tagging
|
|
19
19
|
|
|
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`
|
|
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
|
|
|
@@ -30,7 +30,7 @@ The repository runs CodeQL (`javascript-typescript`; on push, on pull request, a
|
|
|
30
30
|
|
|
31
31
|
## Tagged-release workflow
|
|
32
32
|
|
|
33
|
-
Pushing a version tag triggers the `Release` GitHub Action: it runs `pnpm check` and `pnpm lint` (a tag can point at a commit that never passed CI's lint step), runs `verify:compat` and `verify:pack`, opens the GitHub release from the `CHANGELOG` section, publishes to npm when the `NPM_TOKEN` secret is set, and then runs the **blocking** published-consumer verification
|
|
33
|
+
Pushing a version tag triggers the `Release` GitHub Action: it runs `pnpm check` and `pnpm lint` (a tag can point at a commit that never passed CI's lint step), runs `verify:compat` and `verify:pack`, opens the GitHub release from the `CHANGELOG` section, publishes to npm when the `NPM_TOKEN` secret is set, and then runs the **blocking** published-consumer verification at a budget **twelve times the manual default** — the workflow sets `PUBLISHED_VERIFY_ATTEMPTS=48` and `PUBLISHED_VERIFY_DELAY_MS=7500` (a ~6-minute ceiling) in its own env, while `scripts/verify-published-consumer.mjs` defaults to 6 × 5000 ms = 30 s when run by hand. A release whose published package cannot be imported by a clean consumer fails the workflow — treat every `verify:published` failure as a failed release. For registry propagation or infrastructure failures, rerun the workflow against the unchanged tag; for artifact defects, ship a new patch version. Never move or reuse a published tag. When no token is configured the publish step is skipped, but verification still passes against whatever version is already on npm (e.g. one published manually).
|
|
34
34
|
|
|
35
35
|
## Publishing
|
|
36
36
|
|
|
@@ -42,4 +42,4 @@ For a manual release (no `NPM_TOKEN` in the workflow), run `npm publish --access
|
|
|
42
42
|
2. Install the published tarball or version in a clean consumer and import the root plus every public subpath.
|
|
43
43
|
3. Record the result in the release notes. Do not advance to `1.0.0` until the public API and protocol deprecation policy are explicitly frozen.
|
|
44
44
|
|
|
45
|
-
The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. The workflow's 6-minute ceiling absorbs normal npm CDN propagation lag (the 0.20.89 tag run exhausted the older 2-minute budget after a successful publish); raise `PUBLISHED_VERIFY_ATTEMPTS` / `PUBLISHED_VERIFY_DELAY_MS` further only for unusually slow mirrors.
|
|
45
|
+
The tagged-release workflow already ran the consumer verification above. Run `PUBLISHED_VERSION=<version> pnpm verify:published` by hand only when you need an offline repeat. The workflow's 6-minute ceiling absorbs normal npm CDN propagation lag (the 0.20.89 tag run exhausted the older 2-minute budget after a successful publish); raise `PUBLISHED_VERIFY_ATTEMPTS` / `PUBLISHED_VERIFY_DELAY_MS` further only for unusually slow mirrors. Note the asymmetry when repeating by hand: `verify-published-consumer.mjs` defaults to `6` attempts × `5_000` ms — 30 seconds, not 6 minutes — because the tag run passes the larger pair as env rather than the script owning it. So a manual repeat can report `ETARGET` on a version whose CI run was green, which reads like a failed publish and is not one; reproduce the CI budget explicitly with `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=48 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`, or judge a manual failure against the 30 s budget you actually ran. Expect ~39 retries of 48 in a *green* tag run: registry propagation, not a symptom.
|
package/docs/roadmap.md
CHANGED
|
@@ -1,7 +1,62 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
4
|
-
|
|
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.
|
|
34
|
+
|
|
35
|
+
## 0.21.12 delivered scope
|
|
36
|
+
|
|
37
|
+
- The prose sweep finished where it started: the README. `README.md` and `README.zh.md` promised
|
|
38
|
+
"the persistence layer does not store connection addresses, raw Topic text, or message content"
|
|
39
|
+
two bullets under the line advertising the durable replay persistence that does precisely that —
|
|
40
|
+
its IndexedDB store is created with `keyPath: 'topic'` and its rows carry the publication payloads,
|
|
41
|
+
and the factory is a public export. The claim is now scoped to the coordination records it is true
|
|
42
|
+
of, and both opt-ins that persist real content are named beside it. The Chinese list also gained
|
|
43
|
+
the two feature bullets it never had, so the promise no longer stands without its disclosure.
|
|
44
|
+
- `docs/release-checklist.md`, which ships in the tarball, held three traps. The published-consumer
|
|
45
|
+
gate was said to run "with the same budget as a manual run" — the workflow passes 48 × 7500 ms as
|
|
46
|
+
its own env while the script defaults to 6 × 5000 ms, so a hand-run repeat hits `ETARGET` after 30
|
|
47
|
+
seconds on a tag whose CI run was green. Measured on `v0.21.11`: 39 retries of 48 in a successful
|
|
48
|
+
run, and the offline repeat passed first try. Both budgets are now stated, with the command that
|
|
49
|
+
reproduces the CI one. CI was described as running on "every push and pull request" although both
|
|
50
|
+
triggers are filtered to `branches: [main]`, and `bench:trend` was missing from the local-only list
|
|
51
|
+
despite rewriting `docs/benchmarks.md` from the local archive.
|
|
52
|
+
- The `ci.yml` comment naming the coverage floors still said 96/92/96/97 after 0.21.7 raised them to
|
|
53
|
+
98/96/98/99; the config, the shipped checklist and `AGENTS.md` already agreed, so only the comment
|
|
54
|
+
had drifted. Corrected, and `AGENTS.md` now records how the sweep concluded: an audit of "the option
|
|
55
|
+
surface" covers the tables, while the paragraph under the same heading and the README's summary
|
|
56
|
+
bullets are separate claims — and the sentence shape worth hunting first is an absolute about
|
|
57
|
+
storage, because one opt-in is enough to break it.
|
|
58
|
+
- No behaviour, wire-format, storage-layout or cluster-protocol change, and no `src/` or `tests/`
|
|
59
|
+
file was touched. Coverage stays at 99.01 / 96.90 / 99.26 / 99.69 over **895** tests.
|
|
5
60
|
## 0.21.11 delivered scope
|
|
6
61
|
|
|
7
62
|
- A prose audit of the four docs files no earlier sweep had covered — `architecture.md`,
|
package/docs/transports.md
CHANGED
|
@@ -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.
|
|
30
|
-
* `ReadonlyArray` to whatever your
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
package/docs/zh/benchmarks.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
# 浏览器基准趋势
|
|
4
4
|
|
|
5
|
-
> 数据截至 2026-09-23,基于
|
|
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
|
|
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 |
|
|
13
|
-
| publish per-message (ms, lower is better) — shared |
|
|
14
|
-
| wildcard dispatch ×1000 (ms, lower is better) | 5.
|
|
15
|
-
| publishBatch ×1000 (ms, lower is better) | 3.5 | 3.
|
|
16
|
-
| dedup ×1000 (ms, lower is better) |
|
|
17
|
-
| trace + publish ×1000 (ms, lower is better) |
|
|
18
|
-
| first-packet cold dispatch (ms, lower is better) | 0 | 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
|
|
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
|
-
只有 `pnpm bench:browser` / `pnpm bench:compare` 保持仅本地执行:共享 runner 的计时噪声会让数值型 CI
|
|
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
|
|
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
|
|
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
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
## 打 tag 的发布工作流
|
|
32
32
|
|
|
33
|
-
推送版本 tag 会触发 `Release` GitHub Action:先跑 `pnpm check` 与 `pnpm lint`(tag 可能指向从未通过 CI lint 步骤的提交),再跑 `verify:compat` 与 `verify:pack`,从 `CHANGELOG` 对应章节生成 GitHub release,配置了 `NPM_TOKEN` 时自动发布到 npm
|
|
33
|
+
推送版本 tag 会触发 `Release` GitHub Action:先跑 `pnpm check` 与 `pnpm lint`(tag 可能指向从未通过 CI lint 步骤的提交),再跑 `verify:compat` 与 `verify:pack`,从 `CHANGELOG` 对应章节生成 GitHub release,配置了 `NPM_TOKEN` 时自动发布到 npm,然后运行**阻塞式**消费者验证,其预算是手动默认值的十二倍——工作流在自己的 env 里设置 `PUBLISHED_VERIFY_ATTEMPTS=48`、`PUBLISHED_VERIFY_DELAY_MS=7500`(约 6 分钟上限),而手动运行 `scripts/verify-published-consumer.mjs` 时的默认值是 6 × 5000 ms = 30 秒。已发布包若无法被干净消费者导入,工作流即失败——任何 `verify:published` 失败都应视为发布失败。若为 registry 传播延迟或基础设施故障,针对不变的 tag 重跑工作流;若为产物缺陷,发布新的 patch 版本。禁止移动或重用已发布 tag。未配置 token 时跳过发布步骤,但验证仍会针对 npm 上已有的版本(例如手动发布的)通过。
|
|
34
34
|
|
|
35
35
|
## 发布(手动场景)
|
|
36
36
|
|
|
@@ -42,4 +42,4 @@
|
|
|
42
42
|
2. 在干净消费者中安装已发布版本或 tarball,并导入主入口及所有公开子路径。
|
|
43
43
|
3. 将结果记录到发布说明。在公开 API 和协议弃用策略明确冻结前,不进入 `1.0.0`。
|
|
44
44
|
|
|
45
|
-
打 tag 的发布工作流已自动执行上述消费者验证;仅在需要离线复验时才手动运行 `PUBLISHED_VERSION=<version> pnpm verify:published`。工作流默认的 6 分钟上限足以吸收 npm CDN 的正常传播延迟(0.20.89 tag 首次运行在发布成功后排空了旧的 2 分钟预算);只有遇到异常慢的镜像才需要继续调大 `PUBLISHED_VERIFY_ATTEMPTS` 和 `PUBLISHED_VERIFY_DELAY_MS
|
|
45
|
+
打 tag 的发布工作流已自动执行上述消费者验证;仅在需要离线复验时才手动运行 `PUBLISHED_VERSION=<version> pnpm verify:published`。工作流默认的 6 分钟上限足以吸收 npm CDN 的正常传播延迟(0.20.89 tag 首次运行在发布成功后排空了旧的 2 分钟预算);只有遇到异常慢的镜像才需要继续调大 `PUBLISHED_VERIFY_ATTEMPTS` 和 `PUBLISHED_VERIFY_DELAY_MS`。手动复验时要注意这个不对称:`verify-published-consumer.mjs` 的默认值是 `6` 次 × `5_000` ms——30 秒而不是 6 分钟——因为更大的那一对是由 tag 工作流以 env 传入、而非脚本自带。于是手动复跑可能在一个 CI 全绿的版本上报出 `ETARGET`,这看起来像发布失败而实际不是;要对齐 CI 预算就显式执行 `PUBLISHED_VERSION=<version> PUBLISHED_VERIFY_ATTEMPTS=48 PUBLISHED_VERIFY_DELAY_MS=7500 pnpm verify:published`,否则请按你实际跑到的 30 秒预算来判断手动失败。*绿色*的 tag 运行里出现约 39/48 次重试是预期内的:那是 registry 传播,不是症状。
|
package/docs/zh/roadmap.md
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
# 路线图
|
|
2
2
|
|
|
3
|
-
0.21.
|
|
4
|
-
|
|
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** 个测试。
|
|
25
|
+
|
|
26
|
+
## 0.21.12 已完成范围
|
|
27
|
+
|
|
28
|
+
- 文案审计回到了它的起点:README。`README.md` 与 `README.zh.md` 承诺"持久层不存储连接地址、原始
|
|
29
|
+
Topic 文本和消息内容",而这句话的两条之上正是在宣传那份 durable 回放持久化——它的 IndexedDB 对象仓以
|
|
30
|
+
`keyPath: 'topic'` 创建,行里保存着 publication payload,而工厂函数是公开导出。该说法现在被收敛到它确实
|
|
31
|
+
成立的范围(协调记录),并把两个会写入真实内容的可选开关点名列出。中文特性列表还补上了它此前完全缺失的两条
|
|
32
|
+
feature 条目,于是不再出现"只有隐私承诺、没有对应披露"的读法。
|
|
33
|
+
- 随包分发的 `docs/release-checklist.md` 里有三处会在关键时刻误导维护者。发布后消费者验证门禁被描述为"与手动
|
|
34
|
+
执行相同预算"——实际上工作流以自身 env 传入 48 × 7500 ms,而脚本默认是 6 × 5000 ms,于是手动复跑会在一个 CI
|
|
35
|
+
全绿的 tag 上 30 秒后报 `ETARGET`。以 `v0.21.11` 实测:成功的那次运行重试了 48 次中的 39 次,而离线复验首次即
|
|
36
|
+
通过。现在两个预算都已写明,并给出复现 CI 预算的命令。CI 被写成"在每次 push 与 pull request 上运行",而两个
|
|
37
|
+
触发器都被过滤为 `branches: [main]`;`bench:trend` 也从"仅本地"清单里漏掉了,尽管它会依据本地归档重写
|
|
38
|
+
`docs/benchmarks.md`。
|
|
39
|
+
- `ci.yml` 中记录覆盖率下限的注释在 0.21.7 把下限提升到 98/96/98/99 之后仍写着 96/92/96/97;配置、随包的分发
|
|
40
|
+
清单与 `AGENTS.md` 早已一致,只有注释发生了漂移,现已修正。`AGENTS.md` 同时记录了这轮扫描的结论:审计"选项
|
|
41
|
+
面"覆盖的是表格,而同一标题下的段落与 README 的摘要条目是彼此独立的说法——最值得优先搜寻的句式是关于存储的绝对
|
|
42
|
+
表述,因为一个可选开关就足以把它击穿。
|
|
43
|
+
- 无行为、wire 格式、存储布局或集群协议变更,且本发布未触碰任何 `src/` 或 `tests/` 文件。覆盖率保持
|
|
44
|
+
99.01 / 96.90 / 99.26 / 99.69,共 **895** 个测试。
|
|
5
45
|
## 0.21.11 已完成范围
|
|
6
46
|
|
|
7
47
|
- 一轮针对此前从未被扫过的四个文档文件(`architecture.md`、`capabilities.md`、`getting-started.md`、
|
package/docs/zh/transports.md
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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",
|