@aglyn/tenant-feature-instance 1.0.0-beta.250 → 1.0.0-beta.253
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-feature-instance",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.253",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.253",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.253",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.253",
|
|
31
|
+
"@aglyn/shared-util-email": "1.0.0-beta.253",
|
|
32
|
+
"@aglyn/shared-util-http": "1.0.0-beta.253",
|
|
33
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.253",
|
|
34
34
|
"@swc/helpers": "0.5.23",
|
|
35
35
|
"lodash-es": "^4.18.1"
|
|
36
36
|
},
|
|
@@ -61,21 +61,35 @@ import { type Firestore } from 'firebase/firestore';
|
|
|
61
61
|
* tab.
|
|
62
62
|
* 3. In ONE IndexedDB transaction over `owner`, `clientMetadata` and
|
|
63
63
|
* `mutations`, decide with {@link decideLeaseClaim}. When the holder is
|
|
64
|
-
* another tab that has recorded itself as hidden
|
|
65
|
-
* write this tab's row as foreground and the
|
|
64
|
+
* another tab that has recorded itself as hidden, its lease has LAPSED
|
|
65
|
+
* and no write is pending, write this tab's row as foreground and the
|
|
66
|
+
* lease in this tab's name.
|
|
66
67
|
* 4. Cycle this tab's network (`disableNetwork` then `enableNetwork`): each
|
|
67
68
|
* step runs the SDK's own election at once, and the second finds a valid
|
|
68
69
|
* lease that is this tab's and becomes primary.
|
|
69
70
|
*
|
|
70
71
|
* ## Why this is safe
|
|
71
72
|
*
|
|
72
|
-
* - **
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* - **Never from a live holder (AGL-3788).** A lease renewed under 5 s ago
|
|
74
|
+
* belongs to a tab whose remote store is running: its Watch stream keeps
|
|
75
|
+
* applying events and resuming targets until its next 4 s tick notices the
|
|
76
|
+
* lease is gone. Taking that lease makes two tabs primary over one cache,
|
|
77
|
+
* and their target bookkeeping diverges — a resumed document target that
|
|
78
|
+
* comes back `current` without its key in this cache's mapping is written
|
|
79
|
+
* as a NoDocument, so a live host or org reads as deleted, and the
|
|
80
|
+
* tombstone outlives reloads because a resumed listen only pulls deltas.
|
|
81
|
+
* That is what the Sites page's "No sites yet" and an Enterprise org
|
|
82
|
+
* showing Free were, in a browser with several console tabs. The SDK never
|
|
83
|
+
* takes a valid lease (`canActAsPrimary`), and neither does this.
|
|
84
|
+
* - **It is the SDK's own handoff.** A lapsed lease is one the SDK's next
|
|
85
|
+
* election would hand to a foreground tab anyway; this only stops the
|
|
86
|
+
* hidden holder winning it back on a stale `inForeground: false` row. A
|
|
87
|
+
* holder that then finds the lease in another tab's name fails its next
|
|
88
|
+
* `readwrite-primary` transaction (`verifyPrimaryLease`), flips to
|
|
89
|
+
* secondary and throws the `failed-precondition` its sync engine already
|
|
90
|
+
* swallows (`ignoreIfPrimaryLeaseLoss`). IndexedDB serialises our
|
|
91
|
+
* transaction against the SDK's (which span every store), so no election
|
|
92
|
+
* interleaves with it.
|
|
79
93
|
* - **The old holder cannot take it back.** The lease is valid and not its
|
|
80
94
|
* own, so its `canActAsPrimary` is `false`. Should this tab fail to renew,
|
|
81
95
|
* the lease lapses in 5 s and the corrected `inForeground: true` row makes
|
|
@@ -93,8 +107,8 @@ import { type Firestore } from 'firebase/firestore';
|
|
|
93
107
|
* same one `firestore-stall-recovery.ts` runs.
|
|
94
108
|
* - **Every failure is the old behaviour.** Anything unexpected — no
|
|
95
109
|
* database, the store locked (the AGL-3428 wedge), two new tabs at once, a
|
|
96
|
-
* holder that is visible
|
|
97
|
-
* as it always has.
|
|
110
|
+
* holder that is visible or whose lease is still live — skips the claim,
|
|
111
|
+
* and the SDK's own election runs as it always has.
|
|
98
112
|
*
|
|
99
113
|
* Not chosen: `persistentSingleTabManager` (rejected in AGL-3373/3428, and
|
|
100
114
|
* `forceOwnership` there fails every other tab with
|
|
@@ -126,7 +140,7 @@ export interface ClientRow {
|
|
|
126
140
|
inForeground: boolean;
|
|
127
141
|
}
|
|
128
142
|
/** Why the claim was not made. */
|
|
129
|
-
export type LeaseClaimSkip = 'unidentified' | 'vacant' | 'already-primary' | 'exclusive-holder' | 'holder-unknown' | 'holder-visible' | 'pending-writes';
|
|
143
|
+
export type LeaseClaimSkip = 'unidentified' | 'vacant' | 'already-primary' | 'exclusive-holder' | 'holder-unknown' | 'holder-visible' | 'holder-live' | 'pending-writes';
|
|
130
144
|
export type LeaseClaimDecision = {
|
|
131
145
|
claim: true;
|
|
132
146
|
} | {
|
|
@@ -138,14 +152,17 @@ export type LeaseClaimDecision = {
|
|
|
138
152
|
* caller reads every input inside the one transaction that writes the result.
|
|
139
153
|
*
|
|
140
154
|
* A vacant or lapsed-and-vacant lease is left to the SDK, whose next election
|
|
141
|
-
* takes it; only a lease another tab holds is ever claimed,
|
|
142
|
-
* holder whose own row says it is in the background
|
|
155
|
+
* takes it; only a lease another tab holds is ever claimed, only from a
|
|
156
|
+
* holder whose own row says it is in the background, and only once that
|
|
157
|
+
* lease has lapsed — a live one would leave two tabs running as primary.
|
|
143
158
|
*/
|
|
144
159
|
export declare function decideLeaseClaim(input: {
|
|
145
160
|
owner: LeaseRow | undefined;
|
|
146
161
|
holder: ClientRow | undefined;
|
|
147
162
|
self: ClientRow | undefined;
|
|
148
163
|
pendingWrites: number;
|
|
164
|
+
/** The clock the lease is measured against, in ms. */
|
|
165
|
+
now: number;
|
|
149
166
|
}): LeaseClaimDecision;
|
|
150
167
|
/** What the boot handoff did. */
|
|
151
168
|
export type TabHandoffOutcome = {
|
|
@@ -61,21 +61,35 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
61
61
|
* tab.
|
|
62
62
|
* 3. In ONE IndexedDB transaction over `owner`, `clientMetadata` and
|
|
63
63
|
* `mutations`, decide with {@link decideLeaseClaim}. When the holder is
|
|
64
|
-
* another tab that has recorded itself as hidden
|
|
65
|
-
* write this tab's row as foreground and the
|
|
64
|
+
* another tab that has recorded itself as hidden, its lease has LAPSED
|
|
65
|
+
* and no write is pending, write this tab's row as foreground and the
|
|
66
|
+
* lease in this tab's name.
|
|
66
67
|
* 4. Cycle this tab's network (`disableNetwork` then `enableNetwork`): each
|
|
67
68
|
* step runs the SDK's own election at once, and the second finds a valid
|
|
68
69
|
* lease that is this tab's and becomes primary.
|
|
69
70
|
*
|
|
70
71
|
* ## Why this is safe
|
|
71
72
|
*
|
|
72
|
-
* - **
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* - **Never from a live holder (AGL-3788).** A lease renewed under 5 s ago
|
|
74
|
+
* belongs to a tab whose remote store is running: its Watch stream keeps
|
|
75
|
+
* applying events and resuming targets until its next 4 s tick notices the
|
|
76
|
+
* lease is gone. Taking that lease makes two tabs primary over one cache,
|
|
77
|
+
* and their target bookkeeping diverges — a resumed document target that
|
|
78
|
+
* comes back `current` without its key in this cache's mapping is written
|
|
79
|
+
* as a NoDocument, so a live host or org reads as deleted, and the
|
|
80
|
+
* tombstone outlives reloads because a resumed listen only pulls deltas.
|
|
81
|
+
* That is what the Sites page's "No sites yet" and an Enterprise org
|
|
82
|
+
* showing Free were, in a browser with several console tabs. The SDK never
|
|
83
|
+
* takes a valid lease (`canActAsPrimary`), and neither does this.
|
|
84
|
+
* - **It is the SDK's own handoff.** A lapsed lease is one the SDK's next
|
|
85
|
+
* election would hand to a foreground tab anyway; this only stops the
|
|
86
|
+
* hidden holder winning it back on a stale `inForeground: false` row. A
|
|
87
|
+
* holder that then finds the lease in another tab's name fails its next
|
|
88
|
+
* `readwrite-primary` transaction (`verifyPrimaryLease`), flips to
|
|
89
|
+
* secondary and throws the `failed-precondition` its sync engine already
|
|
90
|
+
* swallows (`ignoreIfPrimaryLeaseLoss`). IndexedDB serialises our
|
|
91
|
+
* transaction against the SDK's (which span every store), so no election
|
|
92
|
+
* interleaves with it.
|
|
79
93
|
* - **The old holder cannot take it back.** The lease is valid and not its
|
|
80
94
|
* own, so its `canActAsPrimary` is `false`. Should this tab fail to renew,
|
|
81
95
|
* the lease lapses in 5 s and the corrected `inForeground: true` row makes
|
|
@@ -93,8 +107,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
93
107
|
* same one `firestore-stall-recovery.ts` runs.
|
|
94
108
|
* - **Every failure is the old behaviour.** Anything unexpected — no
|
|
95
109
|
* database, the store locked (the AGL-3428 wedge), two new tabs at once, a
|
|
96
|
-
* holder that is visible
|
|
97
|
-
* as it always has.
|
|
110
|
+
* holder that is visible or whose lease is still live — skips the claim,
|
|
111
|
+
* and the SDK's own election runs as it always has.
|
|
98
112
|
*
|
|
99
113
|
* Not chosen: `persistentSingleTabManager` (rejected in AGL-3373/3428, and
|
|
100
114
|
* `forceOwnership` there fails every other tab with
|
|
@@ -118,10 +132,11 @@ const MUTATIONS_STORE = 'mutations';
|
|
|
118
132
|
* caller reads every input inside the one transaction that writes the result.
|
|
119
133
|
*
|
|
120
134
|
* A vacant or lapsed-and-vacant lease is left to the SDK, whose next election
|
|
121
|
-
* takes it; only a lease another tab holds is ever claimed,
|
|
122
|
-
* holder whose own row says it is in the background
|
|
135
|
+
* takes it; only a lease another tab holds is ever claimed, only from a
|
|
136
|
+
* holder whose own row says it is in the background, and only once that
|
|
137
|
+
* lease has lapsed — a live one would leave two tabs running as primary.
|
|
123
138
|
*/ export function decideLeaseClaim(input) {
|
|
124
|
-
const { owner, holder, self, pendingWrites } = input;
|
|
139
|
+
const { owner, holder, self, pendingWrites, now } = input;
|
|
125
140
|
if (!self) return {
|
|
126
141
|
claim: false,
|
|
127
142
|
reason: 'unidentified'
|
|
@@ -152,6 +167,12 @@ const MUTATIONS_STORE = 'mutations';
|
|
|
152
167
|
claim: false,
|
|
153
168
|
reason: 'holder-visible'
|
|
154
169
|
};
|
|
170
|
+
// `MAX_PRIMARY_ELIGIBLE_AGE_MS`: the SDK's lease is valid while younger
|
|
171
|
+
// than this, and a timestamp ahead of our clock is younger still.
|
|
172
|
+
if (now - owner.leaseTimestampMs < PRIMARY_LEASE_MAX_AGE_MS) return {
|
|
173
|
+
claim: false,
|
|
174
|
+
reason: 'holder-live'
|
|
175
|
+
};
|
|
155
176
|
return {
|
|
156
177
|
claim: true
|
|
157
178
|
};
|
|
@@ -353,11 +374,13 @@ const escapeRegExp = (text)=>text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
353
374
|
// Requests in one transaction complete in order: both gets landed.
|
|
354
375
|
const self = selfRequest == null ? void 0 : selfRequest.result;
|
|
355
376
|
const holder = holderRequest == null ? void 0 : holderRequest.result;
|
|
377
|
+
const at = now();
|
|
356
378
|
const decision = decideLeaseClaim({
|
|
357
379
|
owner,
|
|
358
380
|
holder,
|
|
359
381
|
self,
|
|
360
|
-
pendingWrites: countRequest.result
|
|
382
|
+
pendingWrites: countRequest.result,
|
|
383
|
+
now: at
|
|
361
384
|
});
|
|
362
385
|
if ('reason' in decision) {
|
|
363
386
|
result = {
|
|
@@ -366,7 +389,6 @@ const escapeRegExp = (text)=>text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
366
389
|
};
|
|
367
390
|
return;
|
|
368
391
|
}
|
|
369
|
-
const at = now();
|
|
370
392
|
// The row the SDK's first election should have written, then the
|
|
371
393
|
// lease in this tab's name — `acquireOrExtendPrimaryLease`'s shape.
|
|
372
394
|
clients.put(_extends({}, self, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firestore-tab-handoff.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n disableNetwork,\n enableNetwork,\n type Firestore,\n} from 'firebase/firestore'\n\n/**\n * A NEWLY OPENED, VISIBLE TAB TAKES THE PRIMARY LEASE FROM A HIDDEN ONE\n * (AGL-3660).\n *\n * ## What the SDK does on its own\n *\n * With `persistentMultipleTabManager`, exactly one tab per origin — the\n * holder of the `owner` row in `firestore/<app>/<project>/main` — runs the\n * network. Every other tab sends its queries to that tab through\n * `localStorage` and reads the answers back out of IndexedDB. Read against\n * `@firebase/firestore` 4.17.2 (`IndexedDbPersistence`):\n *\n * - **A valid lease is never handed over.** `canActAsPrimary` returns `true`\n * for the holder whenever its own lease is under 5 s old\n * (`MAX_PRIMARY_ELIGIBLE_AGE_MS`) and its network is enabled, whatever its\n * visibility. The \"prefer the foreground tab\" rule only applies once the\n * lease has lapsed. A console tab the user left in the background therefore\n * keeps serving the tab they are looking at, for as long as its 4 s renewal\n * timer keeps firing.\n * - **A background holder is a slow server.** Its renewal timer is throttled\n * (production lease rows, 2026-10-08: renewed every ~4.9 s, against a 5 s\n * lapse), and every query the new tab issues waits on that tab's event loop.\n * The workspace read on a fresh `/aglyn-org/media` tab came back only once\n * the new tab had won the lease on a lapse, its own Listen channel opening\n * ~4.7 s in (PR #1313).\n * - **The new tab's first record says it is hidden.** `start()` runs its\n * first election (`updateClientMetadataAndTryBecomePrimary`) before\n * `attachVisibilityHandler()` has read `document.visibilityState`, so the\n * `clientMetadata` row it writes carries `inForeground: false` until the\n * next 4 s tick. When the holder's lease does lapse in that window, the\n * holder finds no better candidate and takes it straight back.\n *\n * ## What this does\n *\n * Once, at boot, in a tab that is visible and online:\n *\n * 1. Snapshot the client ids already registered, synchronously, in the same\n * task as `initializeFirestore` — before anything can start this tab's\n * client. They come from the SDK's `firestore_clients_<prefix>_<id>` keys\n * in `localStorage`, which a tab writes once its persistence has started.\n * 2. Start this tab's Firestore (`enableNetwork` — a no-op on a client whose\n * network is on — resolves once persistence has started and the tab has\n * run its first election). The one id that was not there before is this\n * tab.\n * 3. In ONE IndexedDB transaction over `owner`, `clientMetadata` and\n * `mutations`, decide with {@link decideLeaseClaim}. When the holder is\n * another tab that has recorded itself as hidden and no write is pending,\n * write this tab's row as foreground and the lease in this tab's name.\n * 4. Cycle this tab's network (`disableNetwork` then `enableNetwork`): each\n * step runs the SDK's own election at once, and the second finds a valid\n * lease that is this tab's and becomes primary.\n *\n * ## Why this is safe\n *\n * - **It is the SDK's own handoff.** A holder that finds the lease in another\n * tab's name is exactly a holder whose lease lapsed: every\n * `readwrite-primary` transaction re-verifies the lease inside the\n * transaction (`verifyPrimaryLease`), fails, flips the tab to secondary and\n * throws the `failed-precondition` its sync engine already swallows\n * (`ignoreIfPrimaryLeaseLoss`). IndexedDB serialises our transaction against\n * the SDK's (which span every store), so no election interleaves with it.\n * - **The old holder cannot take it back.** The lease is valid and not its\n * own, so its `canActAsPrimary` is `false`. Should this tab fail to renew,\n * the lease lapses in 5 s and the corrected `inForeground: true` row makes\n * this tab the preferred candidate, not the hidden one.\n * - **Never with a write in flight.** The holder sends pending mutations; a\n * new primary resends every unacknowledged batch. Any row in `mutations`\n * skips the claim, so a write is never sent twice by two tabs.\n * - **Never from a visible holder.** A tab visible in another window serves\n * quickly (measured in production: ~390 ms from a query target written to\n * it being `current`), and taking its lease would only move the work.\n * - **The cycle is invisible in a secondary.** The sync engine ignores its\n * own remote store's online state while secondary\n * (`syncEngineApplyOnlineStateChange`), so no listener sees an offline\n * flicker. If this tab somehow became primary in between, the cycle is the\n * same one `firestore-stall-recovery.ts` runs.\n * - **Every failure is the old behaviour.** Anything unexpected — no\n * database, the store locked (the AGL-3428 wedge), two new tabs at once, a\n * holder that is visible — skips the claim, and the SDK's own election runs\n * as it always has.\n *\n * Not chosen: `persistentSingleTabManager` (rejected in AGL-3373/3428, and\n * `forceOwnership` there fails every other tab with\n * `PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG`); `getDocFromServer` (a secondary routes\n * it through the holder like any listen); a per-tab memory cache for the first\n * read (the cache is fixed at `initializeFirestore`); a synthetic\n * `visibilitychange` to rerun the election (also fires the console's own\n * twenty-odd visibility listeners).\n */\n\n/** The SDK's lease lifetime (`MAX_PRIMARY_ELIGIBLE_AGE_MS`). */\nexport const PRIMARY_LEASE_MAX_AGE_MS = 5_000\n\n/**\n * How long the claim transaction may wait to start. The SDK's own take\n * milliseconds; one that cannot start is behind the AGL-3428 wedge, which\n * `firestore-multitab-wedge.ts` handles.\n */\nexport const CLAIM_TIMEOUT_MS = 1_500\n\n/** The SDK's stores and the lease row's key (`indexeddb_sentinels.ts`). */\nconst OWNER_STORE = 'owner'\nconst OWNER_KEY = 'owner'\nconst CLIENT_METADATA_STORE = 'clientMetadata'\nconst MUTATIONS_STORE = 'mutations'\n\n/** `DbPrimaryClient`. */\nexport interface LeaseRow {\n ownerId: string\n allowTabSynchronization: boolean\n leaseTimestampMs: number\n}\n\n/** `DbClientMetadata`. */\nexport interface ClientRow {\n clientId: string\n updateTimeMs: number\n networkEnabled: boolean\n inForeground: boolean\n}\n\n/** Why the claim was not made. */\nexport type LeaseClaimSkip =\n | 'unidentified'\n | 'vacant'\n | 'already-primary'\n | 'exclusive-holder'\n | 'holder-unknown'\n | 'holder-visible'\n | 'pending-writes'\n\nexport type LeaseClaimDecision =\n { claim: true } | { claim: false; reason: LeaseClaimSkip }\n\n/**\n * Whether this tab (`self`) should take the lease `owner` holds. Pure: the\n * caller reads every input inside the one transaction that writes the result.\n *\n * A vacant or lapsed-and-vacant lease is left to the SDK, whose next election\n * takes it; only a lease another tab holds is ever claimed, and only from a\n * holder whose own row says it is in the background.\n */\nexport function decideLeaseClaim(input: {\n owner: LeaseRow | undefined\n holder: ClientRow | undefined\n self: ClientRow | undefined\n pendingWrites: number\n}): LeaseClaimDecision {\n const { owner, holder, self, pendingWrites } = input\n if (!self) return { claim: false, reason: 'unidentified' }\n if (!owner) return { claim: false, reason: 'vacant' }\n if (owner.ownerId === self.clientId)\n return { claim: false, reason: 'already-primary' }\n // A single-tab holder never shares; `persistentMultipleTabManager` never\n // writes `false`, so this is another configuration on the origin.\n if (!owner.allowTabSynchronization)\n return { claim: false, reason: 'exclusive-holder' }\n if (pendingWrites > 0) return { claim: false, reason: 'pending-writes' }\n if (!holder) return { claim: false, reason: 'holder-unknown' }\n if (holder.inForeground) return { claim: false, reason: 'holder-visible' }\n return { claim: true }\n}\n\n/** What the boot handoff did. */\nexport type TabHandoffOutcome =\n | { result: 'claimed'; fromOwnerId: string; leaseAgeMs: number }\n | {\n result: 'skipped'\n reason:\n | LeaseClaimSkip\n | 'hidden'\n | 'offline'\n | 'alone'\n | 'no-database'\n | 'locked'\n | 'error'\n }\n\n/** What one claim transaction found and did. */\nexport type ClaimTransactionResult =\n | { state: 'claimed'; fromOwnerId: string; leaseAgeMs: number }\n | { state: 'skipped'; reason: LeaseClaimSkip }\n | { state: 'locked' }\n | { state: 'unavailable' }\n\nexport interface TabHandoffDeps {\n isVisible: () => boolean\n isOnline: () => boolean\n /**\n * Client ids registered on this origin. Must be synchronous: the first call\n * is the snapshot taken before this tab's client can start.\n */\n readClientIds: () => ReadonlySet<string>\n /** Starts this tab's Firestore; resolves once its persistence has started. */\n startClient: () => Promise<void>\n /** Runs the claim for `selfId` in one transaction. */\n claim: (selfId: string | undefined) => Promise<ClaimTransactionResult>\n /** `disableNetwork` then `enableNetwork`: two elections, at once. */\n cycleNetwork: () => Promise<void>\n}\n\n/**\n * The boot handoff, with its browser parts injected. Never rejects.\n */\nexport async function runTabHandoff(\n deps: TabHandoffDeps,\n): Promise<TabHandoffOutcome> {\n try {\n if (!deps.isVisible()) return { result: 'skipped', reason: 'hidden' }\n if (!deps.isOnline()) return { result: 'skipped', reason: 'offline' }\n // Before this tab's Firestore starts, or this tab would be in it.\n const before = deps.readClientIds()\n // No other tab: this one wins the lease at its first election.\n if (before.size === 0) return { result: 'skipped', reason: 'alone' }\n await deps.startClient()\n // The user may have switched away while the client started.\n if (!deps.isVisible()) return { result: 'skipped', reason: 'hidden' }\n const added = [...deps.readClientIds()].filter((id) => !before.has(id))\n // Exactly one new client is this tab. None means this tab's Firestore\n // started before the snapshot; two means another tab started alongside.\n // Either way this tab cannot be told apart, so nothing is claimed.\n if (added.length !== 1) return { result: 'skipped', reason: 'unidentified' }\n const selfId = added[0]\n const outcome = await deps.claim(selfId)\n if (outcome.state === 'locked')\n return { result: 'skipped', reason: 'locked' }\n if (outcome.state === 'unavailable')\n return { result: 'skipped', reason: 'no-database' }\n if (outcome.state === 'skipped')\n return { result: 'skipped', reason: outcome.reason }\n await deps.cycleNetwork()\n return {\n result: 'claimed',\n fromOwnerId: outcome.fromOwnerId,\n leaseAgeMs: outcome.leaseAgeMs,\n }\n } catch {\n return { result: 'skipped', reason: 'error' }\n }\n}\n\n/**\n * Opens `databaseName` only if it already exists, at its current version —\n * never creating it under the SDK (the rule `probePrimaryLease` follows) —\n * and closes on `versionchange` so an SDK upgrade is never held up.\n */\nasync function openExisting(\n factory: IDBFactory,\n databaseName: string,\n): Promise<IDBDatabase | undefined> {\n if (typeof factory.databases !== 'function') return undefined\n const databases = await factory.databases()\n if (!databases.some((database) => database.name === databaseName))\n return undefined\n return new Promise((resolve) => {\n let request: IDBOpenDBRequest\n try {\n request = factory.open(databaseName)\n } catch {\n resolve(undefined)\n return\n }\n request.onupgradeneeded = () => request.transaction?.abort()\n request.onerror = () => resolve(undefined)\n request.onblocked = () => resolve(undefined)\n request.onsuccess = () => {\n const database = request.result\n database.onversionchange = () => database.close()\n resolve(database)\n }\n })\n}\n\nconst hasStores = (database: IDBDatabase, stores: string[]) =>\n stores.every((store) => database.objectStoreNames.contains(store))\n\nconst escapeRegExp = (text: string) =>\n text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/**\n * The client ids the SDK has registered under `prefix`\n * (`firestore/<app>/<project>/`), from its `firestore_clients_<prefix>_<id>`\n * keys. Synchronous, so a snapshot can be taken before this tab's client\n * starts. A key whose tab has since closed is harmless here: only ids that\n * APPEAR between two snapshots are read as this tab.\n */\nexport function readSharedClientIds(\n storage: Pick<Storage, 'length' | 'key'> | undefined,\n prefix: string,\n): ReadonlySet<string> {\n const ids = new Set<string>()\n if (!storage) return ids\n // The SDK's own key shape: a client id never contains `_`.\n const clientKey = new RegExp(\n `^firestore_clients_${escapeRegExp(prefix)}_([^_]+)$`,\n )\n try {\n for (let i = 0; i < storage.length; i++) {\n const match = clientKey.exec(storage.key(i) ?? '')\n if (match) ids.add(match[1])\n }\n } catch {\n // Site data blocked: no ids, so the handoff stands down.\n }\n return ids\n}\n\n/**\n * Reads the lease, both client rows and the pending-write count, decides, and\n * writes the claim — all in one `readwrite` transaction, so no election runs\n * between the read and the write. Aborted when it cannot start within\n * `timeoutMs`.\n */\nexport async function claimPrimaryLease(\n factory: IDBFactory | undefined,\n databaseName: string,\n selfId: string | undefined,\n {\n timeoutMs = CLAIM_TIMEOUT_MS,\n now = Date.now,\n }: { timeoutMs?: number; now?: () => number } = {},\n): Promise<ClaimTransactionResult> {\n if (!factory) return { state: 'unavailable' }\n const database = await openExisting(factory, databaseName).catch(\n () => undefined,\n )\n if (!database) return { state: 'unavailable' }\n const stores = [OWNER_STORE, CLIENT_METADATA_STORE, MUTATIONS_STORE]\n if (!hasStores(database, stores)) {\n database.close()\n return { state: 'unavailable' }\n }\n return new Promise<ClaimTransactionResult>((resolve) => {\n let transaction: IDBTransaction\n try {\n transaction = database.transaction(stores, 'readwrite')\n } catch {\n database.close()\n resolve({ state: 'unavailable' })\n return\n }\n let settled = false\n let started = false\n let result: ClaimTransactionResult = { state: 'unavailable' }\n // Only a transaction that cannot START is aborted: once the lease read\n // has landed, the decision and its writes take milliseconds.\n const timer = setTimeout(() => {\n if (started) return\n try {\n transaction.abort()\n } catch {\n // Already finished.\n }\n finish({ state: 'locked' })\n }, timeoutMs)\n const finish = (value: ClaimTransactionResult) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n database.close()\n resolve(value)\n }\n transaction.oncomplete = () => finish(result)\n transaction.onabort = () =>\n finish(started ? { state: 'unavailable' } : { state: 'locked' })\n transaction.onerror = () => finish({ state: 'unavailable' })\n\n const owners = transaction.objectStore(OWNER_STORE)\n const clients = transaction.objectStore(CLIENT_METADATA_STORE)\n const ownerRequest = owners.get(OWNER_KEY)\n ownerRequest.onsuccess = () => {\n started = true\n const owner = ownerRequest.result as LeaseRow | undefined\n const selfRequest = selfId === undefined ? undefined : clients.get(selfId)\n const holderRequest = owner?.ownerId\n ? clients.get(owner.ownerId)\n : undefined\n const countRequest = transaction.objectStore(MUTATIONS_STORE).count()\n countRequest.onsuccess = () => {\n // Requests in one transaction complete in order: both gets landed.\n const self = selfRequest?.result as ClientRow | undefined\n const holder = holderRequest?.result as ClientRow | undefined\n const decision = decideLeaseClaim({\n owner,\n holder,\n self,\n pendingWrites: countRequest.result,\n })\n if ('reason' in decision) {\n result = { state: 'skipped', reason: decision.reason }\n return\n }\n const at = now()\n // The row the SDK's first election should have written, then the\n // lease in this tab's name — `acquireOrExtendPrimaryLease`'s shape.\n clients.put({ ...self, inForeground: true, updateTimeMs: at })\n owners.put(\n {\n ownerId: self!.clientId,\n allowTabSynchronization: true,\n leaseTimestampMs: at,\n } satisfies LeaseRow,\n OWNER_KEY,\n )\n result = {\n state: 'claimed',\n fromOwnerId: owner!.ownerId,\n leaseAgeMs: at - owner!.leaseTimestampMs,\n }\n }\n }\n })\n}\n\nconst browserIndexedDb = (): IDBFactory | undefined => {\n try {\n return typeof window === 'undefined' ? undefined : window.indexedDB\n } catch {\n return undefined\n }\n}\n\nconst browserLocalStorage = (): Storage | undefined => {\n try {\n return typeof window === 'undefined' ? undefined : window.localStorage\n } catch {\n return undefined\n }\n}\n\n/**\n * Runs the handoff for a Firestore that `initializeFirestore` has just\n * configured with the durable multi-tab cache under `prefix`\n * (`firestorePersistencePrefix`). Call it in the same task as\n * `initializeFirestore`, before any read: its first step is the synchronous\n * snapshot of the clients that are not this tab. Fire and forget: it never\n * rejects, and every failure leaves the SDK's own election in charge.\n */\nexport function startFirestoreTabHandoff(\n firestore: Firestore,\n prefix: string,\n): Promise<TabHandoffOutcome> {\n const factory = browserIndexedDb()\n const storage = browserLocalStorage()\n const databaseName = `${prefix}main`\n return runTabHandoff({\n isVisible: () =>\n typeof document !== 'undefined' && document.visibilityState === 'visible',\n isOnline: () =>\n typeof navigator === 'undefined' || navigator.onLine !== false,\n readClientIds: () => readSharedClientIds(storage, prefix),\n startClient: () => enableNetwork(firestore),\n claim: (selfId) => claimPrimaryLease(factory, databaseName, selfId),\n cycleNetwork: async () => {\n await disableNetwork(firestore)\n await enableNetwork(firestore)\n },\n }).then((outcome) => {\n if (outcome.result === 'claimed') {\n console.debug(\n '[firestore] This visible tab took the primary lease from a background tab (AGL-3660).',\n outcome,\n )\n }\n return outcome\n })\n}\n"],"names":["disableNetwork","enableNetwork","PRIMARY_LEASE_MAX_AGE_MS","CLAIM_TIMEOUT_MS","OWNER_STORE","OWNER_KEY","CLIENT_METADATA_STORE","MUTATIONS_STORE","decideLeaseClaim","input","owner","holder","self","pendingWrites","claim","reason","ownerId","clientId","allowTabSynchronization","inForeground","runTabHandoff","deps","isVisible","result","isOnline","before","readClientIds","size","startClient","added","filter","id","has","length","selfId","outcome","state","cycleNetwork","fromOwnerId","leaseAgeMs","openExisting","factory","databaseName","databases","undefined","some","database","name","Promise","resolve","request","open","onupgradeneeded","transaction","abort","onerror","onblocked","onsuccess","onversionchange","close","hasStores","stores","every","store","objectStoreNames","contains","escapeRegExp","text","replace","readSharedClientIds","storage","prefix","ids","Set","clientKey","RegExp","i","match","exec","key","add","claimPrimaryLease","timeoutMs","now","Date","catch","settled","started","timer","setTimeout","finish","value","clearTimeout","oncomplete","onabort","owners","objectStore","clients","ownerRequest","get","selfRequest","holderRequest","countRequest","count","decision","at","put","updateTimeMs","leaseTimestampMs","browserIndexedDb","window","indexedDB","browserLocalStorage","localStorage","startFirestoreTabHandoff","firestore","document","visibilityState","navigator","onLine","then","console","debug"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firestore-tab-handoff.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n disableNetwork,\n enableNetwork,\n type Firestore,\n} from 'firebase/firestore'\n\n/**\n * A NEWLY OPENED, VISIBLE TAB TAKES THE PRIMARY LEASE FROM A HIDDEN ONE\n * (AGL-3660).\n *\n * ## What the SDK does on its own\n *\n * With `persistentMultipleTabManager`, exactly one tab per origin — the\n * holder of the `owner` row in `firestore/<app>/<project>/main` — runs the\n * network. Every other tab sends its queries to that tab through\n * `localStorage` and reads the answers back out of IndexedDB. Read against\n * `@firebase/firestore` 4.17.2 (`IndexedDbPersistence`):\n *\n * - **A valid lease is never handed over.** `canActAsPrimary` returns `true`\n * for the holder whenever its own lease is under 5 s old\n * (`MAX_PRIMARY_ELIGIBLE_AGE_MS`) and its network is enabled, whatever its\n * visibility. The \"prefer the foreground tab\" rule only applies once the\n * lease has lapsed. A console tab the user left in the background therefore\n * keeps serving the tab they are looking at, for as long as its 4 s renewal\n * timer keeps firing.\n * - **A background holder is a slow server.** Its renewal timer is throttled\n * (production lease rows, 2026-10-08: renewed every ~4.9 s, against a 5 s\n * lapse), and every query the new tab issues waits on that tab's event loop.\n * The workspace read on a fresh `/aglyn-org/media` tab came back only once\n * the new tab had won the lease on a lapse, its own Listen channel opening\n * ~4.7 s in (PR #1313).\n * - **The new tab's first record says it is hidden.** `start()` runs its\n * first election (`updateClientMetadataAndTryBecomePrimary`) before\n * `attachVisibilityHandler()` has read `document.visibilityState`, so the\n * `clientMetadata` row it writes carries `inForeground: false` until the\n * next 4 s tick. When the holder's lease does lapse in that window, the\n * holder finds no better candidate and takes it straight back.\n *\n * ## What this does\n *\n * Once, at boot, in a tab that is visible and online:\n *\n * 1. Snapshot the client ids already registered, synchronously, in the same\n * task as `initializeFirestore` — before anything can start this tab's\n * client. They come from the SDK's `firestore_clients_<prefix>_<id>` keys\n * in `localStorage`, which a tab writes once its persistence has started.\n * 2. Start this tab's Firestore (`enableNetwork` — a no-op on a client whose\n * network is on — resolves once persistence has started and the tab has\n * run its first election). The one id that was not there before is this\n * tab.\n * 3. In ONE IndexedDB transaction over `owner`, `clientMetadata` and\n * `mutations`, decide with {@link decideLeaseClaim}. When the holder is\n * another tab that has recorded itself as hidden, its lease has LAPSED\n * and no write is pending, write this tab's row as foreground and the\n * lease in this tab's name.\n * 4. Cycle this tab's network (`disableNetwork` then `enableNetwork`): each\n * step runs the SDK's own election at once, and the second finds a valid\n * lease that is this tab's and becomes primary.\n *\n * ## Why this is safe\n *\n * - **Never from a live holder (AGL-3788).** A lease renewed under 5 s ago\n * belongs to a tab whose remote store is running: its Watch stream keeps\n * applying events and resuming targets until its next 4 s tick notices the\n * lease is gone. Taking that lease makes two tabs primary over one cache,\n * and their target bookkeeping diverges — a resumed document target that\n * comes back `current` without its key in this cache's mapping is written\n * as a NoDocument, so a live host or org reads as deleted, and the\n * tombstone outlives reloads because a resumed listen only pulls deltas.\n * That is what the Sites page's \"No sites yet\" and an Enterprise org\n * showing Free were, in a browser with several console tabs. The SDK never\n * takes a valid lease (`canActAsPrimary`), and neither does this.\n * - **It is the SDK's own handoff.** A lapsed lease is one the SDK's next\n * election would hand to a foreground tab anyway; this only stops the\n * hidden holder winning it back on a stale `inForeground: false` row. A\n * holder that then finds the lease in another tab's name fails its next\n * `readwrite-primary` transaction (`verifyPrimaryLease`), flips to\n * secondary and throws the `failed-precondition` its sync engine already\n * swallows (`ignoreIfPrimaryLeaseLoss`). IndexedDB serialises our\n * transaction against the SDK's (which span every store), so no election\n * interleaves with it.\n * - **The old holder cannot take it back.** The lease is valid and not its\n * own, so its `canActAsPrimary` is `false`. Should this tab fail to renew,\n * the lease lapses in 5 s and the corrected `inForeground: true` row makes\n * this tab the preferred candidate, not the hidden one.\n * - **Never with a write in flight.** The holder sends pending mutations; a\n * new primary resends every unacknowledged batch. Any row in `mutations`\n * skips the claim, so a write is never sent twice by two tabs.\n * - **Never from a visible holder.** A tab visible in another window serves\n * quickly (measured in production: ~390 ms from a query target written to\n * it being `current`), and taking its lease would only move the work.\n * - **The cycle is invisible in a secondary.** The sync engine ignores its\n * own remote store's online state while secondary\n * (`syncEngineApplyOnlineStateChange`), so no listener sees an offline\n * flicker. If this tab somehow became primary in between, the cycle is the\n * same one `firestore-stall-recovery.ts` runs.\n * - **Every failure is the old behaviour.** Anything unexpected — no\n * database, the store locked (the AGL-3428 wedge), two new tabs at once, a\n * holder that is visible or whose lease is still live — skips the claim,\n * and the SDK's own election runs as it always has.\n *\n * Not chosen: `persistentSingleTabManager` (rejected in AGL-3373/3428, and\n * `forceOwnership` there fails every other tab with\n * `PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG`); `getDocFromServer` (a secondary routes\n * it through the holder like any listen); a per-tab memory cache for the first\n * read (the cache is fixed at `initializeFirestore`); a synthetic\n * `visibilitychange` to rerun the election (also fires the console's own\n * twenty-odd visibility listeners).\n */\n\n/** The SDK's lease lifetime (`MAX_PRIMARY_ELIGIBLE_AGE_MS`). */\nexport const PRIMARY_LEASE_MAX_AGE_MS = 5_000\n\n/**\n * How long the claim transaction may wait to start. The SDK's own take\n * milliseconds; one that cannot start is behind the AGL-3428 wedge, which\n * `firestore-multitab-wedge.ts` handles.\n */\nexport const CLAIM_TIMEOUT_MS = 1_500\n\n/** The SDK's stores and the lease row's key (`indexeddb_sentinels.ts`). */\nconst OWNER_STORE = 'owner'\nconst OWNER_KEY = 'owner'\nconst CLIENT_METADATA_STORE = 'clientMetadata'\nconst MUTATIONS_STORE = 'mutations'\n\n/** `DbPrimaryClient`. */\nexport interface LeaseRow {\n ownerId: string\n allowTabSynchronization: boolean\n leaseTimestampMs: number\n}\n\n/** `DbClientMetadata`. */\nexport interface ClientRow {\n clientId: string\n updateTimeMs: number\n networkEnabled: boolean\n inForeground: boolean\n}\n\n/** Why the claim was not made. */\nexport type LeaseClaimSkip =\n | 'unidentified'\n | 'vacant'\n | 'already-primary'\n | 'exclusive-holder'\n | 'holder-unknown'\n | 'holder-visible'\n | 'holder-live'\n | 'pending-writes'\n\nexport type LeaseClaimDecision =\n { claim: true } | { claim: false; reason: LeaseClaimSkip }\n\n/**\n * Whether this tab (`self`) should take the lease `owner` holds. Pure: the\n * caller reads every input inside the one transaction that writes the result.\n *\n * A vacant or lapsed-and-vacant lease is left to the SDK, whose next election\n * takes it; only a lease another tab holds is ever claimed, only from a\n * holder whose own row says it is in the background, and only once that\n * lease has lapsed — a live one would leave two tabs running as primary.\n */\nexport function decideLeaseClaim(input: {\n owner: LeaseRow | undefined\n holder: ClientRow | undefined\n self: ClientRow | undefined\n pendingWrites: number\n /** The clock the lease is measured against, in ms. */\n now: number\n}): LeaseClaimDecision {\n const { owner, holder, self, pendingWrites, now } = input\n if (!self) return { claim: false, reason: 'unidentified' }\n if (!owner) return { claim: false, reason: 'vacant' }\n if (owner.ownerId === self.clientId)\n return { claim: false, reason: 'already-primary' }\n // A single-tab holder never shares; `persistentMultipleTabManager` never\n // writes `false`, so this is another configuration on the origin.\n if (!owner.allowTabSynchronization)\n return { claim: false, reason: 'exclusive-holder' }\n if (pendingWrites > 0) return { claim: false, reason: 'pending-writes' }\n if (!holder) return { claim: false, reason: 'holder-unknown' }\n if (holder.inForeground) return { claim: false, reason: 'holder-visible' }\n // `MAX_PRIMARY_ELIGIBLE_AGE_MS`: the SDK's lease is valid while younger\n // than this, and a timestamp ahead of our clock is younger still.\n if (now - owner.leaseTimestampMs < PRIMARY_LEASE_MAX_AGE_MS)\n return { claim: false, reason: 'holder-live' }\n return { claim: true }\n}\n\n/** What the boot handoff did. */\nexport type TabHandoffOutcome =\n | { result: 'claimed'; fromOwnerId: string; leaseAgeMs: number }\n | {\n result: 'skipped'\n reason:\n | LeaseClaimSkip\n | 'hidden'\n | 'offline'\n | 'alone'\n | 'no-database'\n | 'locked'\n | 'error'\n }\n\n/** What one claim transaction found and did. */\nexport type ClaimTransactionResult =\n | { state: 'claimed'; fromOwnerId: string; leaseAgeMs: number }\n | { state: 'skipped'; reason: LeaseClaimSkip }\n | { state: 'locked' }\n | { state: 'unavailable' }\n\nexport interface TabHandoffDeps {\n isVisible: () => boolean\n isOnline: () => boolean\n /**\n * Client ids registered on this origin. Must be synchronous: the first call\n * is the snapshot taken before this tab's client can start.\n */\n readClientIds: () => ReadonlySet<string>\n /** Starts this tab's Firestore; resolves once its persistence has started. */\n startClient: () => Promise<void>\n /** Runs the claim for `selfId` in one transaction. */\n claim: (selfId: string | undefined) => Promise<ClaimTransactionResult>\n /** `disableNetwork` then `enableNetwork`: two elections, at once. */\n cycleNetwork: () => Promise<void>\n}\n\n/**\n * The boot handoff, with its browser parts injected. Never rejects.\n */\nexport async function runTabHandoff(\n deps: TabHandoffDeps,\n): Promise<TabHandoffOutcome> {\n try {\n if (!deps.isVisible()) return { result: 'skipped', reason: 'hidden' }\n if (!deps.isOnline()) return { result: 'skipped', reason: 'offline' }\n // Before this tab's Firestore starts, or this tab would be in it.\n const before = deps.readClientIds()\n // No other tab: this one wins the lease at its first election.\n if (before.size === 0) return { result: 'skipped', reason: 'alone' }\n await deps.startClient()\n // The user may have switched away while the client started.\n if (!deps.isVisible()) return { result: 'skipped', reason: 'hidden' }\n const added = [...deps.readClientIds()].filter((id) => !before.has(id))\n // Exactly one new client is this tab. None means this tab's Firestore\n // started before the snapshot; two means another tab started alongside.\n // Either way this tab cannot be told apart, so nothing is claimed.\n if (added.length !== 1) return { result: 'skipped', reason: 'unidentified' }\n const selfId = added[0]\n const outcome = await deps.claim(selfId)\n if (outcome.state === 'locked')\n return { result: 'skipped', reason: 'locked' }\n if (outcome.state === 'unavailable')\n return { result: 'skipped', reason: 'no-database' }\n if (outcome.state === 'skipped')\n return { result: 'skipped', reason: outcome.reason }\n await deps.cycleNetwork()\n return {\n result: 'claimed',\n fromOwnerId: outcome.fromOwnerId,\n leaseAgeMs: outcome.leaseAgeMs,\n }\n } catch {\n return { result: 'skipped', reason: 'error' }\n }\n}\n\n/**\n * Opens `databaseName` only if it already exists, at its current version —\n * never creating it under the SDK (the rule `probePrimaryLease` follows) —\n * and closes on `versionchange` so an SDK upgrade is never held up.\n */\nasync function openExisting(\n factory: IDBFactory,\n databaseName: string,\n): Promise<IDBDatabase | undefined> {\n if (typeof factory.databases !== 'function') return undefined\n const databases = await factory.databases()\n if (!databases.some((database) => database.name === databaseName))\n return undefined\n return new Promise((resolve) => {\n let request: IDBOpenDBRequest\n try {\n request = factory.open(databaseName)\n } catch {\n resolve(undefined)\n return\n }\n request.onupgradeneeded = () => request.transaction?.abort()\n request.onerror = () => resolve(undefined)\n request.onblocked = () => resolve(undefined)\n request.onsuccess = () => {\n const database = request.result\n database.onversionchange = () => database.close()\n resolve(database)\n }\n })\n}\n\nconst hasStores = (database: IDBDatabase, stores: string[]) =>\n stores.every((store) => database.objectStoreNames.contains(store))\n\nconst escapeRegExp = (text: string) =>\n text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/**\n * The client ids the SDK has registered under `prefix`\n * (`firestore/<app>/<project>/`), from its `firestore_clients_<prefix>_<id>`\n * keys. Synchronous, so a snapshot can be taken before this tab's client\n * starts. A key whose tab has since closed is harmless here: only ids that\n * APPEAR between two snapshots are read as this tab.\n */\nexport function readSharedClientIds(\n storage: Pick<Storage, 'length' | 'key'> | undefined,\n prefix: string,\n): ReadonlySet<string> {\n const ids = new Set<string>()\n if (!storage) return ids\n // The SDK's own key shape: a client id never contains `_`.\n const clientKey = new RegExp(\n `^firestore_clients_${escapeRegExp(prefix)}_([^_]+)$`,\n )\n try {\n for (let i = 0; i < storage.length; i++) {\n const match = clientKey.exec(storage.key(i) ?? '')\n if (match) ids.add(match[1])\n }\n } catch {\n // Site data blocked: no ids, so the handoff stands down.\n }\n return ids\n}\n\n/**\n * Reads the lease, both client rows and the pending-write count, decides, and\n * writes the claim — all in one `readwrite` transaction, so no election runs\n * between the read and the write. Aborted when it cannot start within\n * `timeoutMs`.\n */\nexport async function claimPrimaryLease(\n factory: IDBFactory | undefined,\n databaseName: string,\n selfId: string | undefined,\n {\n timeoutMs = CLAIM_TIMEOUT_MS,\n now = Date.now,\n }: { timeoutMs?: number; now?: () => number } = {},\n): Promise<ClaimTransactionResult> {\n if (!factory) return { state: 'unavailable' }\n const database = await openExisting(factory, databaseName).catch(\n () => undefined,\n )\n if (!database) return { state: 'unavailable' }\n const stores = [OWNER_STORE, CLIENT_METADATA_STORE, MUTATIONS_STORE]\n if (!hasStores(database, stores)) {\n database.close()\n return { state: 'unavailable' }\n }\n return new Promise<ClaimTransactionResult>((resolve) => {\n let transaction: IDBTransaction\n try {\n transaction = database.transaction(stores, 'readwrite')\n } catch {\n database.close()\n resolve({ state: 'unavailable' })\n return\n }\n let settled = false\n let started = false\n let result: ClaimTransactionResult = { state: 'unavailable' }\n // Only a transaction that cannot START is aborted: once the lease read\n // has landed, the decision and its writes take milliseconds.\n const timer = setTimeout(() => {\n if (started) return\n try {\n transaction.abort()\n } catch {\n // Already finished.\n }\n finish({ state: 'locked' })\n }, timeoutMs)\n const finish = (value: ClaimTransactionResult) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n database.close()\n resolve(value)\n }\n transaction.oncomplete = () => finish(result)\n transaction.onabort = () =>\n finish(started ? { state: 'unavailable' } : { state: 'locked' })\n transaction.onerror = () => finish({ state: 'unavailable' })\n\n const owners = transaction.objectStore(OWNER_STORE)\n const clients = transaction.objectStore(CLIENT_METADATA_STORE)\n const ownerRequest = owners.get(OWNER_KEY)\n ownerRequest.onsuccess = () => {\n started = true\n const owner = ownerRequest.result as LeaseRow | undefined\n const selfRequest = selfId === undefined ? undefined : clients.get(selfId)\n const holderRequest = owner?.ownerId\n ? clients.get(owner.ownerId)\n : undefined\n const countRequest = transaction.objectStore(MUTATIONS_STORE).count()\n countRequest.onsuccess = () => {\n // Requests in one transaction complete in order: both gets landed.\n const self = selfRequest?.result as ClientRow | undefined\n const holder = holderRequest?.result as ClientRow | undefined\n const at = now()\n const decision = decideLeaseClaim({\n owner,\n holder,\n self,\n pendingWrites: countRequest.result,\n now: at,\n })\n if ('reason' in decision) {\n result = { state: 'skipped', reason: decision.reason }\n return\n }\n // The row the SDK's first election should have written, then the\n // lease in this tab's name — `acquireOrExtendPrimaryLease`'s shape.\n clients.put({ ...self, inForeground: true, updateTimeMs: at })\n owners.put(\n {\n ownerId: self!.clientId,\n allowTabSynchronization: true,\n leaseTimestampMs: at,\n } satisfies LeaseRow,\n OWNER_KEY,\n )\n result = {\n state: 'claimed',\n fromOwnerId: owner!.ownerId,\n leaseAgeMs: at - owner!.leaseTimestampMs,\n }\n }\n }\n })\n}\n\nconst browserIndexedDb = (): IDBFactory | undefined => {\n try {\n return typeof window === 'undefined' ? undefined : window.indexedDB\n } catch {\n return undefined\n }\n}\n\nconst browserLocalStorage = (): Storage | undefined => {\n try {\n return typeof window === 'undefined' ? undefined : window.localStorage\n } catch {\n return undefined\n }\n}\n\n/**\n * Runs the handoff for a Firestore that `initializeFirestore` has just\n * configured with the durable multi-tab cache under `prefix`\n * (`firestorePersistencePrefix`). Call it in the same task as\n * `initializeFirestore`, before any read: its first step is the synchronous\n * snapshot of the clients that are not this tab. Fire and forget: it never\n * rejects, and every failure leaves the SDK's own election in charge.\n */\nexport function startFirestoreTabHandoff(\n firestore: Firestore,\n prefix: string,\n): Promise<TabHandoffOutcome> {\n const factory = browserIndexedDb()\n const storage = browserLocalStorage()\n const databaseName = `${prefix}main`\n return runTabHandoff({\n isVisible: () =>\n typeof document !== 'undefined' && document.visibilityState === 'visible',\n isOnline: () =>\n typeof navigator === 'undefined' || navigator.onLine !== false,\n readClientIds: () => readSharedClientIds(storage, prefix),\n startClient: () => enableNetwork(firestore),\n claim: (selfId) => claimPrimaryLease(factory, databaseName, selfId),\n cycleNetwork: async () => {\n await disableNetwork(firestore)\n await enableNetwork(firestore)\n },\n }).then((outcome) => {\n if (outcome.result === 'claimed') {\n console.debug(\n '[firestore] This visible tab took the primary lease from a background tab (AGL-3660).',\n outcome,\n )\n }\n return outcome\n })\n}\n"],"names":["disableNetwork","enableNetwork","PRIMARY_LEASE_MAX_AGE_MS","CLAIM_TIMEOUT_MS","OWNER_STORE","OWNER_KEY","CLIENT_METADATA_STORE","MUTATIONS_STORE","decideLeaseClaim","input","owner","holder","self","pendingWrites","now","claim","reason","ownerId","clientId","allowTabSynchronization","inForeground","leaseTimestampMs","runTabHandoff","deps","isVisible","result","isOnline","before","readClientIds","size","startClient","added","filter","id","has","length","selfId","outcome","state","cycleNetwork","fromOwnerId","leaseAgeMs","openExisting","factory","databaseName","databases","undefined","some","database","name","Promise","resolve","request","open","onupgradeneeded","transaction","abort","onerror","onblocked","onsuccess","onversionchange","close","hasStores","stores","every","store","objectStoreNames","contains","escapeRegExp","text","replace","readSharedClientIds","storage","prefix","ids","Set","clientKey","RegExp","i","match","exec","key","add","claimPrimaryLease","timeoutMs","Date","catch","settled","started","timer","setTimeout","finish","value","clearTimeout","oncomplete","onabort","owners","objectStore","clients","ownerRequest","get","selfRequest","holderRequest","countRequest","count","at","decision","put","updateTimeMs","browserIndexedDb","window","indexedDB","browserLocalStorage","localStorage","startFirestoreTabHandoff","firestore","document","visibilityState","navigator","onLine","then","console","debug"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,cAAc,EACdC,aAAa,QAER,qBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsGC,GAED,8DAA8D,GAC9D,OAAO,MAAMC,2BAA2B,KAAK;AAE7C;;;;CAIC,GACD,OAAO,MAAMC,mBAAmB,KAAK;AAErC,yEAAyE,GACzE,MAAMC,cAAc;AACpB,MAAMC,YAAY;AAClB,MAAMC,wBAAwB;AAC9B,MAAMC,kBAAkB;AA+BxB;;;;;;;;CAQC,GACD,OAAO,SAASC,iBAAiBC,KAOhC;IACC,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAEC,aAAa,EAAEC,GAAG,EAAE,GAAGL;IACpD,IAAI,CAACG,MAAM,OAAO;QAAEG,OAAO;QAAOC,QAAQ;IAAe;IACzD,IAAI,CAACN,OAAO,OAAO;QAAEK,OAAO;QAAOC,QAAQ;IAAS;IACpD,IAAIN,MAAMO,OAAO,KAAKL,KAAKM,QAAQ,EACjC,OAAO;QAAEH,OAAO;QAAOC,QAAQ;IAAkB;IACnD,yEAAyE;IACzE,kEAAkE;IAClE,IAAI,CAACN,MAAMS,uBAAuB,EAChC,OAAO;QAAEJ,OAAO;QAAOC,QAAQ;IAAmB;IACpD,IAAIH,gBAAgB,GAAG,OAAO;QAAEE,OAAO;QAAOC,QAAQ;IAAiB;IACvE,IAAI,CAACL,QAAQ,OAAO;QAAEI,OAAO;QAAOC,QAAQ;IAAiB;IAC7D,IAAIL,OAAOS,YAAY,EAAE,OAAO;QAAEL,OAAO;QAAOC,QAAQ;IAAiB;IACzE,wEAAwE;IACxE,kEAAkE;IAClE,IAAIF,MAAMJ,MAAMW,gBAAgB,GAAGnB,0BACjC,OAAO;QAAEa,OAAO;QAAOC,QAAQ;IAAc;IAC/C,OAAO;QAAED,OAAO;IAAK;AACvB;AAwCA;;CAEC,GACD,OAAO,eAAeO,cACpBC,IAAoB;IAEpB,IAAI;QACF,IAAI,CAACA,KAAKC,SAAS,IAAI,OAAO;YAAEC,QAAQ;YAAWT,QAAQ;QAAS;QACpE,IAAI,CAACO,KAAKG,QAAQ,IAAI,OAAO;YAAED,QAAQ;YAAWT,QAAQ;QAAU;QACpE,kEAAkE;QAClE,MAAMW,SAASJ,KAAKK,aAAa;QACjC,+DAA+D;QAC/D,IAAID,OAAOE,IAAI,KAAK,GAAG,OAAO;YAAEJ,QAAQ;YAAWT,QAAQ;QAAQ;QACnE,MAAMO,KAAKO,WAAW;QACtB,4DAA4D;QAC5D,IAAI,CAACP,KAAKC,SAAS,IAAI,OAAO;YAAEC,QAAQ;YAAWT,QAAQ;QAAS;QACpE,MAAMe,QAAQ;eAAIR,KAAKK,aAAa;SAAG,CAACI,MAAM,CAAC,CAACC,KAAO,CAACN,OAAOO,GAAG,CAACD;QACnE,sEAAsE;QACtE,wEAAwE;QACxE,mEAAmE;QACnE,IAAIF,MAAMI,MAAM,KAAK,GAAG,OAAO;YAAEV,QAAQ;YAAWT,QAAQ;QAAe;QAC3E,MAAMoB,SAASL,KAAK,CAAC,EAAE;QACvB,MAAMM,UAAU,MAAMd,KAAKR,KAAK,CAACqB;QACjC,IAAIC,QAAQC,KAAK,KAAK,UACpB,OAAO;YAAEb,QAAQ;YAAWT,QAAQ;QAAS;QAC/C,IAAIqB,QAAQC,KAAK,KAAK,eACpB,OAAO;YAAEb,QAAQ;YAAWT,QAAQ;QAAc;QACpD,IAAIqB,QAAQC,KAAK,KAAK,WACpB,OAAO;YAAEb,QAAQ;YAAWT,QAAQqB,QAAQrB,MAAM;QAAC;QACrD,MAAMO,KAAKgB,YAAY;QACvB,OAAO;YACLd,QAAQ;YACRe,aAAaH,QAAQG,WAAW;YAChCC,YAAYJ,QAAQI,UAAU;QAChC;IACF,EAAE,eAAM;QACN,OAAO;YAAEhB,QAAQ;YAAWT,QAAQ;QAAQ;IAC9C;AACF;AAEA;;;;CAIC,GACD,eAAe0B,aACbC,OAAmB,EACnBC,YAAoB;IAEpB,IAAI,OAAOD,QAAQE,SAAS,KAAK,YAAY,OAAOC;IACpD,MAAMD,YAAY,MAAMF,QAAQE,SAAS;IACzC,IAAI,CAACA,UAAUE,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,KAAKL,eAClD,OAAOE;IACT,OAAO,IAAII,QAAQ,CAACC;QAClB,IAAIC;QACJ,IAAI;YACFA,UAAUT,QAAQU,IAAI,CAACT;QACzB,EAAE,eAAM;YACNO,QAAQL;YACR;QACF;QACAM,QAAQE,eAAe,GAAG;gBAAMF;oBAAAA,uBAAAA,QAAQG,WAAW,qBAAnBH,qBAAqBI,KAAK;;QAC1DJ,QAAQK,OAAO,GAAG,IAAMN,QAAQL;QAChCM,QAAQM,SAAS,GAAG,IAAMP,QAAQL;QAClCM,QAAQO,SAAS,GAAG;YAClB,MAAMX,WAAWI,QAAQ3B,MAAM;YAC/BuB,SAASY,eAAe,GAAG,IAAMZ,SAASa,KAAK;YAC/CV,QAAQH;QACV;IACF;AACF;AAEA,MAAMc,YAAY,CAACd,UAAuBe,SACxCA,OAAOC,KAAK,CAAC,CAACC,QAAUjB,SAASkB,gBAAgB,CAACC,QAAQ,CAACF;AAE7D,MAAMG,eAAe,CAACC,OACpBA,KAAKC,OAAO,CAAC,uBAAuB;AAEtC;;;;;;CAMC,GACD,OAAO,SAASC,oBACdC,OAAoD,EACpDC,MAAc;IAEd,MAAMC,MAAM,IAAIC;IAChB,IAAI,CAACH,SAAS,OAAOE;IACrB,2DAA2D;IAC3D,MAAME,YAAY,IAAIC,OACpB,CAAC,mBAAmB,EAAET,aAAaK,QAAQ,SAAS,CAAC;IAEvD,IAAI;QACF,IAAK,IAAIK,IAAI,GAAGA,IAAIN,QAAQrC,MAAM,EAAE2C,IAAK;gBACVN;YAA7B,MAAMO,QAAQH,UAAUI,IAAI,EAACR,eAAAA,QAAQS,GAAG,CAACH,cAAZN,eAAkB;YAC/C,IAAIO,OAAOL,IAAIQ,GAAG,CAACH,KAAK,CAAC,EAAE;QAC7B;IACF,EAAE,eAAM;IACN,yDAAyD;IAC3D;IACA,OAAOL;AACT;AAEA;;;;;CAKC,GACD,OAAO,eAAeS,kBACpBxC,OAA+B,EAC/BC,YAAoB,EACpBR,MAA0B,EAC1B,EACEgD,YAAYjF,gBAAgB,EAC5BW,MAAMuE,KAAKvE,GAAG,EAC6B,GAAG,CAAC,CAAC;IAElD,IAAI,CAAC6B,SAAS,OAAO;QAAEL,OAAO;IAAc;IAC5C,MAAMU,WAAW,MAAMN,aAAaC,SAASC,cAAc0C,KAAK,CAC9D,IAAMxC;IAER,IAAI,CAACE,UAAU,OAAO;QAAEV,OAAO;IAAc;IAC7C,MAAMyB,SAAS;QAAC3D;QAAaE;QAAuBC;KAAgB;IACpE,IAAI,CAACuD,UAAUd,UAAUe,SAAS;QAChCf,SAASa,KAAK;QACd,OAAO;YAAEvB,OAAO;QAAc;IAChC;IACA,OAAO,IAAIY,QAAgC,CAACC;QAC1C,IAAII;QACJ,IAAI;YACFA,cAAcP,SAASO,WAAW,CAACQ,QAAQ;QAC7C,EAAE,eAAM;YACNf,SAASa,KAAK;YACdV,QAAQ;gBAAEb,OAAO;YAAc;YAC/B;QACF;QACA,IAAIiD,UAAU;QACd,IAAIC,UAAU;QACd,IAAI/D,SAAiC;YAAEa,OAAO;QAAc;QAC5D,uEAAuE;QACvE,6DAA6D;QAC7D,MAAMmD,QAAQC,WAAW;YACvB,IAAIF,SAAS;YACb,IAAI;gBACFjC,YAAYC,KAAK;YACnB,EAAE,eAAM;YACN,oBAAoB;YACtB;YACAmC,OAAO;gBAAErD,OAAO;YAAS;QAC3B,GAAG8C;QACH,MAAMO,SAAS,CAACC;YACd,IAAIL,SAAS;YACbA,UAAU;YACVM,aAAaJ;YACbzC,SAASa,KAAK;YACdV,QAAQyC;QACV;QACArC,YAAYuC,UAAU,GAAG,IAAMH,OAAOlE;QACtC8B,YAAYwC,OAAO,GAAG,IACpBJ,OAAOH,UAAU;gBAAElD,OAAO;YAAc,IAAI;gBAAEA,OAAO;YAAS;QAChEiB,YAAYE,OAAO,GAAG,IAAMkC,OAAO;gBAAErD,OAAO;YAAc;QAE1D,MAAM0D,SAASzC,YAAY0C,WAAW,CAAC7F;QACvC,MAAM8F,UAAU3C,YAAY0C,WAAW,CAAC3F;QACxC,MAAM6F,eAAeH,OAAOI,GAAG,CAAC/F;QAChC8F,aAAaxC,SAAS,GAAG;YACvB6B,UAAU;YACV,MAAM9E,QAAQyF,aAAa1E,MAAM;YACjC,MAAM4E,cAAcjE,WAAWU,YAAYA,YAAYoD,QAAQE,GAAG,CAAChE;YACnE,MAAMkE,gBAAgB5F,CAAAA,yBAAAA,MAAOO,OAAO,IAChCiF,QAAQE,GAAG,CAAC1F,MAAMO,OAAO,IACzB6B;YACJ,MAAMyD,eAAehD,YAAY0C,WAAW,CAAC1F,iBAAiBiG,KAAK;YACnED,aAAa5C,SAAS,GAAG;gBACvB,mEAAmE;gBACnE,MAAM/C,OAAOyF,+BAAAA,YAAa5E,MAAM;gBAChC,MAAMd,SAAS2F,iCAAAA,cAAe7E,MAAM;gBACpC,MAAMgF,KAAK3F;gBACX,MAAM4F,WAAWlG,iBAAiB;oBAChCE;oBACAC;oBACAC;oBACAC,eAAe0F,aAAa9E,MAAM;oBAClCX,KAAK2F;gBACP;gBACA,IAAI,YAAYC,UAAU;oBACxBjF,SAAS;wBAAEa,OAAO;wBAAWtB,QAAQ0F,SAAS1F,MAAM;oBAAC;oBACrD;gBACF;gBACA,iEAAiE;gBACjE,oEAAoE;gBACpEkF,QAAQS,GAAG,CAAC,aAAK/F;oBAAMQ,cAAc;oBAAMwF,cAAcH;;gBACzDT,OAAOW,GAAG,CACR;oBACE1F,SAASL,KAAMM,QAAQ;oBACvBC,yBAAyB;oBACzBE,kBAAkBoF;gBACpB,GACApG;gBAEFoB,SAAS;oBACPa,OAAO;oBACPE,aAAa9B,MAAOO,OAAO;oBAC3BwB,YAAYgE,KAAK/F,MAAOW,gBAAgB;gBAC1C;YACF;QACF;IACF;AACF;AAEA,MAAMwF,mBAAmB;IACvB,IAAI;QACF,OAAO,OAAOC,WAAW,cAAchE,YAAYgE,OAAOC,SAAS;IACrE,EAAE,eAAM;QACN,OAAOjE;IACT;AACF;AAEA,MAAMkE,sBAAsB;IAC1B,IAAI;QACF,OAAO,OAAOF,WAAW,cAAchE,YAAYgE,OAAOG,YAAY;IACxE,EAAE,eAAM;QACN,OAAOnE;IACT;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,SAASoE,yBACdC,SAAoB,EACpB1C,MAAc;IAEd,MAAM9B,UAAUkE;IAChB,MAAMrC,UAAUwC;IAChB,MAAMpE,eAAe,GAAG6B,OAAO,IAAI,CAAC;IACpC,OAAOnD,cAAc;QACnBE,WAAW,IACT,OAAO4F,aAAa,eAAeA,SAASC,eAAe,KAAK;QAClE3F,UAAU,IACR,OAAO4F,cAAc,eAAeA,UAAUC,MAAM,KAAK;QAC3D3F,eAAe,IAAM2C,oBAAoBC,SAASC;QAClD3C,aAAa,IAAM7B,cAAckH;QACjCpG,OAAO,CAACqB,SAAW+C,kBAAkBxC,SAASC,cAAcR;QAC5DG,cAAc;YACZ,MAAMvC,eAAemH;YACrB,MAAMlH,cAAckH;QACtB;IACF,GAAGK,IAAI,CAAC,CAACnF;QACP,IAAIA,QAAQZ,MAAM,KAAK,WAAW;YAChCgG,QAAQC,KAAK,CACX,yFACArF;QAEJ;QACA,OAAOA;IACT;AACF"}
|