cross-tab-worker-databus 0.21.55 → 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 +42 -0
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.js +1 -1
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-TJ7XHT7I.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-TJ7XHT7I.js.map +0 -7
|
@@ -144,12 +144,13 @@ function canUseStorage(storage, probeKey) {
|
|
|
144
144
|
return false;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
function getOrCreateTabId(environment, key = TAB_ID_STORAGE_KEY) {
|
|
147
|
+
function getOrCreateTabId(environment, key = TAB_ID_STORAGE_KEY, options) {
|
|
148
148
|
const storage = environment.sessionStorage;
|
|
149
149
|
try {
|
|
150
150
|
const existing = storage?.getItem(key);
|
|
151
151
|
const hasOpener = typeof window !== "undefined" && Boolean(window.opener);
|
|
152
|
-
|
|
152
|
+
const claimedByPeer = existing !== null && existing !== void 0 && existing !== "" && !hasOpener && !tabIdentityInitialized && options?.isIdentityClaimedByPeer?.(existing) === true;
|
|
153
|
+
if (existing && (!hasOpener || tabIdentityInitialized) && !claimedByPeer) {
|
|
153
154
|
tabIdentityInitialized = true;
|
|
154
155
|
cachedTabId = existing;
|
|
155
156
|
return existing;
|
|
@@ -716,7 +717,21 @@ var WorkerClusterRuntime = class {
|
|
|
716
717
|
this.subscriberPrefix = `${baseKey}:subscriber:`;
|
|
717
718
|
this.channelName = `${prefix}:bus:${clusterHash}`;
|
|
718
719
|
this.storage = canUseStorage(this.environment.storage, `${baseKey}:probe`) ? new BatchingStorageWriter(this.environment.storage) : null;
|
|
719
|
-
this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id
|
|
720
|
+
this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id`, {
|
|
721
|
+
// A browser-duplicated tab inherits sessionStorage without an opener, so
|
|
722
|
+
// `getOrCreateTabId()`'s opener guard cannot see it. A live Worker
|
|
723
|
+
// record carrying that id is the evidence the copy cannot carry: it
|
|
724
|
+
// lives in localStorage under a random `workerId`, and this probe runs
|
|
725
|
+
// in the constructor, before this Runtime has written a record of its
|
|
726
|
+
// own — so every match belongs to a different live document. A refresh
|
|
727
|
+
// removes its record in `pause()` and flushes that removal before the
|
|
728
|
+
// document goes away, so the refresh path keeps the stored id. The one
|
|
729
|
+
// case that does not is an abnormal exit, whose record survives for up
|
|
730
|
+
// to `workerTtlMs`; a reload inside that window takes a fresh identity,
|
|
731
|
+
// which costs one observer a changed `tabId` and nothing else — the
|
|
732
|
+
// stale record is pruned by the normal TTL path.
|
|
733
|
+
isIdentityClaimedByPeer: (candidate) => this.isTabIdLiveElsewhere(candidate)
|
|
734
|
+
});
|
|
720
735
|
this.workerId = options.workerId ?? `worker-${this.tabId}-${this.environment.randomId()}`;
|
|
721
736
|
const now = this.nowMs();
|
|
722
737
|
this.currentRecord = {
|
|
@@ -910,12 +925,12 @@ var WorkerClusterRuntime = class {
|
|
|
910
925
|
const topicKey = this.releaseSubscription(topic);
|
|
911
926
|
if (topicKey && !this.assignedTopics.has(topicKey)) this.knownTopics.delete(topicKey);
|
|
912
927
|
}
|
|
913
|
-
/** Remove this
|
|
928
|
+
/** Remove this Runtime's subscriber record and, when it was the last one, delete
|
|
914
929
|
* the route. Returns the topicKey (so callers like `unsubscribe` can reuse
|
|
915
930
|
* it instead of re-hashing the topic to evict the reverse cache). */
|
|
916
931
|
releaseSubscription(topic, notifyOwner = true) {
|
|
917
932
|
const topicKey = this.rememberTopic(topic);
|
|
918
|
-
this.removeStorage(this.subscriberStorageKey(topicKey, this.
|
|
933
|
+
this.removeStorage(this.subscriberStorageKey(topicKey, this.workerId));
|
|
919
934
|
const route = this.readRoute(topicKey);
|
|
920
935
|
if (!route) return topicKey;
|
|
921
936
|
const subscribers = this.readSubscriberTabIds(topicKey, this.readWorkers());
|
|
@@ -1357,11 +1372,7 @@ var WorkerClusterRuntime = class {
|
|
|
1357
1372
|
) ?? this.currentRecord;
|
|
1358
1373
|
recoveryProjectedLoads.set(owner.workerId, (recoveryProjectedLoads.get(owner.workerId) ?? owner.load) + 1);
|
|
1359
1374
|
if (owner.workerId !== this.workerId) {
|
|
1360
|
-
|
|
1361
|
-
const ownerSubscribed = workers.some(
|
|
1362
|
-
(worker) => worker.workerId === owner.workerId && subscriberTabIds.has(worker.tabId)
|
|
1363
|
-
);
|
|
1364
|
-
if (ownerSubscribed) continue;
|
|
1375
|
+
if (this.readSubscriberIds(topicKey, workers).workerIds.has(owner.workerId)) continue;
|
|
1365
1376
|
}
|
|
1366
1377
|
this.writeRoute(topicKey, owner, void 0, route.generation + 1);
|
|
1367
1378
|
this.sendControl(owner.workerId, CONTROL_ACTION.SUBSCRIBE, topic, topicKey);
|
|
@@ -1482,6 +1493,38 @@ var WorkerClusterRuntime = class {
|
|
|
1482
1493
|
return false;
|
|
1483
1494
|
}
|
|
1484
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Whether any other live Worker record already carries `tabId`.
|
|
1498
|
+
*
|
|
1499
|
+
* Used once per document, from the constructor, to tell a browser-duplicated
|
|
1500
|
+
* tab from this tab's own refresh: the copy inherits `sessionStorage` but not
|
|
1501
|
+
* the opener, so the identity stored there is the only shared state and the
|
|
1502
|
+
* Worker records are the only evidence that another live document holds it.
|
|
1503
|
+
* Liveness is the same heartbeat window `readWorkers()` prunes by, so a record
|
|
1504
|
+
* left behind by an abnormal exit is reported as live until it expires — the
|
|
1505
|
+
* only false positive, and it re-mints one identity instead of letting two
|
|
1506
|
+
* live tabs share one.
|
|
1507
|
+
*
|
|
1508
|
+
* The `!this.storage` leg is the degraded mode, and it earns its line for two
|
|
1509
|
+
* reasons rather than one. It is what narrows the field for the scan below
|
|
1510
|
+
* (`StorageLike | null` is rejected by `readAllByPrefix`), so the compiler
|
|
1511
|
+
* holds it; and it is *not* what decides the verdict, because `listKeys()`
|
|
1512
|
+
* catches its own failure and returns no records either way — measured by
|
|
1513
|
+
* emptying this consequent, which leaves the suite green. What the inverted
|
|
1514
|
+
* return does prove is the behaviour, not the deletion: returning `true` here
|
|
1515
|
+
* fails 'keeps the inherited id when there is no storage to probe', since
|
|
1516
|
+
* without `localStorage` there is no second document to collide with.
|
|
1517
|
+
*/
|
|
1518
|
+
isTabIdLiveElsewhere(tabId) {
|
|
1519
|
+
if (!this.storage) return false;
|
|
1520
|
+
const now = this.nowMs();
|
|
1521
|
+
for (const { value: worker } of readAllByPrefix(this.storage, this.workerPrefix)) {
|
|
1522
|
+
if (worker.tabId !== tabId) continue;
|
|
1523
|
+
if (now - worker.heartbeatAt > this.workerTtlMs) continue;
|
|
1524
|
+
return true;
|
|
1525
|
+
}
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1485
1528
|
/** Read all live worker records from storage, pruning stale entries past the TTL. */
|
|
1486
1529
|
readWorkers() {
|
|
1487
1530
|
if (!this.storage) return [this.currentRecord];
|
|
@@ -1499,23 +1542,61 @@ var WorkerClusterRuntime = class {
|
|
|
1499
1542
|
}
|
|
1500
1543
|
/** Enumerate all tab IDs that have a subscriber record for `topicKey`. */
|
|
1501
1544
|
readSubscriberTabIds(topicKey, workers) {
|
|
1545
|
+
return Array.from(this.readSubscriberIds(topicKey, workers).tabIds);
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* One scan of `topicKey`'s subscriber records, answering the two questions
|
|
1549
|
+
* callers ask: which Tabs hold a subscription (emptiness checks, orphan-route
|
|
1550
|
+
* pruning) and which **Runtimes** do (the stranded-handoff re-election).
|
|
1551
|
+
*
|
|
1552
|
+
* The second answer is the finer one and the reason this is one method rather
|
|
1553
|
+
* than two scans: a tab-level answer lets a leftover record from a sibling
|
|
1554
|
+
* Runtime speak for a Worker that never subscribed to the topic, and standing
|
|
1555
|
+
* down on that proof stalls the recovery forever (see the call site).
|
|
1556
|
+
*
|
|
1557
|
+
* A legacy record — written before records carried `workerId` — proves only
|
|
1558
|
+
* that *some* Runtime in its tab subscribes, so it contributes to the tab set
|
|
1559
|
+
* and **not** to the worker set. Treating it as the whole tab, which is what
|
|
1560
|
+
* the tab-level reader effectively did, is the conservative-looking choice
|
|
1561
|
+
* that can stall; the cost of the other direction is one redundant
|
|
1562
|
+
* `CONTROL/SUBSCRIBE` to a peer that is about to confirm anyway.
|
|
1563
|
+
*/
|
|
1564
|
+
readSubscriberIds(topicKey, workers) {
|
|
1565
|
+
const tabIds = /* @__PURE__ */ new Set();
|
|
1566
|
+
const workerIds = /* @__PURE__ */ new Set();
|
|
1502
1567
|
if (!this.storage) {
|
|
1503
1568
|
const topic = this.knownTopics.get(topicKey);
|
|
1504
|
-
|
|
1569
|
+
if (topic && this.subscribedTopics.has(topic)) {
|
|
1570
|
+
tabIds.add(this.tabId);
|
|
1571
|
+
workerIds.add(this.workerId);
|
|
1572
|
+
}
|
|
1573
|
+
return { tabIds, workerIds };
|
|
1505
1574
|
}
|
|
1506
1575
|
const activeTabIds = new Set(workers.map((worker) => worker.tabId));
|
|
1507
|
-
const
|
|
1576
|
+
const activeWorkerIds = new Set(workers.map((worker) => worker.workerId));
|
|
1508
1577
|
for (const { key, value: record } of readAllByPrefix(
|
|
1509
1578
|
this.storage,
|
|
1510
1579
|
`${this.subscriberPrefix}${topicKey}:`
|
|
1511
1580
|
)) {
|
|
1512
|
-
if (!
|
|
1581
|
+
if (!this.isSubscriberRecordLive(record, activeTabIds, activeWorkerIds)) {
|
|
1513
1582
|
this.removeStorage(key);
|
|
1514
1583
|
continue;
|
|
1515
1584
|
}
|
|
1516
|
-
|
|
1585
|
+
tabIds.add(record.tabId);
|
|
1586
|
+
if (record.workerId !== void 0) workerIds.add(record.workerId);
|
|
1517
1587
|
}
|
|
1518
|
-
return
|
|
1588
|
+
return { tabIds, workerIds };
|
|
1589
|
+
}
|
|
1590
|
+
/** Whether a subscriber record still belongs to a live Runtime.
|
|
1591
|
+
*
|
|
1592
|
+
* A record written by this version names its `workerId`, so liveness is the
|
|
1593
|
+
* Worker set the reconcile pass already prunes by — which is what keeps two
|
|
1594
|
+
* Runtimes in one tab independent. A record from an older version carries no
|
|
1595
|
+
* `workerId`; it is honoured while its *tab* lives, the finest identity it
|
|
1596
|
+
* has, and each Runtime replaces its own with a worker-keyed record on its
|
|
1597
|
+
* next write. */
|
|
1598
|
+
isSubscriberRecordLive(record, activeTabIds, activeWorkerIds) {
|
|
1599
|
+
return record.workerId === void 0 ? activeTabIds.has(record.tabId) : activeWorkerIds.has(record.workerId);
|
|
1519
1600
|
}
|
|
1520
1601
|
/** Read the current route for `topicKey`. With a storage layer this returns
|
|
1521
1602
|
* the durable route, or null when none is recorded. Without one (degraded
|
|
@@ -1581,19 +1662,31 @@ var WorkerClusterRuntime = class {
|
|
|
1581
1662
|
this.removeStorage(key);
|
|
1582
1663
|
}
|
|
1583
1664
|
}
|
|
1584
|
-
/** Remove subscriber records
|
|
1665
|
+
/** Remove subscriber records whose owning Runtime (or, for a legacy record,
|
|
1666
|
+
* whose tab) is no longer active. */
|
|
1585
1667
|
cleanupOrphanedSubscribers(workers) {
|
|
1586
1668
|
if (!this.storage) return;
|
|
1587
1669
|
const activeTabIds = new Set(workers.map((worker) => worker.tabId));
|
|
1670
|
+
const activeWorkerIds = new Set(workers.map((worker) => worker.workerId));
|
|
1588
1671
|
for (const { key, value: record } of readAllByPrefix(this.storage, this.subscriberPrefix)) {
|
|
1589
|
-
if (!
|
|
1672
|
+
if (!this.isSubscriberRecordLive(record, activeTabIds, activeWorkerIds)) this.removeStorage(key);
|
|
1590
1673
|
}
|
|
1591
1674
|
}
|
|
1592
|
-
/** Persist a subscriber record for this
|
|
1675
|
+
/** Persist a subscriber record for this Runtime on `topicKey`.
|
|
1676
|
+
*
|
|
1677
|
+
* Keyed by `workerId`, not `tabId`. A document may run several Runtimes that
|
|
1678
|
+
* share one `tabId` on purpose (the market + notice pattern), and a tab-keyed
|
|
1679
|
+
* record belonged to all of them at once: whichever unsubscribed first deleted
|
|
1680
|
+
* the others' intent, the route then looked orphaned, and the owner dropped a
|
|
1681
|
+
* transport subscription its surviving sibling still needed — a delivery gap
|
|
1682
|
+
* until that sibling's next reconcile tick re-elected an owner. Duplicated
|
|
1683
|
+
* tabs get the same protection even when the identity probe cannot see them,
|
|
1684
|
+
* which is why the fix is here as well as in `isTabIdLiveElsewhere()`. */
|
|
1593
1685
|
writeSubscriber(topicKey) {
|
|
1594
1686
|
if (!this.storage) return;
|
|
1595
|
-
writeJson(this.storage, this.subscriberStorageKey(topicKey, this.
|
|
1687
|
+
writeJson(this.storage, this.subscriberStorageKey(topicKey, this.workerId), {
|
|
1596
1688
|
tabId: this.tabId,
|
|
1689
|
+
workerId: this.workerId,
|
|
1597
1690
|
updatedAt: this.nowMs()
|
|
1598
1691
|
});
|
|
1599
1692
|
}
|
|
@@ -1667,8 +1760,9 @@ var WorkerClusterRuntime = class {
|
|
|
1667
1760
|
routeStorageKey(topicKey) {
|
|
1668
1761
|
return `${this.routePrefix}${topicKey}`;
|
|
1669
1762
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1763
|
+
/** `ownerId` is the Runtime's `workerId`; older versions used the `tabId`. */
|
|
1764
|
+
subscriberStorageKey(topicKey, ownerId) {
|
|
1765
|
+
return `${this.subscriberPrefix}${topicKey}:${ownerId}`;
|
|
1672
1766
|
}
|
|
1673
1767
|
removeStorage(key) {
|
|
1674
1768
|
try {
|
|
@@ -2548,7 +2642,7 @@ var SDK_VERSION = (
|
|
|
2548
2642
|
// declaration's type is `string | undefined` and the export's is `string`
|
|
2549
2643
|
// (dropping the conditional is TS2322, measured). Recorded here so the leg reads
|
|
2550
2644
|
// as a shape the harness cannot reach rather than as a case nobody exercised.
|
|
2551
|
-
true ? "0.21.
|
|
2645
|
+
true ? "0.21.57" : ""
|
|
2552
2646
|
);
|
|
2553
2647
|
|
|
2554
2648
|
// src/core/data-bus.ts
|
|
@@ -3801,4 +3895,4 @@ export {
|
|
|
3801
3895
|
parseDataBusPublication,
|
|
3802
3896
|
selectWorkerBackend
|
|
3803
3897
|
};
|
|
3804
|
-
//# sourceMappingURL=chunk-
|
|
3898
|
+
//# sourceMappingURL=chunk-ZH7VP3IY.js.map
|