@aglyn/tenant-feature-instance 1.0.0-beta.218 → 1.0.0-beta.220
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/README.md +6 -2
- package/package.json +7 -7
- package/src/lib/components/container-picker.d.ts +70 -0
- package/src/lib/components/container-picker.js +140 -0
- package/src/lib/components/container-picker.js.map +1 -0
- package/src/lib/hooks/firebase/firebase-services.js +15 -3
- package/src/lib/hooks/firebase/firebase-services.js.map +1 -1
- package/src/lib/hooks/firebase/firestore-cache.d.ts +13 -0
- package/src/lib/hooks/firebase/firestore-cache.js +16 -0
- package/src/lib/hooks/firebase/firestore-cache.js.map +1 -1
- package/src/lib/hooks/firebase/firestore-multitab-wedge.d.ts +200 -0
- package/src/lib/hooks/firebase/firestore-multitab-wedge.js +325 -0
- package/src/lib/hooks/firebase/firestore-multitab-wedge.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-stall-recovery.d.ts +29 -2
- package/src/lib/hooks/firebase/firestore-stall-recovery.js +41 -11
- package/src/lib/hooks/firebase/firestore-stall-recovery.js.map +1 -1
- package/src/lib/hooks/use-container-options.d.ts +98 -0
- package/src/lib/hooks/use-container-options.js +174 -0
- package/src/lib/hooks/use-container-options.js.map +1 -0
- package/src/lib/hooks/use-host-document-version.d.ts +52 -0
- package/src/lib/hooks/{use-form-version.js → use-host-document-version.js} +6 -16
- package/src/lib/hooks/use-host-document-version.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +1 -1
- package/src/lib/hooks/use-org-member-options.js +1 -1
- package/src/lib/hooks/use-org-member-options.js.map +1 -1
- package/src/lib/tenant.d.ts +2 -2
- package/src/lib/tenant.js +2 -2
- package/src/lib/tenant.js.map +1 -1
- package/src/lib/hooks/use-form-version.d.ts +0 -44
- package/src/lib/hooks/use-form-version.js.map +0 -1
- package/src/lib/hooks/use-host-campaigns.d.ts +0 -98
- package/src/lib/hooks/use-host-campaigns.js +0 -160
- package/src/lib/hooks/use-host-campaigns.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firestore-cache.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'use client'\n\nimport { type FirebaseApp } from 'firebase/app'\nimport {\n type FirestoreLocalCache,\n memoryLocalCache,\n memoryLruGarbageCollector,\n persistentLocalCache,\n persistentMultipleTabManager,\n} from 'firebase/firestore'\n\nimport { type AuthPersistenceClass } from './auth-persistence'\nimport {\n pruneBrowserSharedClientState,\n type SharedClientStatePrunePlan,\n} from './firestore-shared-client-state'\n\n/**\n * The durable cache's size bound: the SDK's own default, 40 MB, written down.\n * See \"The durable cache's size\" on {@link localCacheFor}.\n */\nexport const DURABLE_CACHE_SIZE_BYTES = 40 * 1024 * 1024\n\n/**\n * The Firestore `localCache` an origin of this persistence class may use\n * (AGL-1456).\n *\n * ## Why this keys off the *auth* persistence class\n *\n * The name is historical — the class predates this file (AGL-1379) — but the\n * question it answers was never auth-specific. `AuthPersistenceClass` is\n * documented as *\"how much of a session an origin is allowed to keep\"*, and\n * AGL-1099a's D6 justified `ephemeral` on the grounds that **\"the customer can\n * re-point the DNS and read this origin.\"**\n *\n * That reasoning does not stop at the credential. It is equally true of every\n * document body Firestore caches in IndexedDB on that origin — measured, not\n * argued: the PoC (`docs/design/agl-1099a-poc-findings.md` §5) dumped every\n * object store on the origin and found full document contents under\n * `firestore/<app>/<project>/main → remoteDocumentsV14`, alongside\n * `firestore_clients_*` / `firestore_online_state_*` /\n * `firestore_sequence_number_*` in `localStorage`. D6 hardened the key and\n * left the safe behind it open; this closes it.\n *\n * **One declaration, two consequences — deliberately.** A separate\n * `firestoreCache` prop would let a caller declare a custom console domain\n * `ephemeral` for auth and still hand it a persistent cache, which is exactly\n * the split that produced this bug. There is one fact about the origin, so\n * there is one place to state it.\n *\n * ## The two classes\n *\n * - `durable` → `persistentLocalCache` with the multi-tab manager. Unchanged\n * from what every host has always run, and it must stay that way:\n * `*.aglyn.com` / `*.aglyn.app` are ours forever, and this cache is part of\n * why console read volume is what it is (AGL-1440).\n * - `ephemeral` → `memoryLocalCache`, so **nothing** reaches disk on an origin\n * whose DNS the customer can re-point at their own server.\n *\n * ## The durable cache's size\n *\n * `cacheSizeBytes` is spelled out at the SDK default so the bound is stated\n * here and cannot quietly become `CACHE_SIZE_UNLIMITED` if that default moves.\n * Stating it changes nothing at runtime, and what it governs is narrower than\n * the name suggests — measured against `@firebase/firestore` 4.17.1:\n *\n * - LRU collection is on without it: an unset `cacheSizeBytes` resolves to\n * `LruParams.DEFAULT`, the same 40 MB threshold.\n * - It runs only in the tab holding the primary lease, 1 minute after that tab\n * becomes primary and every 5 minutes after.\n * - The threshold is compared with the byte size of **cached documents**\n * (`remoteDocumentsV14`) and nothing else. `targets` and `targetDocuments`\n * rows are collected alongside documents once documents cross it, never\n * before.\n * - It never touches `localStorage`, where the multi-tab manager strands a\n * record per tab and per query target; {@link pruneSharedClientStateFor}\n * handles those.\n *\n * It is deliberately not lowered. A smaller cache evicts exactly the documents\n * that make this cache worth having — a cold load resuming from disk, and\n * listeners painting through a stale session — and a collected target is given\n * a NEW id the next time its query is listened to, which adds a `localStorage`\n * record rather than removing one.\n *\n * ## Why the memory cache gets the LRU collector, not the default\n *\n * `memoryLocalCache()` defaults to `memoryEagerGarbageCollector`, which drops\n * a document the moment no listener references it — so navigating away from a\n * console page and back re-reads its whole working set from the server. The\n * LRU collector keeps documents in the JS heap up to its 40 MB default instead,\n * which recovers most of the intra-session read saving `persistentLocalCache`\n * was giving us.\n *\n * It costs nothing against the property being protected: the LRU collector is\n * still **memory only**. Nothing is written to IndexedDB, nothing survives a\n * tab close, and a page load on a re-pointed origin starts empty — the same\n * bound `inMemoryPersistence` gives the refresh token.\n *\n * The read cost that remains is real and is the stated trade: a **cold** page\n * load on a custom console domain re-reads its working set where a durable\n * origin would have resumed from disk, and multiple tabs no longer share one\n * backend connection through `persistentMultipleTabManager`. That is the right\n * trade for an Enterprise-only feature, but it is a trade.\n *\n * ## `Failed to obtain primary lease for action '…'` is not a bug to fix\n *\n * Exactly one open tab holds the IndexedDB primary lease and applies remote\n * events; the rest read the results it writes. When the lease changes hands —\n * a tab opening, closing, or coming back to the foreground — a transaction\n * already in flight on the losing tab finds itself no longer primary, and the\n * SDK `logError`s that message immediately before throwing a\n * `failed-precondition` that its OWN multi-tab sync engine catches\n * (`ignoreIfPrimaryLeaseLoss`) and downgrades to a debug line. The tab then\n * continues as a secondary. Nothing is dropped and no listener stops.\n *\n * It is therefore noise from a handled handoff, and it scales with how many\n * tabs are churning rather than with anything the console does. The message\n * names a lease, so the obvious reading is that the multi-tab manager is\n * missing and should be added — it is already here, which is why the message\n * appears at all: `persistentSingleTabManager` refuses the second tab's\n * persistence outright instead of sharing it. Confirmed at runtime by the\n * `owner` record in `firestore/<app>/<project>/main`, which carries\n * `allowTabSynchronization: true`.\n */\nexport function localCacheFor(\n originClass: AuthPersistenceClass,\n): FirestoreLocalCache {\n if (originClass === 'ephemeral') {\n return memoryLocalCache({ garbageCollector: memoryLruGarbageCollector() })\n }\n return persistentLocalCache({\n tabManager: persistentMultipleTabManager(),\n cacheSizeBytes: DURABLE_CACHE_SIZE_BYTES,\n })\n}\n\n/**\n * Prunes the multi-tab records the durable cache strands in `localStorage`\n * (AGL-2845), for an app whose Firestore this page has just initialized.\n *\n * Keyed off the same origin class as {@link localCacheFor}, because the\n * durable class is the only one running `persistentMultipleTabManager`, the\n * only writer of those records. An ephemeral origin's memory cache writes\n * none, so it resolves without looking.\n *\n * Fire-and-forget: it never rejects and nothing waits on it. What it deletes,\n * and why a live tab's records never are, is in\n * `firestore-shared-client-state.ts`.\n */\nexport function pruneSharedClientStateFor(\n originClass: AuthPersistenceClass,\n app: FirebaseApp,\n): Promise<SharedClientStatePrunePlan | undefined> {\n if (originClass === 'ephemeral') return Promise.resolve(undefined)\n return pruneBrowserSharedClientState(app.name, app.options?.projectId)\n}\n"],"names":["memoryLocalCache","memoryLruGarbageCollector","persistentLocalCache","persistentMultipleTabManager","pruneBrowserSharedClientState","DURABLE_CACHE_SIZE_BYTES","localCacheFor","originClass","garbageCollector","tabManager","cacheSizeBytes","pruneSharedClientStateFor","app","Promise","resolve","undefined","name","options","projectId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAEEA,gBAAgB,EAChBC,yBAAyB,EACzBC,oBAAoB,EACpBC,4BAA4B,QACvB,qBAAoB;AAG3B,SACEC,6BAA6B,QAExB,qCAAiC;AAExC;;;CAGC,GACD,OAAO,MAAMC,2BAA2B,KAAK,OAAO,KAAI;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoGC,GACD,OAAO,SAASC,cACdC,WAAiC;IAEjC,IAAIA,gBAAgB,aAAa;QAC/B,OAAOP,iBAAiB;YAAEQ,kBAAkBP;QAA4B;IAC1E;IACA,OAAOC,qBAAqB;QAC1BO,YAAYN;QACZO,gBAAgBL;IAClB;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASM,0BACdJ,WAAiC,EACjCK,GAAgB;QAG+BA;IAD/C,IAAIL,gBAAgB,aAAa,OAAOM,QAAQC,OAAO,CAACC;IACxD,OAAOX,8BAA8BQ,IAAII,IAAI,GAAEJ,eAAAA,IAAIK,OAAO,qBAAXL,aAAaM,SAAS;AACvE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firestore-cache.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'use client'\n\nimport { type FirebaseApp } from 'firebase/app'\nimport {\n type FirestoreLocalCache,\n memoryLocalCache,\n memoryLruGarbageCollector,\n persistentLocalCache,\n persistentMultipleTabManager,\n} from 'firebase/firestore'\n\nimport { type AuthPersistenceClass } from './auth-persistence'\nimport { memoryCacheFallbackActive } from './firestore-multitab-wedge'\nimport {\n pruneBrowserSharedClientState,\n type SharedClientStatePrunePlan,\n} from './firestore-shared-client-state'\n\n/**\n * The durable cache's size bound: the SDK's own default, 40 MB, written down.\n * See \"The durable cache's size\" on {@link localCacheFor}.\n */\nexport const DURABLE_CACHE_SIZE_BYTES = 40 * 1024 * 1024\n\n/**\n * The Firestore `localCache` an origin of this persistence class may use\n * (AGL-1456).\n *\n * ## Why this keys off the *auth* persistence class\n *\n * The name is historical — the class predates this file (AGL-1379) — but the\n * question it answers was never auth-specific. `AuthPersistenceClass` is\n * documented as *\"how much of a session an origin is allowed to keep\"*, and\n * AGL-1099a's D6 justified `ephemeral` on the grounds that **\"the customer can\n * re-point the DNS and read this origin.\"**\n *\n * That reasoning does not stop at the credential. It is equally true of every\n * document body Firestore caches in IndexedDB on that origin — measured, not\n * argued: the PoC (`docs/design/agl-1099a-poc-findings.md` §5) dumped every\n * object store on the origin and found full document contents under\n * `firestore/<app>/<project>/main → remoteDocumentsV14`, alongside\n * `firestore_clients_*` / `firestore_online_state_*` /\n * `firestore_sequence_number_*` in `localStorage`. D6 hardened the key and\n * left the safe behind it open; this closes it.\n *\n * **One declaration, two consequences — deliberately.** A separate\n * `firestoreCache` prop would let a caller declare a custom console domain\n * `ephemeral` for auth and still hand it a persistent cache, which is exactly\n * the split that produced this bug. There is one fact about the origin, so\n * there is one place to state it.\n *\n * ## The two classes\n *\n * - `durable` → `persistentLocalCache` with the multi-tab manager. Unchanged\n * from what every host has always run, and it must stay that way:\n * `*.aglyn.com` / `*.aglyn.app` are ours forever, and this cache is part of\n * why console read volume is what it is (AGL-1440).\n * - `ephemeral` → `memoryLocalCache`, so **nothing** reaches disk on an origin\n * whose DNS the customer can re-point at their own server.\n *\n * ## The durable cache's size\n *\n * `cacheSizeBytes` is spelled out at the SDK default so the bound is stated\n * here and cannot quietly become `CACHE_SIZE_UNLIMITED` if that default moves.\n * Stating it changes nothing at runtime, and what it governs is narrower than\n * the name suggests — measured against `@firebase/firestore` 4.17.1:\n *\n * - LRU collection is on without it: an unset `cacheSizeBytes` resolves to\n * `LruParams.DEFAULT`, the same 40 MB threshold.\n * - It runs only in the tab holding the primary lease, 1 minute after that tab\n * becomes primary and every 5 minutes after.\n * - The threshold is compared with the byte size of **cached documents**\n * (`remoteDocumentsV14`) and nothing else. `targets` and `targetDocuments`\n * rows are collected alongside documents once documents cross it, never\n * before.\n * - It never touches `localStorage`, where the multi-tab manager strands a\n * record per tab and per query target; {@link pruneSharedClientStateFor}\n * handles those.\n *\n * It is deliberately not lowered. A smaller cache evicts exactly the documents\n * that make this cache worth having — a cold load resuming from disk, and\n * listeners painting through a stale session — and a collected target is given\n * a NEW id the next time its query is listened to, which adds a `localStorage`\n * record rather than removing one.\n *\n * ## Why the memory cache gets the LRU collector, not the default\n *\n * `memoryLocalCache()` defaults to `memoryEagerGarbageCollector`, which drops\n * a document the moment no listener references it — so navigating away from a\n * console page and back re-reads its whole working set from the server. The\n * LRU collector keeps documents in the JS heap up to its 40 MB default instead,\n * which recovers most of the intra-session read saving `persistentLocalCache`\n * was giving us.\n *\n * It costs nothing against the property being protected: the LRU collector is\n * still **memory only**. Nothing is written to IndexedDB, nothing survives a\n * tab close, and a page load on a re-pointed origin starts empty — the same\n * bound `inMemoryPersistence` gives the refresh token.\n *\n * The read cost that remains is real and is the stated trade: a **cold** page\n * load on a custom console domain re-reads its working set where a durable\n * origin would have resumed from disk, and multiple tabs no longer share one\n * backend connection through `persistentMultipleTabManager`. That is the right\n * trade for an Enterprise-only feature, but it is a trade.\n *\n * ## `Failed to obtain primary lease for action '…'` is not a bug to fix\n *\n * Exactly one open tab holds the IndexedDB primary lease and applies remote\n * events; the rest read the results it writes. When the lease changes hands —\n * a tab opening, closing, or coming back to the foreground — a transaction\n * already in flight on the losing tab finds itself no longer primary, and the\n * SDK `logError`s that message immediately before throwing a\n * `failed-precondition` that its OWN multi-tab sync engine catches\n * (`ignoreIfPrimaryLeaseLoss`) and downgrades to a debug line. The tab then\n * continues as a secondary. Nothing is dropped and no listener stops.\n *\n * It is therefore noise from a handled handoff, and it scales with how many\n * tabs are churning rather than with anything the console does. The message\n * names a lease, so the obvious reading is that the multi-tab manager is\n * missing and should be added — it is already here, which is why the message\n * appears at all: `persistentSingleTabManager` refuses the second tab's\n * persistence outright instead of sharing it. Confirmed at runtime by the\n * `owner` record in `firestore/<app>/<project>/main`, which carries\n * `allowTabSynchronization: true`.\n */\nexport function localCacheFor(\n originClass: AuthPersistenceClass,\n): FirestoreLocalCache {\n if (originClass === 'ephemeral') {\n return memoryLocalCache({ garbageCollector: memoryLruGarbageCollector() })\n }\n return persistentLocalCache({\n tabManager: persistentMultipleTabManager(),\n cacheSizeBytes: DURABLE_CACHE_SIZE_BYTES,\n })\n}\n\n/**\n * The cache class THIS TAB's Firestore runs, which is the origin's own class\n * unless the tab has fallen back to the memory cache (AGL-3428).\n *\n * A durable origin's tab that found the shared IndexedDB locked by another\n * tab records a fallback in its `sessionStorage` and reloads; this is where\n * that reload picks it up. The answer is `ephemeral` then because the\n * ephemeral cache is exactly what the tab needs — memory only, shared with no\n * other tab — and nothing else keys off this answer: the AUTH persistence of\n * the tab keeps the origin's class, so the session survives the reload.\n * Every other tab keeps the durable cache. See `firestore-multitab-wedge.ts`.\n */\nexport function firestoreCacheClassFor(\n originClass: AuthPersistenceClass,\n fallbackActive: () => boolean = () => memoryCacheFallbackActive(),\n): AuthPersistenceClass {\n if (originClass === 'durable' && fallbackActive()) return 'ephemeral'\n return originClass\n}\n\n/**\n * Prunes the multi-tab records the durable cache strands in `localStorage`\n * (AGL-2845), for an app whose Firestore this page has just initialized.\n *\n * Keyed off the same origin class as {@link localCacheFor}, because the\n * durable class is the only one running `persistentMultipleTabManager`, the\n * only writer of those records. An ephemeral origin's memory cache writes\n * none, so it resolves without looking.\n *\n * Fire-and-forget: it never rejects and nothing waits on it. What it deletes,\n * and why a live tab's records never are, is in\n * `firestore-shared-client-state.ts`.\n */\nexport function pruneSharedClientStateFor(\n originClass: AuthPersistenceClass,\n app: FirebaseApp,\n): Promise<SharedClientStatePrunePlan | undefined> {\n if (originClass === 'ephemeral') return Promise.resolve(undefined)\n return pruneBrowserSharedClientState(app.name, app.options?.projectId)\n}\n"],"names":["memoryLocalCache","memoryLruGarbageCollector","persistentLocalCache","persistentMultipleTabManager","memoryCacheFallbackActive","pruneBrowserSharedClientState","DURABLE_CACHE_SIZE_BYTES","localCacheFor","originClass","garbageCollector","tabManager","cacheSizeBytes","firestoreCacheClassFor","fallbackActive","pruneSharedClientStateFor","app","Promise","resolve","undefined","name","options","projectId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAEEA,gBAAgB,EAChBC,yBAAyB,EACzBC,oBAAoB,EACpBC,4BAA4B,QACvB,qBAAoB;AAG3B,SAASC,yBAAyB,QAAQ,gCAA4B;AACtE,SACEC,6BAA6B,QAExB,qCAAiC;AAExC;;;CAGC,GACD,OAAO,MAAMC,2BAA2B,KAAK,OAAO,KAAI;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoGC,GACD,OAAO,SAASC,cACdC,WAAiC;IAEjC,IAAIA,gBAAgB,aAAa;QAC/B,OAAOR,iBAAiB;YAAES,kBAAkBR;QAA4B;IAC1E;IACA,OAAOC,qBAAqB;QAC1BQ,YAAYP;QACZQ,gBAAgBL;IAClB;AACF;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASM,uBACdJ,WAAiC,EACjCK,iBAAgC,IAAMT,2BAA2B;IAEjE,IAAII,gBAAgB,aAAaK,kBAAkB,OAAO;IAC1D,OAAOL;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASM,0BACdN,WAAiC,EACjCO,GAAgB;QAG+BA;IAD/C,IAAIP,gBAAgB,aAAa,OAAOQ,QAAQC,OAAO,CAACC;IACxD,OAAOb,8BAA8BU,IAAII,IAAI,GAAEJ,eAAAA,IAAIK,OAAO,qBAAXL,aAAaM,SAAS;AACvE"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type Firestore } from 'firebase/firestore';
|
|
18
|
+
/**
|
|
19
|
+
* A MULTI-TAB CACHE THAT ANOTHER TAB HAS LOCKED, AND THE WAY OUT (AGL-3428).
|
|
20
|
+
*
|
|
21
|
+
* ## The wedge the network cycle cannot clear
|
|
22
|
+
*
|
|
23
|
+
* `firestore-stall-recovery.ts` recovers a lapsed primary lease by cycling the
|
|
24
|
+
* network, which runs the lease election at once (AGL-3373). That needs the
|
|
25
|
+
* election's IndexedDB transaction to run. Measured against
|
|
26
|
+
* `@firebase/firestore` 4.17.2: `IndexedDbPersistence.runTransaction` opens
|
|
27
|
+
* EVERY transaction over EVERY object store (`getObjectStores(schemaVersion)`),
|
|
28
|
+
* so one tab that stops partway through a readwrite transaction — frozen or
|
|
29
|
+
* discarded by the browser while it runs — holds every store for every tab on
|
|
30
|
+
* the origin. Behind it, in every other tab:
|
|
31
|
+
*
|
|
32
|
+
* - the lease election waits, so the lease row is never renewed by anyone;
|
|
33
|
+
* - cache reads wait, so pages sit on their spinners;
|
|
34
|
+
* - the mutation-queue write waits, so a save never reaches the network;
|
|
35
|
+
* - a freshly opened tab's persistence `start()` waits, so a new tab hangs too;
|
|
36
|
+
* - `disableNetwork`, `enableNetwork` and `terminate` are queued on the same
|
|
37
|
+
* AsyncQueue behind the stuck operation, so none of them ever runs.
|
|
38
|
+
*
|
|
39
|
+
* On 2026-09-30 production (beta.218) did exactly this three times in two
|
|
40
|
+
* hours: every console tab stopped, and the `owner` row named a background
|
|
41
|
+
* tab that had stopped renewing it.
|
|
42
|
+
*
|
|
43
|
+
* ## How it is told apart from a slow read
|
|
44
|
+
*
|
|
45
|
+
* Only after the network cycle, and only from a visible tab on a browser that
|
|
46
|
+
* reports a network. Such a tab wins a lapsed lease within one 4-second
|
|
47
|
+
* election tick, and the cycle has just forced one. So, after a short settle,
|
|
48
|
+
* the tab reads the `owner` row itself:
|
|
49
|
+
*
|
|
50
|
+
* - **the read cannot complete in time** — the store is locked by another
|
|
51
|
+
* tab's open transaction; or
|
|
52
|
+
* - **the lease is older than {@link STALE_PRIMARY_LEASE_MS}** — no tab,
|
|
53
|
+
* including this one, is completing an election.
|
|
54
|
+
*
|
|
55
|
+
* Either is the wedge. A fresh lease — this tab's or another's — is a healthy
|
|
56
|
+
* cache and a merely slow server, and nothing more happens.
|
|
57
|
+
*
|
|
58
|
+
* ## The way out
|
|
59
|
+
*
|
|
60
|
+
* The tab records a {@link MemoryCacheFallback} in `sessionStorage` and
|
|
61
|
+
* reloads. `firestoreCacheClassFor` reads it at startup and gives this tab the
|
|
62
|
+
* memory cache, which shares nothing with other tabs, so the locked database
|
|
63
|
+
* cannot reach it. `sessionStorage` is per tab: other tabs keep the durable
|
|
64
|
+
* cache, and the fallback leaves when this tab closes or after
|
|
65
|
+
* {@link MEMORY_CACHE_FALLBACK_TTL_MS}.
|
|
66
|
+
*
|
|
67
|
+
* A reload, not an in-place swap, because nothing in place can work: the
|
|
68
|
+
* wedged instance's `terminate()` is queued behind the stuck operation, a
|
|
69
|
+
* second instance for the same app throws until the first is gone, and
|
|
70
|
+
* hundreds of readers hold the instance from context and from closures.
|
|
71
|
+
* Nothing that could be saved is lost — the wedge had already stopped every
|
|
72
|
+
* write in the tab from leaving it.
|
|
73
|
+
*/
|
|
74
|
+
/** `sessionStorage` key of this tab's memory-cache fallback. */
|
|
75
|
+
export declare const MEMORY_CACHE_FALLBACK_KEY = "aglyn:firestore-memory-cache-fallback";
|
|
76
|
+
/**
|
|
77
|
+
* How long a tab keeps the memory cache after falling back. The SDK's own
|
|
78
|
+
* inactivity horizon (`MAX_CLIENT_AGE_MS`), after which a tab that is still
|
|
79
|
+
* stuck no longer counts as a client; past it, a reload tries the durable
|
|
80
|
+
* cache again, and falls back again if it has to.
|
|
81
|
+
*/
|
|
82
|
+
export declare const MEMORY_CACHE_FALLBACK_TTL_MS: number;
|
|
83
|
+
/**
|
|
84
|
+
* A lease older than this, read from a visible online tab right after a
|
|
85
|
+
* forced election, means no election is completing. The SDK treats a lease as
|
|
86
|
+
* lapsed at 5 s (`MAX_PRIMARY_ELIGIBLE_AGE_MS`) and runs the election every
|
|
87
|
+
* 4 s; this is three times the first, so an election that is merely late is
|
|
88
|
+
* never mistaken for one that cannot run.
|
|
89
|
+
*/
|
|
90
|
+
export declare const STALE_PRIMARY_LEASE_MS = 15000;
|
|
91
|
+
/**
|
|
92
|
+
* How long the lease read may take before the store is judged locked. The
|
|
93
|
+
* SDK's own transactions take milliseconds.
|
|
94
|
+
*/
|
|
95
|
+
export declare const LEASE_PROBE_TIMEOUT_MS = 4000;
|
|
96
|
+
/**
|
|
97
|
+
* How long after the network cycle the lease is read, so the election the
|
|
98
|
+
* cycle queued has run.
|
|
99
|
+
*/
|
|
100
|
+
export declare const LEASE_SETTLE_MS = 3000;
|
|
101
|
+
/** What reading the lease found. */
|
|
102
|
+
export type PrimaryLeaseProbe =
|
|
103
|
+
/** The read did not complete in time: another tab's transaction holds the store. */
|
|
104
|
+
{
|
|
105
|
+
state: 'locked';
|
|
106
|
+
}
|
|
107
|
+
/** The lease row: who holds it and how long ago it was renewed. */
|
|
108
|
+
| {
|
|
109
|
+
state: 'leased';
|
|
110
|
+
ownerId: string;
|
|
111
|
+
ageMs: number;
|
|
112
|
+
}
|
|
113
|
+
/** There is no lease row. */
|
|
114
|
+
| {
|
|
115
|
+
state: 'vacant';
|
|
116
|
+
}
|
|
117
|
+
/** The lease could not be read: no IndexedDB, no database, no store, an error. */
|
|
118
|
+
| {
|
|
119
|
+
state: 'unknown';
|
|
120
|
+
};
|
|
121
|
+
/** Why a tab concluded the multi-tab cache is wedged. */
|
|
122
|
+
export type WedgeEvidence = {
|
|
123
|
+
reason: 'lease-locked';
|
|
124
|
+
} | {
|
|
125
|
+
reason: 'lease-stale';
|
|
126
|
+
ownerId: string;
|
|
127
|
+
ageMs: number;
|
|
128
|
+
};
|
|
129
|
+
/** The fallback this tab recorded, and why. */
|
|
130
|
+
export type MemoryCacheFallback = WedgeEvidence & {
|
|
131
|
+
at: number;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Reads the SDK's `owner` row from `databaseName`.
|
|
135
|
+
*
|
|
136
|
+
* Opens only a database that already exists, at whatever version it has (the
|
|
137
|
+
* same rule as `readHeartbeatClientIds`): `open(name)` on a missing database
|
|
138
|
+
* would create it at version 1 under the SDK. The connection serves one
|
|
139
|
+
* read-only request and is closed — also on `versionchange`, so it never
|
|
140
|
+
* holds up the SDK's own upgrade, and also when the read lands after the
|
|
141
|
+
* timeout.
|
|
142
|
+
*/
|
|
143
|
+
export declare function probePrimaryLease(factory: IDBFactory | undefined, databaseName: string, { timeoutMs, now, }?: {
|
|
144
|
+
timeoutMs?: number;
|
|
145
|
+
now?: () => number;
|
|
146
|
+
}): Promise<PrimaryLeaseProbe>;
|
|
147
|
+
/**
|
|
148
|
+
* Whether a lease read taken right after a forced election, from a visible
|
|
149
|
+
* online tab, shows the wedge. Anything that could be a healthy cache —
|
|
150
|
+
* a fresh lease, no lease row, an unreadable one — is not evidence.
|
|
151
|
+
*/
|
|
152
|
+
export declare function wedgeEvidenceFrom(probe: PrimaryLeaseProbe, staleMs?: number): WedgeEvidence | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* This tab's fallback, or `undefined` when there is none, it has expired, or
|
|
155
|
+
* it does not parse.
|
|
156
|
+
*/
|
|
157
|
+
export declare function readMemoryCacheFallback(storage: Pick<Storage, 'getItem'> | undefined, now: number, ttlMs?: number): MemoryCacheFallback | undefined;
|
|
158
|
+
/** Whether this browser tab is running on its memory-cache fallback. */
|
|
159
|
+
export declare function memoryCacheFallbackActive(now?: number): boolean;
|
|
160
|
+
export interface WedgeEscalationDeps {
|
|
161
|
+
/** Reads the lease. */
|
|
162
|
+
probe: () => Promise<PrimaryLeaseProbe>;
|
|
163
|
+
/** Moves this tab off the shared cache. */
|
|
164
|
+
fallBack: (evidence: WedgeEvidence) => void;
|
|
165
|
+
/** Whether this tab is already on the fallback — it then never escalates. */
|
|
166
|
+
isFallbackActive?: () => boolean;
|
|
167
|
+
isOnline?: () => boolean;
|
|
168
|
+
isVisible?: () => boolean;
|
|
169
|
+
/** Waits `ms`; the settle before the lease read. */
|
|
170
|
+
wait?: (ms: number) => Promise<void>;
|
|
171
|
+
settleMs?: number;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The step `createStallRecovery` runs after its network cycle for a
|
|
175
|
+
* multi-tab client. Resolves `true` when it has moved the tab to the fallback.
|
|
176
|
+
* Never rejects.
|
|
177
|
+
*/
|
|
178
|
+
export declare function createWedgeEscalation(deps: WedgeEscalationDeps): () => Promise<boolean>;
|
|
179
|
+
/**
|
|
180
|
+
* Records the fallback for this tab and reloads it onto the memory cache.
|
|
181
|
+
* When the record cannot be written it does not reload: without it, the
|
|
182
|
+
* reloaded tab would open the same locked database and hang again.
|
|
183
|
+
*/
|
|
184
|
+
export declare function fallBackToMemoryCache(evidence: WedgeEvidence, { storage, reload, now, }?: {
|
|
185
|
+
storage?: Pick<Storage, 'setItem'>;
|
|
186
|
+
reload?: () => void;
|
|
187
|
+
now?: () => number;
|
|
188
|
+
}): void;
|
|
189
|
+
/**
|
|
190
|
+
* Registers `firestore` as running the durable multi-tab cache under
|
|
191
|
+
* `databaseName` (`<persistence prefix>main`). Only a registered client ever
|
|
192
|
+
* escalates past the network cycle.
|
|
193
|
+
*/
|
|
194
|
+
export declare function markMultiTabFirestore(firestore: Firestore, databaseName: string): void;
|
|
195
|
+
/**
|
|
196
|
+
* The browser escalation for `firestore`, or `undefined` when it does not run
|
|
197
|
+
* the durable multi-tab cache (the emulator configuration, an ephemeral
|
|
198
|
+
* origin, a tab already on the fallback).
|
|
199
|
+
*/
|
|
200
|
+
export declare function browserWedgeEscalationFor(firestore: Firestore): (() => Promise<boolean>) | undefined;
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
/**
|
|
18
|
+
* A MULTI-TAB CACHE THAT ANOTHER TAB HAS LOCKED, AND THE WAY OUT (AGL-3428).
|
|
19
|
+
*
|
|
20
|
+
* ## The wedge the network cycle cannot clear
|
|
21
|
+
*
|
|
22
|
+
* `firestore-stall-recovery.ts` recovers a lapsed primary lease by cycling the
|
|
23
|
+
* network, which runs the lease election at once (AGL-3373). That needs the
|
|
24
|
+
* election's IndexedDB transaction to run. Measured against
|
|
25
|
+
* `@firebase/firestore` 4.17.2: `IndexedDbPersistence.runTransaction` opens
|
|
26
|
+
* EVERY transaction over EVERY object store (`getObjectStores(schemaVersion)`),
|
|
27
|
+
* so one tab that stops partway through a readwrite transaction — frozen or
|
|
28
|
+
* discarded by the browser while it runs — holds every store for every tab on
|
|
29
|
+
* the origin. Behind it, in every other tab:
|
|
30
|
+
*
|
|
31
|
+
* - the lease election waits, so the lease row is never renewed by anyone;
|
|
32
|
+
* - cache reads wait, so pages sit on their spinners;
|
|
33
|
+
* - the mutation-queue write waits, so a save never reaches the network;
|
|
34
|
+
* - a freshly opened tab's persistence `start()` waits, so a new tab hangs too;
|
|
35
|
+
* - `disableNetwork`, `enableNetwork` and `terminate` are queued on the same
|
|
36
|
+
* AsyncQueue behind the stuck operation, so none of them ever runs.
|
|
37
|
+
*
|
|
38
|
+
* On 2026-09-30 production (beta.218) did exactly this three times in two
|
|
39
|
+
* hours: every console tab stopped, and the `owner` row named a background
|
|
40
|
+
* tab that had stopped renewing it.
|
|
41
|
+
*
|
|
42
|
+
* ## How it is told apart from a slow read
|
|
43
|
+
*
|
|
44
|
+
* Only after the network cycle, and only from a visible tab on a browser that
|
|
45
|
+
* reports a network. Such a tab wins a lapsed lease within one 4-second
|
|
46
|
+
* election tick, and the cycle has just forced one. So, after a short settle,
|
|
47
|
+
* the tab reads the `owner` row itself:
|
|
48
|
+
*
|
|
49
|
+
* - **the read cannot complete in time** — the store is locked by another
|
|
50
|
+
* tab's open transaction; or
|
|
51
|
+
* - **the lease is older than {@link STALE_PRIMARY_LEASE_MS}** — no tab,
|
|
52
|
+
* including this one, is completing an election.
|
|
53
|
+
*
|
|
54
|
+
* Either is the wedge. A fresh lease — this tab's or another's — is a healthy
|
|
55
|
+
* cache and a merely slow server, and nothing more happens.
|
|
56
|
+
*
|
|
57
|
+
* ## The way out
|
|
58
|
+
*
|
|
59
|
+
* The tab records a {@link MemoryCacheFallback} in `sessionStorage` and
|
|
60
|
+
* reloads. `firestoreCacheClassFor` reads it at startup and gives this tab the
|
|
61
|
+
* memory cache, which shares nothing with other tabs, so the locked database
|
|
62
|
+
* cannot reach it. `sessionStorage` is per tab: other tabs keep the durable
|
|
63
|
+
* cache, and the fallback leaves when this tab closes or after
|
|
64
|
+
* {@link MEMORY_CACHE_FALLBACK_TTL_MS}.
|
|
65
|
+
*
|
|
66
|
+
* A reload, not an in-place swap, because nothing in place can work: the
|
|
67
|
+
* wedged instance's `terminate()` is queued behind the stuck operation, a
|
|
68
|
+
* second instance for the same app throws until the first is gone, and
|
|
69
|
+
* hundreds of readers hold the instance from context and from closures.
|
|
70
|
+
* Nothing that could be saved is lost — the wedge had already stopped every
|
|
71
|
+
* write in the tab from leaving it.
|
|
72
|
+
*/ /** `sessionStorage` key of this tab's memory-cache fallback. */ export const MEMORY_CACHE_FALLBACK_KEY = 'aglyn:firestore-memory-cache-fallback';
|
|
73
|
+
/**
|
|
74
|
+
* How long a tab keeps the memory cache after falling back. The SDK's own
|
|
75
|
+
* inactivity horizon (`MAX_CLIENT_AGE_MS`), after which a tab that is still
|
|
76
|
+
* stuck no longer counts as a client; past it, a reload tries the durable
|
|
77
|
+
* cache again, and falls back again if it has to.
|
|
78
|
+
*/ export const MEMORY_CACHE_FALLBACK_TTL_MS = 30 * 60 * 1000;
|
|
79
|
+
/**
|
|
80
|
+
* A lease older than this, read from a visible online tab right after a
|
|
81
|
+
* forced election, means no election is completing. The SDK treats a lease as
|
|
82
|
+
* lapsed at 5 s (`MAX_PRIMARY_ELIGIBLE_AGE_MS`) and runs the election every
|
|
83
|
+
* 4 s; this is three times the first, so an election that is merely late is
|
|
84
|
+
* never mistaken for one that cannot run.
|
|
85
|
+
*/ export const STALE_PRIMARY_LEASE_MS = 15000;
|
|
86
|
+
/**
|
|
87
|
+
* How long the lease read may take before the store is judged locked. The
|
|
88
|
+
* SDK's own transactions take milliseconds.
|
|
89
|
+
*/ export const LEASE_PROBE_TIMEOUT_MS = 4000;
|
|
90
|
+
/**
|
|
91
|
+
* How long after the network cycle the lease is read, so the election the
|
|
92
|
+
* cycle queued has run.
|
|
93
|
+
*/ export const LEASE_SETTLE_MS = 3000;
|
|
94
|
+
/** The SDK's lease store and its single row's key (`DbPrimaryClientStore`, `DbPrimaryClientKey`). */ const OWNER_STORE = 'owner';
|
|
95
|
+
const OWNER_KEY = 'owner';
|
|
96
|
+
/**
|
|
97
|
+
* Reads the SDK's `owner` row from `databaseName`.
|
|
98
|
+
*
|
|
99
|
+
* Opens only a database that already exists, at whatever version it has (the
|
|
100
|
+
* same rule as `readHeartbeatClientIds`): `open(name)` on a missing database
|
|
101
|
+
* would create it at version 1 under the SDK. The connection serves one
|
|
102
|
+
* read-only request and is closed — also on `versionchange`, so it never
|
|
103
|
+
* holds up the SDK's own upgrade, and also when the read lands after the
|
|
104
|
+
* timeout.
|
|
105
|
+
*/ export async function probePrimaryLease(factory, databaseName, { timeoutMs = LEASE_PROBE_TIMEOUT_MS, now = Date.now } = {}) {
|
|
106
|
+
if (!factory || typeof factory.databases !== 'function') return {
|
|
107
|
+
state: 'unknown'
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
const databases = await factory.databases();
|
|
111
|
+
if (!databases.some((database)=>database.name === databaseName)) {
|
|
112
|
+
return {
|
|
113
|
+
state: 'unknown'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
} catch (unused) {
|
|
117
|
+
return {
|
|
118
|
+
state: 'unknown'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return new Promise((resolve)=>{
|
|
122
|
+
let settled = false;
|
|
123
|
+
let database;
|
|
124
|
+
const settle = (probe)=>{
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
resolve(probe);
|
|
129
|
+
};
|
|
130
|
+
const close = ()=>{
|
|
131
|
+
try {
|
|
132
|
+
database == null ? void 0 : database.close();
|
|
133
|
+
} catch (unused) {
|
|
134
|
+
// Already closed.
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
// Only a lock can hold a read-only request this long; the connection is
|
|
138
|
+
// closed when the request finally lands.
|
|
139
|
+
const timer = setTimeout(()=>settle({
|
|
140
|
+
state: 'locked'
|
|
141
|
+
}), timeoutMs);
|
|
142
|
+
let request;
|
|
143
|
+
try {
|
|
144
|
+
request = factory.open(databaseName);
|
|
145
|
+
} catch (unused) {
|
|
146
|
+
settle({
|
|
147
|
+
state: 'unknown'
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// The database was deleted between `databases()` and `open()`: abort the
|
|
152
|
+
// creation rather than leave the SDK an empty version-1 database.
|
|
153
|
+
request.onupgradeneeded = ()=>{
|
|
154
|
+
var _request_transaction;
|
|
155
|
+
return (_request_transaction = request.transaction) == null ? void 0 : _request_transaction.abort();
|
|
156
|
+
};
|
|
157
|
+
request.onerror = ()=>settle({
|
|
158
|
+
state: 'unknown'
|
|
159
|
+
});
|
|
160
|
+
request.onsuccess = ()=>{
|
|
161
|
+
database = request.result;
|
|
162
|
+
database.onversionchange = close;
|
|
163
|
+
if (settled || !database.objectStoreNames.contains(OWNER_STORE)) {
|
|
164
|
+
close();
|
|
165
|
+
settle({
|
|
166
|
+
state: 'unknown'
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const read = database.transaction(OWNER_STORE, 'readonly').objectStore(OWNER_STORE).get(OWNER_KEY);
|
|
172
|
+
read.onsuccess = ()=>{
|
|
173
|
+
close();
|
|
174
|
+
const row = read.result;
|
|
175
|
+
if (!row || typeof row.ownerId !== 'string' || typeof row.leaseTimestampMs !== 'number') {
|
|
176
|
+
settle({
|
|
177
|
+
state: 'vacant'
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
settle({
|
|
182
|
+
state: 'leased',
|
|
183
|
+
ownerId: row.ownerId,
|
|
184
|
+
ageMs: now() - row.leaseTimestampMs
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
read.onerror = ()=>{
|
|
188
|
+
close();
|
|
189
|
+
settle({
|
|
190
|
+
state: 'unknown'
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
} catch (unused) {
|
|
194
|
+
close();
|
|
195
|
+
settle({
|
|
196
|
+
state: 'unknown'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether a lease read taken right after a forced election, from a visible
|
|
204
|
+
* online tab, shows the wedge. Anything that could be a healthy cache —
|
|
205
|
+
* a fresh lease, no lease row, an unreadable one — is not evidence.
|
|
206
|
+
*/ export function wedgeEvidenceFrom(probe, staleMs = STALE_PRIMARY_LEASE_MS) {
|
|
207
|
+
if (probe.state === 'locked') return {
|
|
208
|
+
reason: 'lease-locked'
|
|
209
|
+
};
|
|
210
|
+
if (probe.state === 'leased' && probe.ageMs >= staleMs) {
|
|
211
|
+
return {
|
|
212
|
+
reason: 'lease-stale',
|
|
213
|
+
ownerId: probe.ownerId,
|
|
214
|
+
ageMs: probe.ageMs
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* This tab's fallback, or `undefined` when there is none, it has expired, or
|
|
221
|
+
* it does not parse.
|
|
222
|
+
*/ export function readMemoryCacheFallback(storage, now, ttlMs = MEMORY_CACHE_FALLBACK_TTL_MS) {
|
|
223
|
+
let raw;
|
|
224
|
+
try {
|
|
225
|
+
var _ref;
|
|
226
|
+
raw = (_ref = storage == null ? void 0 : storage.getItem(MEMORY_CACHE_FALLBACK_KEY)) != null ? _ref : null;
|
|
227
|
+
} catch (unused) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
if (raw === null) return undefined;
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = JSON.parse(raw);
|
|
234
|
+
} catch (unused) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
if (typeof parsed !== 'object' || parsed === null) return undefined;
|
|
238
|
+
const record = parsed;
|
|
239
|
+
if (typeof record.at !== 'number' || !Number.isFinite(record.at)) return undefined;
|
|
240
|
+
if (now - record.at >= ttlMs || record.at > now) return undefined;
|
|
241
|
+
return record;
|
|
242
|
+
}
|
|
243
|
+
const browserSessionStorage = ()=>{
|
|
244
|
+
try {
|
|
245
|
+
return typeof window === 'undefined' ? undefined : window.sessionStorage;
|
|
246
|
+
} catch (unused) {
|
|
247
|
+
// Site data blocked: reading `window.sessionStorage` throws.
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
/** Whether this browser tab is running on its memory-cache fallback. */ export function memoryCacheFallbackActive(now = Date.now()) {
|
|
252
|
+
return readMemoryCacheFallback(browserSessionStorage(), now) !== undefined;
|
|
253
|
+
}
|
|
254
|
+
const browserOnline = ()=>typeof navigator === 'undefined' || navigator.onLine !== false;
|
|
255
|
+
const documentVisible = ()=>typeof document === 'undefined' || document.visibilityState !== 'hidden';
|
|
256
|
+
/**
|
|
257
|
+
* The step `createStallRecovery` runs after its network cycle for a
|
|
258
|
+
* multi-tab client. Resolves `true` when it has moved the tab to the fallback.
|
|
259
|
+
* Never rejects.
|
|
260
|
+
*/ export function createWedgeEscalation(deps) {
|
|
261
|
+
const { probe, fallBack, isFallbackActive = ()=>memoryCacheFallbackActive(), isOnline = browserOnline, isVisible = documentVisible, wait = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms)), settleMs = LEASE_SETTLE_MS } = deps;
|
|
262
|
+
return async ()=>{
|
|
263
|
+
try {
|
|
264
|
+
if (isFallbackActive()) return false;
|
|
265
|
+
await wait(settleMs);
|
|
266
|
+
// Both guards again: the reader may have gone, or the network with it.
|
|
267
|
+
if (!isOnline() || !isVisible()) return false;
|
|
268
|
+
const evidence = wedgeEvidenceFrom(await probe());
|
|
269
|
+
if (!evidence) return false;
|
|
270
|
+
fallBack(evidence);
|
|
271
|
+
return true;
|
|
272
|
+
} catch (unused) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Records the fallback for this tab and reloads it onto the memory cache.
|
|
279
|
+
* When the record cannot be written it does not reload: without it, the
|
|
280
|
+
* reloaded tab would open the same locked database and hang again.
|
|
281
|
+
*/ export function fallBackToMemoryCache(evidence, { storage = browserSessionStorage(), reload = ()=>window.location.reload(), now = Date.now } = {}) {
|
|
282
|
+
if (!storage) return;
|
|
283
|
+
const record = _extends({}, evidence, {
|
|
284
|
+
at: now()
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
storage.setItem(MEMORY_CACHE_FALLBACK_KEY, JSON.stringify(record));
|
|
288
|
+
} catch (unused) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
console.warn('[firestore] The multi-tab cache is locked by another tab; reloading this tab on the memory cache (AGL-3428).', record);
|
|
292
|
+
reload();
|
|
293
|
+
}
|
|
294
|
+
/** The IndexedDB database of each client running the durable multi-tab cache. */ const multiTabDatabases = new WeakMap();
|
|
295
|
+
/**
|
|
296
|
+
* Registers `firestore` as running the durable multi-tab cache under
|
|
297
|
+
* `databaseName` (`<persistence prefix>main`). Only a registered client ever
|
|
298
|
+
* escalates past the network cycle.
|
|
299
|
+
*/ export function markMultiTabFirestore(firestore, databaseName) {
|
|
300
|
+
if (firestore && typeof firestore === 'object') {
|
|
301
|
+
multiTabDatabases.set(firestore, databaseName);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The browser escalation for `firestore`, or `undefined` when it does not run
|
|
306
|
+
* the durable multi-tab cache (the emulator configuration, an ephemeral
|
|
307
|
+
* origin, a tab already on the fallback).
|
|
308
|
+
*/ export function browserWedgeEscalationFor(firestore) {
|
|
309
|
+
const databaseName = multiTabDatabases.get(firestore);
|
|
310
|
+
if (databaseName === undefined) return undefined;
|
|
311
|
+
return createWedgeEscalation({
|
|
312
|
+
probe: ()=>{
|
|
313
|
+
let factory;
|
|
314
|
+
try {
|
|
315
|
+
factory = typeof window === 'undefined' ? undefined : window.indexedDB;
|
|
316
|
+
} catch (unused) {
|
|
317
|
+
factory = undefined;
|
|
318
|
+
}
|
|
319
|
+
return probePrimaryLease(factory, databaseName);
|
|
320
|
+
},
|
|
321
|
+
fallBack: (evidence)=>fallBackToMemoryCache(evidence)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//# sourceMappingURL=firestore-multitab-wedge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firestore-multitab-wedge.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 { type Firestore } from 'firebase/firestore'\n\n/**\n * A MULTI-TAB CACHE THAT ANOTHER TAB HAS LOCKED, AND THE WAY OUT (AGL-3428).\n *\n * ## The wedge the network cycle cannot clear\n *\n * `firestore-stall-recovery.ts` recovers a lapsed primary lease by cycling the\n * network, which runs the lease election at once (AGL-3373). That needs the\n * election's IndexedDB transaction to run. Measured against\n * `@firebase/firestore` 4.17.2: `IndexedDbPersistence.runTransaction` opens\n * EVERY transaction over EVERY object store (`getObjectStores(schemaVersion)`),\n * so one tab that stops partway through a readwrite transaction — frozen or\n * discarded by the browser while it runs — holds every store for every tab on\n * the origin. Behind it, in every other tab:\n *\n * - the lease election waits, so the lease row is never renewed by anyone;\n * - cache reads wait, so pages sit on their spinners;\n * - the mutation-queue write waits, so a save never reaches the network;\n * - a freshly opened tab's persistence `start()` waits, so a new tab hangs too;\n * - `disableNetwork`, `enableNetwork` and `terminate` are queued on the same\n * AsyncQueue behind the stuck operation, so none of them ever runs.\n *\n * On 2026-09-30 production (beta.218) did exactly this three times in two\n * hours: every console tab stopped, and the `owner` row named a background\n * tab that had stopped renewing it.\n *\n * ## How it is told apart from a slow read\n *\n * Only after the network cycle, and only from a visible tab on a browser that\n * reports a network. Such a tab wins a lapsed lease within one 4-second\n * election tick, and the cycle has just forced one. So, after a short settle,\n * the tab reads the `owner` row itself:\n *\n * - **the read cannot complete in time** — the store is locked by another\n * tab's open transaction; or\n * - **the lease is older than {@link STALE_PRIMARY_LEASE_MS}** — no tab,\n * including this one, is completing an election.\n *\n * Either is the wedge. A fresh lease — this tab's or another's — is a healthy\n * cache and a merely slow server, and nothing more happens.\n *\n * ## The way out\n *\n * The tab records a {@link MemoryCacheFallback} in `sessionStorage` and\n * reloads. `firestoreCacheClassFor` reads it at startup and gives this tab the\n * memory cache, which shares nothing with other tabs, so the locked database\n * cannot reach it. `sessionStorage` is per tab: other tabs keep the durable\n * cache, and the fallback leaves when this tab closes or after\n * {@link MEMORY_CACHE_FALLBACK_TTL_MS}.\n *\n * A reload, not an in-place swap, because nothing in place can work: the\n * wedged instance's `terminate()` is queued behind the stuck operation, a\n * second instance for the same app throws until the first is gone, and\n * hundreds of readers hold the instance from context and from closures.\n * Nothing that could be saved is lost — the wedge had already stopped every\n * write in the tab from leaving it.\n */\n\n/** `sessionStorage` key of this tab's memory-cache fallback. */\nexport const MEMORY_CACHE_FALLBACK_KEY = 'aglyn:firestore-memory-cache-fallback'\n\n/**\n * How long a tab keeps the memory cache after falling back. The SDK's own\n * inactivity horizon (`MAX_CLIENT_AGE_MS`), after which a tab that is still\n * stuck no longer counts as a client; past it, a reload tries the durable\n * cache again, and falls back again if it has to.\n */\nexport const MEMORY_CACHE_FALLBACK_TTL_MS = 30 * 60 * 1000\n\n/**\n * A lease older than this, read from a visible online tab right after a\n * forced election, means no election is completing. The SDK treats a lease as\n * lapsed at 5 s (`MAX_PRIMARY_ELIGIBLE_AGE_MS`) and runs the election every\n * 4 s; this is three times the first, so an election that is merely late is\n * never mistaken for one that cannot run.\n */\nexport const STALE_PRIMARY_LEASE_MS = 15_000\n\n/**\n * How long the lease read may take before the store is judged locked. The\n * SDK's own transactions take milliseconds.\n */\nexport const LEASE_PROBE_TIMEOUT_MS = 4_000\n\n/**\n * How long after the network cycle the lease is read, so the election the\n * cycle queued has run.\n */\nexport const LEASE_SETTLE_MS = 3_000\n\n/** The SDK's lease store and its single row's key (`DbPrimaryClientStore`, `DbPrimaryClientKey`). */\nconst OWNER_STORE = 'owner'\nconst OWNER_KEY = 'owner'\n\n/** What reading the lease found. */\nexport type PrimaryLeaseProbe =\n /** The read did not complete in time: another tab's transaction holds the store. */\n | { state: 'locked' }\n /** The lease row: who holds it and how long ago it was renewed. */\n | { state: 'leased'; ownerId: string; ageMs: number }\n /** There is no lease row. */\n | { state: 'vacant' }\n /** The lease could not be read: no IndexedDB, no database, no store, an error. */\n | { state: 'unknown' }\n\n/** Why a tab concluded the multi-tab cache is wedged. */\nexport type WedgeEvidence =\n | { reason: 'lease-locked' }\n | { reason: 'lease-stale'; ownerId: string; ageMs: number }\n\n/** The fallback this tab recorded, and why. */\nexport type MemoryCacheFallback = WedgeEvidence & { at: number }\n\n/**\n * Reads the SDK's `owner` row from `databaseName`.\n *\n * Opens only a database that already exists, at whatever version it has (the\n * same rule as `readHeartbeatClientIds`): `open(name)` on a missing database\n * would create it at version 1 under the SDK. The connection serves one\n * read-only request and is closed — also on `versionchange`, so it never\n * holds up the SDK's own upgrade, and also when the read lands after the\n * timeout.\n */\nexport async function probePrimaryLease(\n factory: IDBFactory | undefined,\n databaseName: string,\n {\n timeoutMs = LEASE_PROBE_TIMEOUT_MS,\n now = Date.now,\n }: { timeoutMs?: number; now?: () => number } = {},\n): Promise<PrimaryLeaseProbe> {\n if (!factory || typeof factory.databases !== 'function') return { state: 'unknown' }\n try {\n const databases = await factory.databases()\n if (!databases.some((database) => database.name === databaseName)) {\n return { state: 'unknown' }\n }\n } catch {\n return { state: 'unknown' }\n }\n\n return new Promise((resolve) => {\n let settled = false\n let database: IDBDatabase | undefined\n const settle = (probe: PrimaryLeaseProbe) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n resolve(probe)\n }\n const close = () => {\n try {\n database?.close()\n } catch {\n // Already closed.\n }\n }\n // Only a lock can hold a read-only request this long; the connection is\n // closed when the request finally lands.\n const timer = setTimeout(() => settle({ state: 'locked' }), timeoutMs)\n\n let request: IDBOpenDBRequest\n try {\n request = factory.open(databaseName)\n } catch {\n settle({ state: 'unknown' })\n return\n }\n // The database was deleted between `databases()` and `open()`: abort the\n // creation rather than leave the SDK an empty version-1 database.\n request.onupgradeneeded = () => request.transaction?.abort()\n request.onerror = () => settle({ state: 'unknown' })\n request.onsuccess = () => {\n database = request.result\n database.onversionchange = close\n if (settled || !database.objectStoreNames.contains(OWNER_STORE)) {\n close()\n settle({ state: 'unknown' })\n return\n }\n try {\n const read = database\n .transaction(OWNER_STORE, 'readonly')\n .objectStore(OWNER_STORE)\n .get(OWNER_KEY)\n read.onsuccess = () => {\n close()\n const row = read.result as\n | { ownerId?: unknown; leaseTimestampMs?: unknown }\n | undefined\n if (\n !row ||\n typeof row.ownerId !== 'string' ||\n typeof row.leaseTimestampMs !== 'number'\n ) {\n settle({ state: 'vacant' })\n return\n }\n settle({\n state: 'leased',\n ownerId: row.ownerId,\n ageMs: now() - row.leaseTimestampMs,\n })\n }\n read.onerror = () => {\n close()\n settle({ state: 'unknown' })\n }\n } catch {\n close()\n settle({ state: 'unknown' })\n }\n }\n })\n}\n\n/**\n * Whether a lease read taken right after a forced election, from a visible\n * online tab, shows the wedge. Anything that could be a healthy cache —\n * a fresh lease, no lease row, an unreadable one — is not evidence.\n */\nexport function wedgeEvidenceFrom(\n probe: PrimaryLeaseProbe,\n staleMs = STALE_PRIMARY_LEASE_MS,\n): WedgeEvidence | undefined {\n if (probe.state === 'locked') return { reason: 'lease-locked' }\n if (probe.state === 'leased' && probe.ageMs >= staleMs) {\n return { reason: 'lease-stale', ownerId: probe.ownerId, ageMs: probe.ageMs }\n }\n return undefined\n}\n\n/**\n * This tab's fallback, or `undefined` when there is none, it has expired, or\n * it does not parse.\n */\nexport function readMemoryCacheFallback(\n storage: Pick<Storage, 'getItem'> | undefined,\n now: number,\n ttlMs = MEMORY_CACHE_FALLBACK_TTL_MS,\n): MemoryCacheFallback | undefined {\n let raw: string | null\n try {\n raw = storage?.getItem(MEMORY_CACHE_FALLBACK_KEY) ?? null\n } catch {\n return undefined\n }\n if (raw === null) return undefined\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return undefined\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined\n const record = parsed as Partial<MemoryCacheFallback>\n if (typeof record.at !== 'number' || !Number.isFinite(record.at)) return undefined\n if (now - record.at >= ttlMs || record.at > now) return undefined\n return record as MemoryCacheFallback\n}\n\nconst browserSessionStorage = (): Storage | undefined => {\n try {\n return typeof window === 'undefined' ? undefined : window.sessionStorage\n } catch {\n // Site data blocked: reading `window.sessionStorage` throws.\n return undefined\n }\n}\n\n/** Whether this browser tab is running on its memory-cache fallback. */\nexport function memoryCacheFallbackActive(now = Date.now()): boolean {\n return readMemoryCacheFallback(browserSessionStorage(), now) !== undefined\n}\n\nexport interface WedgeEscalationDeps {\n /** Reads the lease. */\n probe: () => Promise<PrimaryLeaseProbe>\n /** Moves this tab off the shared cache. */\n fallBack: (evidence: WedgeEvidence) => void\n /** Whether this tab is already on the fallback — it then never escalates. */\n isFallbackActive?: () => boolean\n isOnline?: () => boolean\n isVisible?: () => boolean\n /** Waits `ms`; the settle before the lease read. */\n wait?: (ms: number) => Promise<void>\n settleMs?: number\n}\n\nconst browserOnline = () =>\n typeof navigator === 'undefined' || navigator.onLine !== false\n\nconst documentVisible = () =>\n typeof document === 'undefined' || document.visibilityState !== 'hidden'\n\n/**\n * The step `createStallRecovery` runs after its network cycle for a\n * multi-tab client. Resolves `true` when it has moved the tab to the fallback.\n * Never rejects.\n */\nexport function createWedgeEscalation(\n deps: WedgeEscalationDeps,\n): () => Promise<boolean> {\n const {\n probe,\n fallBack,\n isFallbackActive = () => memoryCacheFallbackActive(),\n isOnline = browserOnline,\n isVisible = documentVisible,\n wait = (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),\n settleMs = LEASE_SETTLE_MS,\n } = deps\n return async () => {\n try {\n if (isFallbackActive()) return false\n await wait(settleMs)\n // Both guards again: the reader may have gone, or the network with it.\n if (!isOnline() || !isVisible()) return false\n const evidence = wedgeEvidenceFrom(await probe())\n if (!evidence) return false\n fallBack(evidence)\n return true\n } catch {\n return false\n }\n }\n}\n\n/**\n * Records the fallback for this tab and reloads it onto the memory cache.\n * When the record cannot be written it does not reload: without it, the\n * reloaded tab would open the same locked database and hang again.\n */\nexport function fallBackToMemoryCache(\n evidence: WedgeEvidence,\n {\n storage = browserSessionStorage(),\n reload = () => window.location.reload(),\n now = Date.now,\n }: {\n storage?: Pick<Storage, 'setItem'>\n reload?: () => void\n now?: () => number\n } = {},\n): void {\n if (!storage) return\n const record: MemoryCacheFallback = { ...evidence, at: now() }\n try {\n storage.setItem(MEMORY_CACHE_FALLBACK_KEY, JSON.stringify(record))\n } catch {\n return\n }\n console.warn(\n '[firestore] The multi-tab cache is locked by another tab; reloading this tab on the memory cache (AGL-3428).',\n record,\n )\n reload()\n}\n\n/** The IndexedDB database of each client running the durable multi-tab cache. */\nconst multiTabDatabases = new WeakMap<Firestore, string>()\n\n/**\n * Registers `firestore` as running the durable multi-tab cache under\n * `databaseName` (`<persistence prefix>main`). Only a registered client ever\n * escalates past the network cycle.\n */\nexport function markMultiTabFirestore(firestore: Firestore, databaseName: string): void {\n if (firestore && typeof firestore === 'object') {\n multiTabDatabases.set(firestore, databaseName)\n }\n}\n\n/**\n * The browser escalation for `firestore`, or `undefined` when it does not run\n * the durable multi-tab cache (the emulator configuration, an ephemeral\n * origin, a tab already on the fallback).\n */\nexport function browserWedgeEscalationFor(\n firestore: Firestore,\n): (() => Promise<boolean>) | undefined {\n const databaseName = multiTabDatabases.get(firestore)\n if (databaseName === undefined) return undefined\n return createWedgeEscalation({\n probe: () => {\n let factory: IDBFactory | undefined\n try {\n factory = typeof window === 'undefined' ? undefined : window.indexedDB\n } catch {\n factory = undefined\n }\n return probePrimaryLease(factory, databaseName)\n },\n fallBack: (evidence) => fallBackToMemoryCache(evidence),\n })\n}\n"],"names":["MEMORY_CACHE_FALLBACK_KEY","MEMORY_CACHE_FALLBACK_TTL_MS","STALE_PRIMARY_LEASE_MS","LEASE_PROBE_TIMEOUT_MS","LEASE_SETTLE_MS","OWNER_STORE","OWNER_KEY","probePrimaryLease","factory","databaseName","timeoutMs","now","Date","databases","state","some","database","name","Promise","resolve","settled","settle","probe","clearTimeout","timer","close","setTimeout","request","open","onupgradeneeded","transaction","abort","onerror","onsuccess","result","onversionchange","objectStoreNames","contains","read","objectStore","get","row","ownerId","leaseTimestampMs","ageMs","wedgeEvidenceFrom","staleMs","reason","undefined","readMemoryCacheFallback","storage","ttlMs","raw","getItem","parsed","JSON","parse","record","at","Number","isFinite","browserSessionStorage","window","sessionStorage","memoryCacheFallbackActive","browserOnline","navigator","onLine","documentVisible","document","visibilityState","createWedgeEscalation","deps","fallBack","isFallbackActive","isOnline","isVisible","wait","ms","settleMs","evidence","fallBackToMemoryCache","reload","location","setItem","stringify","console","warn","multiTabDatabases","WeakMap","markMultiTabFirestore","firestore","set","browserWedgeEscalationFor","indexedDB"],"mappings":"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"}
|