cross-tab-worker-databus 0.20.85 → 0.20.87

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 (44) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/centrifuge.js +1 -1
  3. package/dist/{chunk-PW63EWIK.js → chunk-ZNHJ5OMY.js} +356 -54
  4. package/dist/{chunk-PW63EWIK.js.map → chunk-ZNHJ5OMY.js.map} +3 -3
  5. package/dist/cjs/centrifuge.cjs +354 -53
  6. package/dist/cjs/centrifuge.cjs.map +3 -3
  7. package/dist/cjs/hooks.cjs +2 -2
  8. package/dist/cjs/hooks.cjs.map +2 -2
  9. package/dist/cjs/index.cjs +530 -112
  10. package/dist/cjs/index.cjs.map +3 -3
  11. package/dist/cjs/vue.cjs +1 -1
  12. package/dist/cjs/vue.cjs.map +2 -2
  13. package/dist/core/data-bus.d.ts +71 -15
  14. package/dist/core/data-bus.d.ts.map +1 -1
  15. package/dist/core/replay-manager.d.ts +3 -2
  16. package/dist/core/replay-manager.d.ts.map +1 -1
  17. package/dist/core/replay-persistence.d.ts.map +1 -1
  18. package/dist/core/replay-pruning.d.ts +21 -0
  19. package/dist/core/replay-pruning.d.ts.map +1 -0
  20. package/dist/hooks.d.ts +2 -1
  21. package/dist/hooks.d.ts.map +1 -1
  22. package/dist/hooks.js +2 -2
  23. package/dist/hooks.js.map +2 -2
  24. package/dist/index.js +178 -60
  25. package/dist/index.js.map +2 -2
  26. package/dist/vue.d.ts +2 -1
  27. package/dist/vue.d.ts.map +1 -1
  28. package/dist/vue.js +1 -1
  29. package/dist/vue.js.map +2 -2
  30. package/dist/websocket.d.ts +24 -2
  31. package/dist/websocket.d.ts.map +1 -1
  32. package/docs/api.md +27 -11
  33. package/docs/architecture.md +18 -3
  34. package/docs/benchmarks.md +8 -8
  35. package/docs/configuration.md +2 -2
  36. package/docs/roadmap.md +15 -1
  37. package/docs/transports.md +19 -2
  38. package/docs/zh/api.md +27 -11
  39. package/docs/zh/architecture.md +18 -3
  40. package/docs/zh/benchmarks.md +8 -8
  41. package/docs/zh/configuration.md +2 -2
  42. package/docs/zh/roadmap.md +15 -1
  43. package/docs/zh/transports.md +14 -2
  44. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,9 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.20.87] - 2026-09-16
