cross-tab-worker-databus 0.21.56 → 0.21.57
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 +25 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-VDCLCCMN.js → chunk-ZH7VP3IY.js} +117 -23
- package/dist/chunk-ZH7VP3IY.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +116 -22
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +116 -22
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts +64 -3
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/environment.d.ts +23 -1
- package/dist/core/environment.d.ts.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/docs/api.md +3 -1
- package/docs/architecture.md +18 -16
- package/docs/benchmarks.md +8 -8
- package/docs/configuration.md +2 -2
- package/docs/roadmap.md +1 -1
- package/docs/zh/api.md +3 -1
- package/docs/zh/architecture.md +15 -13
- package/docs/zh/benchmarks.md +8 -8
- package/docs/zh/configuration.md +2 -2
- package/docs/zh/roadmap.md +1 -1
- package/package.json +1 -1
- package/dist/chunk-VDCLCCMN.js.map +0 -7
package/dist/cjs/index.cjs
CHANGED
|
@@ -288,12 +288,13 @@ function canUseStorage(storage, probeKey) {
|
|
|
288
288
|
return false;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
-
function getOrCreateTabId(environment, key = TAB_ID_STORAGE_KEY) {
|
|
291
|
+
function getOrCreateTabId(environment, key = TAB_ID_STORAGE_KEY, options) {
|
|
292
292
|
const storage = environment.sessionStorage;
|
|
293
293
|
try {
|
|
294
294
|
const existing = storage?.getItem(key);
|
|
295
295
|
const hasOpener = typeof window !== "undefined" && Boolean(window.opener);
|
|
296
|
-
|
|
296
|
+
const claimedByPeer = existing !== null && existing !== void 0 && existing !== "" && !hasOpener && !tabIdentityInitialized && options?.isIdentityClaimedByPeer?.(existing) === true;
|
|
297
|
+
if (existing && (!hasOpener || tabIdentityInitialized) && !claimedByPeer) {
|
|
297
298
|
tabIdentityInitialized = true;
|
|
298
299
|
cachedTabId = existing;
|
|
299
300
|
return existing;
|
|
@@ -811,7 +812,21 @@ var WorkerClusterRuntime = class {
|
|
|
811
812
|
this.subscriberPrefix = `${baseKey}:subscriber:`;
|
|
812
813
|
this.channelName = `${prefix}:bus:${clusterHash}`;
|
|
813
814
|
this.storage = canUseStorage(this.environment.storage, `${baseKey}:probe`) ? new BatchingStorageWriter(this.environment.storage) : null;
|
|
814
|
-
this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id
|
|
815
|
+
this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id`, {
|
|
816
|
+
// A browser-duplicated tab inherits sessionStorage without an opener, so
|
|
817
|
+
// `getOrCreateTabId()`'s opener guard cannot see it. A live Worker
|
|
818
|
+
// record carrying that id is the evidence the copy cannot carry: it
|
|
819
|
+
// lives in localStorage under a random `workerId`, and this probe runs
|
|
820
|
+
// in the constructor, before this Runtime has written a record of its
|
|
821
|
+
// own — so every match belongs to a different live document. A refresh
|
|
822
|
+
// removes its record in `pause()` and flushes that removal before the
|
|
823
|
+
// document goes away, so the refresh path keeps the stored id. The one
|
|
824
|
+
// case that does not is an abnormal exit, whose record survives for up
|
|
825
|
+
// to `workerTtlMs`; a reload inside that window takes a fresh identity,
|
|
826
|
+
// which costs one observer a changed `tabId` and nothing else — the
|
|
827
|
+
// stale record is pruned by the normal TTL path.
|
|
828
|
+
isIdentityClaimedByPeer: (candidate) => this.isTabIdLiveElsewhere(candidate)
|
|
829
|
+
});
|
|
815
830
|
this.workerId = options.workerId ?? `worker-${this.tabId}-${this.environment.randomId()}`;
|
|
816
831
|
const now = this.nowMs();
|
|
817
832
|
this.currentRecord = {
|
|
@@ -1005,12 +1020,12 @@ var WorkerClusterRuntime = class {
|
|
|
1005
1020
|
const topicKey = this.releaseSubscription(topic);
|
|
1006
1021
|
if (topicKey && !this.assignedTopics.has(topicKey)) this.knownTopics.delete(topicKey);
|
|
1007
1022
|
}
|
|
1008
|
-
/** Remove this
|
|
1023
|
+
/** Remove this Runtime's subscriber record and, when it was the last one, delete
|
|
1009
1024
|
* the route. Returns the topicKey (so callers like `unsubscribe` can reuse
|
|
1010
1025
|
* it instead of re-hashing the topic to evict the reverse cache). */
|
|
1011
1026
|
releaseSubscription(topic, notifyOwner = true) {
|
|
1012
1027
|
const topicKey = this.rememberTopic(topic);
|
|
1013
|
-
this.removeStorage(this.subscriberStorageKey(topicKey, this.
|
|
1028
|
+
this.removeStorage(this.subscriberStorageKey(topicKey, this.workerId));
|
|
1014
1029
|
const route = this.readRoute(topicKey);
|
|
1015
1030
|
if (!route) return topicKey;
|
|
1016
1031
|
const subscribers = this.readSubscriberTabIds(topicKey, this.readWorkers());
|
|
@@ -1452,11 +1467,7 @@ var WorkerClusterRuntime = class {
|
|
|
1452
1467
|
) ?? this.currentRecord;
|
|
1453
1468
|
recoveryProjectedLoads.set(owner.workerId, (recoveryProjectedLoads.get(owner.workerId) ?? owner.load) + 1);
|
|
1454
1469
|
if (owner.workerId !== this.workerId) {
|
|
1455
|
-
|
|
1456
|
-
const ownerSubscribed = workers.some(
|
|
1457
|
-
(worker) => worker.workerId === owner.workerId && subscriberTabIds.has(worker.tabId)
|
|
1458
|
-
);
|
|
1459
|
-
if (ownerSubscribed) continue;
|
|
1470
|
+
if (this.readSubscriberIds(topicKey, workers).workerIds.has(owner.workerId)) continue;
|
|
1460
1471
|
}
|
|
1461
1472
|
this.writeRoute(topicKey, owner, void 0, route.generation + 1);
|
|
1462
1473
|
this.sendControl(owner.workerId, CONTROL_ACTION.SUBSCRIBE, topic, topicKey);
|
|
@@ -1577,6 +1588,38 @@ var WorkerClusterRuntime = class {
|
|
|
1577
1588
|
return false;
|
|
1578
1589
|
}
|
|
1579
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Whether any other live Worker record already carries `tabId`.
|
|
1593
|
+
*
|
|
1594
|
+
* Used once per document, from the constructor, to tell a browser-duplicated
|
|
1595
|
+
* tab from this tab's own refresh: the copy inherits `sessionStorage` but not
|
|
1596
|
+
* the opener, so the identity stored there is the only shared state and the
|
|
1597
|
+
* Worker records are the only evidence that another live document holds it.
|
|
1598
|
+
* Liveness is the same heartbeat window `readWorkers()` prunes by, so a record
|
|
1599
|
+
* left behind by an abnormal exit is reported as live until it expires — the
|
|
1600
|
+
* only false positive, and it re-mints one identity instead of letting two
|
|
1601
|
+
* live tabs share one.
|
|
1602
|
+
*
|
|
1603
|
+
* The `!this.storage` leg is the degraded mode, and it earns its line for two
|
|
1604
|
+
* reasons rather than one. It is what narrows the field for the scan below
|
|
1605
|
+
* (`StorageLike | null` is rejected by `readAllByPrefix`), so the compiler
|
|
1606
|
+
* holds it; and it is *not* what decides the verdict, because `listKeys()`
|
|
1607
|
+
* catches its own failure and returns no records either way — measured by
|
|
1608
|
+
* emptying this consequent, which leaves the suite green. What the inverted
|
|
1609
|
+
* return does prove is the behaviour, not the deletion: returning `true` here
|
|
1610
|
+
* fails 'keeps the inherited id when there is no storage to probe', since
|
|
1611
|
+
* without `localStorage` there is no second document to collide with.
|
|
1612
|
+
*/
|
|
1613
|
+
isTabIdLiveElsewhere(tabId) {
|
|
1614
|
+
if (!this.storage) return false;
|
|
1615
|
+
const now = this.nowMs();
|
|
1616
|
+
for (const { value: worker } of readAllByPrefix(this.storage, this.workerPrefix)) {
|
|
1617
|
+
if (worker.tabId !== tabId) continue;
|
|
1618
|
+
if (now - worker.heartbeatAt > this.workerTtlMs) continue;
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1580
1623
|
/** Read all live worker records from storage, pruning stale entries past the TTL. */
|
|
1581
1624
|
readWorkers() {
|
|
1582
1625
|
if (!this.storage) return [this.currentRecord];
|
|
@@ -1594,23 +1637,61 @@ var WorkerClusterRuntime = class {
|
|
|
1594
1637
|
}
|
|
1595
1638
|
/** Enumerate all tab IDs that have a subscriber record for `topicKey`. */
|
|
1596
1639
|
readSubscriberTabIds(topicKey, workers) {
|
|
1640
|
+
return Array.from(this.readSubscriberIds(topicKey, workers).tabIds);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* One scan of `topicKey`'s subscriber records, answering the two questions
|
|
1644
|
+
* callers ask: which Tabs hold a subscription (emptiness checks, orphan-route
|
|
1645
|
+
* pruning) and which **Runtimes** do (the stranded-handoff re-election).
|
|
1646
|
+
*
|
|
1647
|
+
* The second answer is the finer one and the reason this is one method rather
|
|
1648
|
+
* than two scans: a tab-level answer lets a leftover record from a sibling
|
|
1649
|
+
* Runtime speak for a Worker that never subscribed to the topic, and standing
|
|
1650
|
+
* down on that proof stalls the recovery forever (see the call site).
|
|
1651
|
+
*
|
|
1652
|
+
* A legacy record — written before records carried `workerId` — proves only
|
|
1653
|
+
* that *some* Runtime in its tab subscribes, so it contributes to the tab set
|
|
1654
|
+
* and **not** to the worker set. Treating it as the whole tab, which is what
|
|
1655
|
+
* the tab-level reader effectively did, is the conservative-looking choice
|
|
1656
|
+
* that can stall; the cost of the other direction is one redundant
|
|
1657
|
+
* `CONTROL/SUBSCRIBE` to a peer that is about to confirm anyway.
|
|
1658
|
+
*/
|
|
1659
|
+
readSubscriberIds(topicKey, workers) {
|
|
1660
|
+
const tabIds = /* @__PURE__ */ new Set();
|
|
1661
|
+
const workerIds = /* @__PURE__ */ new Set();
|
|
1597
1662
|
if (!this.storage) {
|
|
1598
1663
|
const topic = this.knownTopics.get(topicKey);
|
|
1599
|
-
|
|
1664
|
+
if (topic && this.subscribedTopics.has(topic)) {
|
|
1665
|
+
tabIds.add(this.tabId);
|
|
1666
|
+
workerIds.add(this.workerId);
|
|
1667
|
+
}
|
|
1668
|
+
return { tabIds, workerIds };
|
|
1600
1669
|
}
|
|
1601
1670
|
const activeTabIds = new Set(workers.map((worker) => worker.tabId));
|
|
1602
|
-
const
|
|
1671
|
+
const activeWorkerIds = new Set(workers.map((worker) => worker.workerId));
|
|
1603
1672
|
for (const { key, value: record } of readAllByPrefix(
|
|
1604
1673
|
this.storage,
|
|
1605
1674
|
`${this.subscriberPrefix}${topicKey}:`
|
|
1606
1675
|
)) {
|
|
1607
|
-
if (!
|
|
1676
|
+
if (!this.isSubscriberRecordLive(record, activeTabIds, activeWorkerIds)) {
|
|
1608
1677
|
this.removeStorage(key);
|
|
1609
1678
|
continue;
|
|
1610
1679
|
}
|
|
1611
|
-
|
|
1680
|
+
tabIds.add(record.tabId);
|
|
1681
|
+
if (record.workerId !== void 0) workerIds.add(record.workerId);
|
|
1612
1682
|
}
|
|
1613
|
-
return
|
|
1683
|
+
return { tabIds, workerIds };
|
|
1684
|
+
}
|
|
1685
|
+
/** Whether a subscriber record still belongs to a live Runtime.
|
|
1686
|
+
*
|
|
1687
|
+
* A record written by this version names its `workerId`, so liveness is the
|
|
1688
|
+
* Worker set the reconcile pass already prunes by — which is what keeps two
|
|
1689
|
+
* Runtimes in one tab independent. A record from an older version carries no
|
|
1690
|
+
* `workerId`; it is honoured while its *tab* lives, the finest identity it
|
|
1691
|
+
* has, and each Runtime replaces its own with a worker-keyed record on its
|
|
1692
|
+
* next write. */
|
|
1693
|
+
isSubscriberRecordLive(record, activeTabIds, activeWorkerIds) {
|
|
1694
|
+
return record.workerId === void 0 ? activeTabIds.has(record.tabId) : activeWorkerIds.has(record.workerId);
|
|
1614
1695
|
}
|
|
1615
1696
|
/** Read the current route for `topicKey`. With a storage layer this returns
|
|
1616
1697
|
* the durable route, or null when none is recorded. Without one (degraded
|
|
@@ -1676,19 +1757,31 @@ var WorkerClusterRuntime = class {
|
|
|
1676
1757
|
this.removeStorage(key);
|
|
1677
1758
|
}
|
|
1678
1759
|
}
|
|
1679
|
-
/** Remove subscriber records
|
|
1760
|
+
/** Remove subscriber records whose owning Runtime (or, for a legacy record,
|
|
1761
|
+
* whose tab) is no longer active. */
|
|
1680
1762
|
cleanupOrphanedSubscribers(workers) {
|
|
1681
1763
|
if (!this.storage) return;
|
|
1682
1764
|
const activeTabIds = new Set(workers.map((worker) => worker.tabId));
|
|
1765
|
+
const activeWorkerIds = new Set(workers.map((worker) => worker.workerId));
|
|
1683
1766
|
for (const { key, value: record } of readAllByPrefix(this.storage, this.subscriberPrefix)) {
|
|
1684
|
-
if (!
|
|
1767
|
+
if (!this.isSubscriberRecordLive(record, activeTabIds, activeWorkerIds)) this.removeStorage(key);
|
|
1685
1768
|
}
|
|
1686
1769
|
}
|
|
1687
|
-
/** Persist a subscriber record for this
|
|
1770
|
+
/** Persist a subscriber record for this Runtime on `topicKey`.
|
|
1771
|
+
*
|
|
1772
|
+
* Keyed by `workerId`, not `tabId`. A document may run several Runtimes that
|
|
1773
|
+
* share one `tabId` on purpose (the market + notice pattern), and a tab-keyed
|
|
1774
|
+
* record belonged to all of them at once: whichever unsubscribed first deleted
|
|
1775
|
+
* the others' intent, the route then looked orphaned, and the owner dropped a
|
|
1776
|
+
* transport subscription its surviving sibling still needed — a delivery gap
|
|
1777
|
+
* until that sibling's next reconcile tick re-elected an owner. Duplicated
|
|
1778
|
+
* tabs get the same protection even when the identity probe cannot see them,
|
|
1779
|
+
* which is why the fix is here as well as in `isTabIdLiveElsewhere()`. */
|
|
1688
1780
|
writeSubscriber(topicKey) {
|
|
1689
1781
|
if (!this.storage) return;
|
|
1690
|
-
writeJson(this.storage, this.subscriberStorageKey(topicKey, this.
|
|
1782
|
+
writeJson(this.storage, this.subscriberStorageKey(topicKey, this.workerId), {
|
|
1691
1783
|
tabId: this.tabId,
|
|
1784
|
+
workerId: this.workerId,
|
|
1692
1785
|
updatedAt: this.nowMs()
|
|
1693
1786
|
});
|
|
1694
1787
|
}
|
|
@@ -1762,8 +1855,9 @@ var WorkerClusterRuntime = class {
|
|
|
1762
1855
|
routeStorageKey(topicKey) {
|
|
1763
1856
|
return `${this.routePrefix}${topicKey}`;
|
|
1764
1857
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1858
|
+
/** `ownerId` is the Runtime's `workerId`; older versions used the `tabId`. */
|
|
1859
|
+
subscriberStorageKey(topicKey, ownerId) {
|
|
1860
|
+
return `${this.subscriberPrefix}${topicKey}:${ownerId}`;
|
|
1767
1861
|
}
|
|
1768
1862
|
removeStorage(key) {
|
|
1769
1863
|
try {
|
|
@@ -2643,7 +2737,7 @@ var SDK_VERSION = (
|
|
|
2643
2737
|
// declaration's type is `string | undefined` and the export's is `string`
|
|
2644
2738
|
// (dropping the conditional is TS2322, measured). Recorded here so the leg reads
|
|
2645
2739
|
// as a shape the harness cannot reach rather than as a case nobody exercised.
|
|
2646
|
-
true ? "0.21.
|
|
2740
|
+
true ? "0.21.57" : ""
|
|
2647
2741
|
);
|
|
2648
2742
|
|
|
2649
2743
|
// src/core/data-bus.ts
|