4
+
5
+ ### Fixed
6
+ - Transport operations issued after a runtime `error` no longer reach the connection that just failed. While an automatic or demand-driven reopen is pending, `runTransport()` parks `subscribe()` / `publish()` behind a recovery gate and releases them only after a reopen succeeds (or the transport self-heals to `connected`). `transportReady` deliberately stays true through the error so `ready()` keeps tracking the installed transport, which meant a subscribe or publish inside the cooldown window was written to the dead connection and lost. A failed automatic attempt keeps the gate closed but now lets the next explicit operation start an immediate on-demand reopen instead of waiting out another cooldown, and parked operations flush behind that success; exhausting `recovery.maxAttempts`, or superseding the wait with `stop()` / page hide, releases the gate so the explicit-retry path and the documented suspend-drop semantics are unchanged. A clean `disconnected` still never schedules automatic recovery.
7
+ - An operation issued after a clean transport `disconnected` now reopens the transport on demand instead of being written to the closed connection. `runTransport()` previously exempted only `error`, so a `subscribe()` / `publish()` arriving after a clean `close` took the ready fast path and the backend dropped it (the WebSocket send guard reports dropped frames but never sends them). The fast path is now refused once the transport has actually reached `connected` and then reports `disconnected`: the clean close still schedules no background recovery, but the next explicit operation demands exactly one reopen and flushes the parked operation behind it. A transport that resolves `start()` before its first `connected` (worker-style backends report the connection asynchronously) keeps the previous behaviour, so the pre-connect window does not trigger a redundant reopen.
8
+ - `createWebSocketDataBus()` automatic recovery now actually reopens a failed WebSocket. `WebSocketTransport.start()` previously returned whenever `this.socket` was non-null, and an error/close left that reference in place, so every post-cooldown `reopenTransport()` was a no-op against the dead socket. The transport now tracks whether the current socket is active, replaces the stale connection on the next `start()`, re-sends subscriptions after the replacement opens, and ignores late callbacks from the superseded socket. An error followed by close also keeps the `error` status that schedules recovery.
9
+ - React and Vue `useCrossTabHealth` bindings now apply `intervalMs` changes without recreating the bus. React previously ignored an option change unless the bus identity changed, and the Vue composable ignored changes to a reactive options object; switching to `0` therefore left polling active, while changing a positive cadence kept the old timer. Both adapters now tear down the old listener/timer set and install the new cadence.
10
+ - `stop()` now resolves once the bus is torn down even when the transport's own `stop()` rejects or throws. The failure is routed through the same `onError` / unified `lastFailure` channel that page-hide suspension and open-failure cleanup already use, so the fire-and-forget `void bus.stop()` teardown in the React and Vue adapters can no longer surface as an unhandled rejection. The instance stays restartable either way.
11
+ - A failed transport open is now stamped once: `getRecoveryStats().errorAt` and the `lastFailure.at` of the same failure are equal instead of differing by a clock re-read. The open-failure path called the injected clock twice for one failure, so a consumer correlating the two ledgers (or a test using an advancing clock) saw two timestamps for one event. The open-failure path also clears `transportReady` before notifying status/error handlers, keeping the "transport is not accepting operations" verdict local to the failure block.
12
+ - `ready()` no longer reports a BFCache-suspended bus as ready. After `pagehide`, `suspendTransport()` chains `transport.stop()` and reuses `startPromise` as the stop gate; `ready()` previously returned that gate, so it resolved the moment cleanup finished even though the transport was intentionally stopped and publications were dropped. Readiness now rejects with a clear suspended-state error while hidden; `pageshow` (or an explicit `start()`) clears the flag and installs a real reopen promise, after which `ready()` resolves as before. This restores the documented invariant that `ready()` never resolves for a transport that cannot carry data.
13
+ - A runtime transport failure reported through `onError` now lands in the transport recovery ledger, not only in the unified `lastFailure` record. `getRecoveryStats().hasError` / `errorMessage` / `errorAt` were written exclusively by the transport-*open* failure path, so a failure raised after a successful open produced a self-contradicting health snapshot: `state: 'recovering'` and a retained `lastFailure` alongside `recovery.hasError: false, errorMessage: null`. The recovery ledger now tracks every transport-sourced failure (open or runtime) and still keeps non-transport failures (`persistence`, `dispatch`) out of it, where they remain visible through `lastFailure` and `getPersistenceStats()`.
14
+ - The native WebSocket backend now honors the `DataBusTransport.start()` contract: it resolves only after the socket `open` and rejects when the handshake errors, closes before opening, or exceeds the new `connectTimeoutMs` (default 30s). Previously `start()` returned while the socket was still `CONNECTING`, so `await bus.ready()` resolved before the connection was usable and an immediately following `publish()` was dropped by the not-open guard. A timed-out attempt now closes its half-open socket and ignores a late `open`; an in-place re-open after a successful handshake still re-asserts subscriptions.
15
+ - `getHealthSummary()` now treats a live `connected` transport as healthy even during the brief window before `start()` settles and `transportReady` flips to true. With a handshake-gated WebSocket start, the `connected` status event fires while `transportReady` is still false, so an event-driven `useCrossTabHealth` snapshot with `intervalMs: 0` could remain stuck in `starting`/`recovering`. `transportReady` remains a diagnostic field; operations are queued behind the in-flight start rather than dropped.
16
+
17
+
18
+ ## [0.20.86] - 2026-09-16
19
+
3
20
  ### Added
4
21
  - The configuration reference now documents the full public option surface: `replay.pruneStrategy` (previously absent entirely) and the replay options table (`maxPerTopic`, `persistence`, `retentionMs`, `pruneStrategy`, `retentionSweepMs`, `persistenceRetry`), plus a deduplication options table (`maxEntries`, `ttlMs`, `sweepMs`, `now`, `adaptiveTtl`) — in both languages. A guard derives the field list from the built declarations and fails when a `DataBusReplayOptions`/`DataBusDedupOptions` field is undocumented in either configuration reference. The `pruneStrategy` JSDoc no longer claims a default of `'both'` (the actual default is `'count'`).
5
22
  - Property invariants for `selectActiveWorkers` / `selectRebalanceTarget`: the returned worker set is always a subset of the input, active selection stays within `maxActiveWorkers` and non-empty for a non-empty input, and neither helper throws on arbitrary corrupt worker records.
6
23
 
24
+ ### Fixed
25
+ - `ready()` no longer reports readiness for a restart that a later `stop()` canceled. The queued `start()` promise retains its documented resolve-on-cancellation behavior, but readiness now rejects with a clear lifecycle error instead of resolving against a stopped bus.
26
+ - A failed restart queued behind an in-flight `stop()` now remains observable through `ready()`. Previously the failure cleared `started`, so a later `ready()` without `initialConfig` was masked by the generic "requires initialConfig" error; it now resurfaces the actual transport startup failure and preserves the explicit manual-retry path.
27
+ - `publish()` and non-empty `publishBatch()` calls issued while `stop()` is in flight now report through `onError` instead of silently returning once `runTransport()` refuses to touch the stopping transport. Empty batches remain no-ops, publications already queued behind an in-flight open are still canceled by the stop (latest lifecycle intent wins), and page-hide suspension keeps its documented drop-without-defer behavior.
28
+ - `subscribe()` and `ready()` now honor the explicit-stop barrier. A subscription requested while `stop()` is settling reports through `onError` and returns a no-op cleanup instead of being erased by teardown or leaking into a later restart; `ready()` rejects rather than resolving against the stopping transport. A `start()` already queued behind that stop remains the newest lifecycle intent, so `ready()` continues to follow its restart promise.
29
+ - A `stop()` that arrives before a restart queued behind an earlier in-flight `stop()` could run now cancels that queued restart instead of being swallowed by the in-flight stop gate. The queued continuation is invalidated with a monotonic token, so the latest lifecycle intent always wins: `stop → start → stop` ends stopped with no extra transport open, while a later `start()` still queues a fresh restart with a higher token.
30
+ - Superseded asynchronous transport opens can no longer tear down or detach a newer page-hide/pageshow reopen. Each open now carries a lifecycle epoch, so callbacks, failures, success telemetry, and cleanup from an older open are ignored once a newer suspend/resume/stop transition owns the lifecycle; `stop()` also waits for any pending open or reopen even when `started` has already been cleared. Previously an initial open failing after a queued resume reset `started` to false and nulled `startPromise`, making `stop()` resolve immediately while the queued transport could still start afterwards.
31
+ - `start()` now serialises correctly with both page-hide suspension and an in-flight explicit stop. A hidden bus whose asynchronous `transport.stop()` was still settling returned that cleanup promise from `start()` and never reopened the transport; an in-flight `stop()` likewise allowed `start()` to observe the old ready state and resolve as a no-op before the bus finished stopping. Both paths now queue one fresh start after cleanup, share concurrent start/stop promises, and preserve the existing degraded manual-recovery behavior.
32
+ - Explicit `start()` now performs a manual transport recovery when the bus has already started but its automatic recovery budget is exhausted; it preserves the cluster, subscriptions, and replay state while resetting the failure/recovery ledger before reopening. Previously `start()` returned a resolved no-op in the degraded state even though the public health contract documents it as the manual retry path. Covered by a regression that exhausts automatic recovery, succeeds through `start()`, and verifies the health summary returns to healthy.
33
+ - IndexedDB replay persistence now settles every mutation when a transaction aborts, including connection-loss aborts that fire `onabort` without a preceding request error. Previously the serialized mutation queue could remain blocked forever, preventing later appends and clears from running. Loads now resolve only after `transaction.oncomplete`, so a request that succeeds before a later abort cannot be reported as a successful read. Request/transaction failures without an `error` object use operation-specific fallback messages.
34
+ - Replay age pruning is now position-independent and uses one shared policy for the in-memory rings and IndexedDB adapter. Previously an expired timestamped entry after a timestamp-less legacy entry (or after a non-expired entry) was never removed, and hydrated history was always truncated to `maxPerTopic` even when `pruneStrategy: 'age'` was configured. Timestamp-less entries are still preserved for compatibility, but are now capped by `maxPerTopic` under AGE so they cannot grow without bound; timestamped entries remain bounded by the retention window. Covered across live recording, hydration without a `clearBefore` adapter, and durable append paths.
35
+
7
36
  ## [0.20.85] - 2026-09-13
8
37
 
9
38
  ### Added
@@ -5,7 +5,7 @@ import {
5
5
  parseDataBusPublication,
6
6
  publicationMetadata,
7
7
  selectWorkerBackend
8
- } from "./chunk-PW63EWIK.js";
8
+ } from "./chunk-ZNHJ5OMY.js";
9
9
  import {
10
10
  CENTRIFUGE_INPUT_TYPE,
11
11
  CENTRIFUGE_OUTPUT_TYPE,