@aglyn/tenant-feature-instance 1.0.0-beta.287 → 1.0.0-beta.288

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.287",
3
+ "version": "1.0.0-beta.288",
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.287",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.287",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.287",
31
- "@aglyn/shared-util-email": "1.0.0-beta.287",
32
- "@aglyn/shared-util-http": "1.0.0-beta.287",
33
- "@aglyn/shared-util-timestamp": "1.0.0-beta.287",
28
+ "@aglyn/aglyn": "1.0.0-beta.288",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.288",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.288",
31
+ "@aglyn/shared-util-email": "1.0.0-beta.288",
32
+ "@aglyn/shared-util-http": "1.0.0-beta.288",
33
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.288",
34
34
  "@swc/helpers": "0.5.23",
35
35
  "lodash-es": "^4.18.1",
36
36
  "mobx": "^7.0.5"
@@ -32,7 +32,7 @@ import { authEmulatorUrl, firestoreEmulatorHost } from "../../constants/firebase
32
32
  import { createAuthInstance } from "./auth-persistence.js";
33
33
  import { firestoreCacheClassFor, localCacheFor, pruneSharedClientStateFor } from "./firestore-cache.js";
34
34
  import { keepAppCheckRecaptchaContainer } from "./app-check-recaptcha-container.js";
35
- import { markMultiTabFirestore } from "./firestore-multitab-wedge.js";
35
+ import { confirmSharedCacheLock, markMultiTabFirestore } from "./firestore-multitab-wedge.js";
36
36
  import { firestorePersistencePrefix } from "./firestore-shared-client-state.js";
37
37
  import { startFirestoreTabHandoff } from "./firestore-tab-handoff.js";
38
38
  const FirebaseServicesContext = /*#__PURE__*/ createContext(undefined);
@@ -184,13 +184,18 @@ let analyticsConsentGate = null;
184
184
  * except Analytics, whose consent verdict belongs to the render that reads it.
185
185
  */ function bootFirebaseCore(firebaseConfig, appName, authPersistence) {
186
186
  var _getApps_find;
187
+ var _app_options;
187
188
  const app = (_getApps_find = getApps().find((existing)=>existing.name === appName)) != null ? _getApps_find : initializeApp(firebaseConfig, appName);
188
189
  // `durable` resolves to the same `getAuth(app)` this always called.
189
190
  const auth = createAuthInstance(app, authPersistence);
191
+ // The durable cache's IndexedDB database, `<persistence prefix>main`.
192
+ const persistencePrefix = ((_app_options = app.options) == null ? void 0 : _app_options.projectId) ? firestorePersistencePrefix(app.name, app.options.projectId) : undefined;
193
+ const databaseName = persistencePrefix && `${persistencePrefix}main`;
190
194
  // The origin's class, unless this tab fell back to the memory cache
191
- // because another tab locked the shared one (AGL-3428). Firestore only:
192
- // `auth` above keeps the origin's class.
193
- const firestoreCacheClass = firestoreCacheClassFor(authPersistence);
195
+ // because another tab locked the shared one (AGL-3428), or a tab has just
196
+ // seen that lock (AGL-4090). Firestore only: `auth` above keeps the
197
+ // origin's class.
198
+ const firestoreCacheClass = firestoreCacheClassFor(authPersistence, databaseName);
194
199
  if (!firestoreInitialized.has(appName)) {
195
200
  try {
196
201
  // Under the emulator (dev/e2e only): force long-polling and skip
@@ -235,22 +240,25 @@ let analyticsConsentGate = null;
235
240
  const emulator = firestoreEmulatorHost();
236
241
  connectFirestoreEmulator(getFirestore(app), emulator.host, emulator.port);
237
242
  } else {
238
- var _app_options;
239
243
  // The SDK never sweeps the multi-tab records the durable cache
240
244
  // strands in localStorage, and a full localStorage fails the whole
241
245
  // Firestore client — see `firestore-shared-client-state.ts`.
242
246
  void pruneSharedClientStateFor(firestoreCacheClass, app);
243
247
  // Only the durable multi-tab cache can be locked by another tab,
244
248
  // so only it gets the lease check after a stalled recovery.
245
- if (firestoreCacheClass === 'durable' && ((_app_options = app.options) == null ? void 0 : _app_options.projectId)) {
246
- const prefix = firestorePersistencePrefix(app.name, app.options.projectId);
247
- markMultiTabFirestore(getFirestore(app), `${prefix}main`);
249
+ if (firestoreCacheClass === 'durable' && persistencePrefix && databaseName) {
250
+ markMultiTabFirestore(getFirestore(app), databaseName);
248
251
  // A visible tab opened while a background tab holds the shared
249
252
  // cache's lease takes the lease at boot instead of routing every
250
253
  // query through the background tab until its lease lapses
251
254
  // (AGL-3660). In initializeFirestore's task: its first step
252
255
  // snapshots the other tabs before this tab's client can start.
253
- void startFirestoreTabHandoff(getFirestore(app), prefix);
256
+ void startFirestoreTabHandoff(getFirestore(app), persistencePrefix);
257
+ } else if (authPersistence === 'durable' && databaseName) {
258
+ // A durable origin's tab on the memory cache. If that is because a
259
+ // tab saw the shared cache locked, read the lease once, read-only,
260
+ // so the record lasts only as long as the lock (AGL-4090).
261
+ void confirmSharedCacheLock(databaseName);
254
262
  }
255
263
  }
256
264
  } catch (unused) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firebase-services.tsx"],"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 {\n type FirebaseApp,\n type FirebaseOptions,\n getApps,\n initializeApp,\n} from 'firebase/app'\nimport {\n type Analytics,\n getAnalytics as getAnalyticsInstance,\n initializeAnalytics as initializeAnalyticsInstance,\n} from 'firebase/analytics'\nimport { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check'\nimport { analyticsMayEmit } from '@aglyn/aglyn/app-utils/analytics-environment'\nimport { pushPlatformConsentDefault } from '@aglyn/aglyn/app-utils/platform-consent-default'\nimport { VISITOR_CONSENT_CHANGED_EVENT } from '@aglyn/aglyn/app-utils/visitor-consent'\nimport {\n type Auth,\n connectAuthEmulator,\n onIdTokenChanged,\n type User,\n} from 'firebase/auth'\nimport {\n type Firestore,\n connectFirestoreEmulator,\n getFirestore,\n initializeFirestore,\n} from 'firebase/firestore'\nimport {\n type RemoteConfig,\n getRemoteConfig as getRemoteConfigInstance,\n} from 'firebase/remote-config'\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react'\nimport {\n FIREBASE_AUTH_EMULATOR_ENABLED,\n FIREBASE_FIRESTORE_EMULATOR_ENABLED,\n} from '@aglyn/shared-data-enums'\nimport {\n APP_CHECK_KEY_MISSING_MESSAGE,\n appCheckSiteKey,\n} from '../../constants/firebase-config'\nimport {\n authEmulatorUrl,\n firestoreEmulatorHost,\n} from '../../constants/firebase-emulator-hosts'\nimport {\n type AuthPersistenceClass,\n createAuthInstance,\n} from './auth-persistence'\nimport {\n firestoreCacheClassFor,\n localCacheFor,\n pruneSharedClientStateFor,\n} from './firestore-cache'\nimport { keepAppCheckRecaptchaContainer } from './app-check-recaptcha-container'\nimport { markMultiTabFirestore } from './firestore-multitab-wedge'\nimport { firestorePersistencePrefix } from './firestore-shared-client-state'\nimport { startFirestoreTabHandoff } from './firestore-tab-handoff'\n\n/**\n * Drop-in replacement for reactfire's `ObservableStatus<T>` — reactfire is\n * unmaintained against firebase majors past v9, but its shape is preserved\n * here so `helpers/use-doc.ts`'s already-battle-tested onSnapshot+retry\n * implementation (AGL-216/223) didn't need to change.\n */\nexport interface ObservableStatus<T> {\n status: 'loading' | 'error' | 'success'\n hasEmitted: boolean\n isComplete: boolean\n data: T\n error: Error | undefined\n firstValuePromise: Promise<void>\n /**\n * The emitted `data` includes local writes the server has NOT\n * acknowledged (`snapshot.metadata.hasPendingWrites`).\n *\n * Carried rather than dropped because a caller cannot otherwise tell \"the\n * store holds this\" from \"my browser holds this\". `persistentLocalCache`\n * replays a queued write into the very first snapshot after a reload, so a\n * caller that treats every snapshot as authoritative will adopt its own\n * unacknowledged edit as the saved state — which is how the besigner ended\n * up unable to save real changes (AGL-1262).\n */\n hasPendingWrites: boolean\n /**\n * The emitted `data` came from the local cache and the server has NOT\n * confirmed it (`snapshot.metadata.fromCache`).\n *\n * Carried because it is the only per-listener ground truth about\n * freshness the SDK offers, and the console needs it (AGL-1066). Under\n * `persistentLocalCache` a listener whose server listen is being REFUSED\n * still emits cached snapshots and still reports `status: 'success'` —\n * the retry budget is reset by each cached emission and never spends, so\n * `status` can never reveal the fault (see\n * `use-firestore-collection-cached-retry.spec.ts`).\n *\n * Prefer this over `session-health`'s `staleSession` for anything that\n * asks \"is what I am looking at live?\". `staleSession` is a heuristic\n * needing two denied collections inside a window, and it is unreachable\n * on listener-only pages; this is a fact about THIS listener, costs\n * nothing, and clears itself the instant a server snapshot lands.\n */\n fromCache: boolean\n /**\n * The server has REFUSED this listen for longer than the retry budget, and\n * no server snapshot has arrived since (AGL-1066).\n *\n * `fromCache` says \"unconfirmed\", which is also true offline and true for\n * the first snapshot of a perfectly healthy load. This says \"refused\" —\n * only `permission-denied` counts it, and it takes a whole budget's worth\n * in a row, so it cannot fire offline and cannot fire on the AGL-216/217\n * post-sign-in token race.\n *\n * It is what `status` would say if the retry budget were spendable. It is\n * carried separately because correcting `status` in place would blank\n * every surface that renders cached data through an `error` branch — see\n * the note on `attempt = 0` in `use-firestore-collection`.\n */\n serverDenied: boolean\n}\n\n/** Replaces reactfire's `ReactFireOptions<T>` — drops the `suspense` field, which was never read. */\nexport interface FirestoreDocOptions<T> {\n idField?: string\n initialData?: T\n}\n\n/**\n * The Firebase products this provider builds, and therefore the ones every\n * page that renders it downloads.\n *\n * ⛔ NO Realtime Database and NO Cloud Storage (AGL-2706). Both were built\n * here and published as `useDatabase()` / `useStorage()`, and neither hook had\n * a single caller anywhere in the repo — so `@firebase/database` (125.9 KB\n * raw) and `@firebase/storage` (13.9 KB) were in the first load of every\n * console route to satisfy a context field nothing read.\n *\n * Realtime Database IS used, by `apps/console/hooks/use-presence.ts` and\n * `use-coediting.ts` — on their own Firebase app, with their own\n * `getDatabase`, behind the editor routes that need presence. That is the\n * shape to copy if a surface needs one of these: reach for the product where\n * the feature is, not in the provider every page mounts. A field here is paid\n * for by every route, used or not.\n */\ninterface FirebaseServices {\n app: FirebaseApp\n firestore: Firestore\n auth: Auth\n analytics: Analytics\n remoteConfig: RemoteConfig\n /**\n * The persistence class `auth` was actually created with (AGL-1379).\n *\n * Published so a **second** auth instance derives its class from the\n * primary rather than re-deciding it. `apps/console/hooks/use-presence.ts`\n * builds its own Firebase app and its own `Auth`; before this it took the\n * SDK default unconditionally, so configuring only the provider would have\n * left presence persisting a refresh token on an origin the provider had\n * just been careful not to persist one on.\n */\n authPersistence: AuthPersistenceClass\n}\n\nconst FirebaseServicesContext = createContext<FirebaseServices | undefined>(undefined)\n\n// Module-scope guards so HMR / remounts don't re-run emulator connection\n// (which throws if called twice) or double-initialize Firestore's\n// persistent cache.\n//\n// Firestore's is keyed by app NAME, not a bare boolean (AGL-1456): Firestore\n// initialization is once-per-app, so a module-global flag made the FIRST app\n// to mount decide for every later one — a second app would silently skip\n// `initializeFirestore` and take SDK defaults. That mattered the moment the\n// cache stopped being the same on every host.\nconst firestoreInitialized = new Set<string>()\nlet connectedAuth = false\n\n/**\n * True when THIS Chrome instance is under WebDriver automation control\n * (Puppeteer, Playwright, Selenium, or a raw CDP client) — never true for a\n * person's own browser.\n *\n * The `initializeFirestore` call below already documents the fault this\n * exists for: \"the emulator's WebChannel streaming misbehaves in automated\n * Chrome — listeners serve the initial empty from-cache snapshot and the\n * server sync never arrives\" (AGL-217). That mitigation only reaches\n * `FIREBASE_FIRESTORE_EMULATOR_ENABLED`, so an automated session against a\n * REAL, deployed backend — a browser-automation agent driving a live\n * console, which is exactly what `navigator.webdriver` detects — stayed\n * exposed. `getDocs`/`getDoc` and `onSnapshot` all resolve over the same\n * Watch stream internally, so a one-shot read wedges exactly like a listener\n * does: no error, nothing to catch, `loading` simply never gets its\n * settling event. That is the Media Library grid spinning on \"Loading\n * media…\" indefinitely with a perfectly healthy account and a perfectly\n * healthy network.\n *\n * `undefined` during SSR reads as `false` — `navigator` does not exist in\n * Node, and a server render can only ever produce the auto-detected\n * transport, never the forced one.\n */\nconst isAutomatedChromeSession = (): boolean =>\n typeof navigator !== 'undefined' && navigator.webdriver === true\n\n/**\n * The console tag's gtag config — ONE object, at module scope, deliberately\n * (AGL-1979).\n *\n * `initializeAnalytics(app, options)` throws `already-initialized` unless the\n * options `deepEqual` the ones the instance was first created with, and the\n * provider re-enters this block on every mount, every StrictMode double\n * invoke and every Fast Refresh. A fresh object literal per call survives\n * that only by *value*; a shared constant survives it by *identity*, which is\n * the first branch of the SDK's own comparison and cannot drift when someone\n * edits one call site and not another.\n *\n * ⚠️ The SDK MUTATES this object: `_initializeAnalytics` reads `options.config`\n * and writes `origin`, `update` and the installation id onto it in place.\n * That is precisely why sharing it is safe rather than dangerous — the object\n * the provider holds and the object the SDK stored are the same one, so they\n * cannot disagree.\n */\nconst CONSOLE_ANALYTICS_OPTIONS = {\n config: { send_page_view: false, content_group: 'console' },\n}\n\n/**\n * A surface's answer to \"may an analytics tag exist for this visitor at all?\".\n *\n * Registered from the app, at module scope, in the same shape as\n * `setFirestoreSessionReporters` and `setStaleSessionCheck` — and for a\n * sharper version of the same reason. This is consulted during the provider's\n * FIRST render, before any effect anywhere has run, so a gate installed from\n * an effect would arrive after the decision it exists to make.\n *\n * Answering `false` means the SDK is never asked to initialize: `gtag.js` is\n * never fetched, `window.gtag` never comes into existence, and the shared\n * `deliver()` in `analytics-events.ts` — whose fallback IS `window.gtag` —\n * therefore has nowhere to put a hit. That is the difference between a gate\n * and a suppression, and it is the difference AGL-1608 paid for: a resident\n * tag reports on its own, through enhanced measurement, whatever any flag\n * says afterwards.\n */\nexport type AnalyticsConsentGate = () => boolean\n\nlet analyticsConsentGate: AnalyticsConsentGate | null = null\n\n/**\n * Install the consent gate for this document, or clear it with `null`.\n *\n * UNREGISTERED MEANS ALLOWED, which is deliberate and is the only default that\n * is safe here. The tenant runtime does not use this provider's analytics\n * branch at all (it builds its own tag in `site-analytics.tsx`, behind the\n * AGL-1498 gate), and a self-hosted console points at the operator's own GA\n * property — silencing an operator's analytics because Aglyn's own surface\n * grew a gate is a failure they cannot diagnose. The surface that needs the\n * gate is the one that registers it.\n */\nexport function setAnalyticsConsentGate(\n gate: AnalyticsConsentGate | null,\n): void {\n analyticsConsentGate = gate\n}\n\n/** The gate's verdict, with an unregistered gate reading as \"allowed\". */\nfunction analyticsConsentAllows(): boolean {\n if (analyticsConsentGate === null) return true\n try {\n return analyticsConsentGate() === true\n } catch {\n // A gate that throws cannot be read as consent. Storage can throw in\n // private mode, and \"we could not tell\" is the one state this whole\n // mechanism resolves to \"no\".\n return false\n }\n}\n\n/**\n * The Analytics instance per Firebase app, so a re-boot after a consent change\n * hands back the tag that is already resident rather than asking the SDK for a\n * second one.\n *\n * `initializeAnalytics` is idempotent while the options match, so this is not\n * what makes re-entry safe — `CONSOLE_ANALYTICS_OPTIONS` being a shared\n * constant is. What this avoids is the noisy path: once anything has\n * initialized the provider with different options, every later call throws and\n * is logged, and a withdrawal followed by a re-grant would log it again.\n */\nconst analyticsByApp = new WeakMap<FirebaseApp, Analytics>()\n\n/**\n * Boot the console's Analytics tag, or return the one already booted.\n *\n * Extracted from the provider body so that \"when is the tag created\" and \"may\n * it be created\" are separable: the consent gate can defer this call to a\n * later render without the ordering below moving.\n */\nfunction bootAnalytics(app: FirebaseApp): Analytics | undefined {\n const existing = analyticsByApp.get(app)\n if (existing) return existing\n let analytics: Analytics\n // The region-conditional consent default (AGL-1597), declared BEFORE the\n // SDK boots the tag. Ordering is the whole of it: a `default` read after\n // `config` is not a default, and the SDK issues `config` inside\n // `initializeAnalytics` on the next line. Pushing onto `dataLayer` here\n // works even though gtag.js is not loaded yet — the queue is what makes the\n // ordering expressible.\n //\n // Still declared even though the gate above has already said yes, and the\n // two are not redundant. The gate answers \"may a tag exist\"; the default\n // answers \"what may the tag that does exist do\", per region, from its very\n // first hit. Ad storage stays denied everywhere on this surface.\n pushPlatformConsentDefault(\n typeof window === 'undefined' ? null : (window as never),\n )\n try {\n analytics = initializeAnalyticsInstance(app, CONSOLE_ANALYTICS_OPTIONS)\n } catch (error) {\n console.error(error)\n try {\n // Already initialized by someone else, with someone else's options:\n // take THAT instance rather than leaving consumers with nothing. The\n // tag it is attached to was configured without `send_page_view: false`\n // and without `content_group`, so this session reports a duplicate\n // startup page_view and an unstamped surface — degraded, and loudly so,\n // instead of silently dead.\n analytics = getAnalyticsInstance(app)\n } catch (fallbackError) {\n console.error(fallbackError)\n }\n }\n if (analytics) analyticsByApp.set(app, analytics)\n return analytics\n}\n\n/**\n * The tag this document may have right now: none outside a real production\n * deployment, none while consent is absent, otherwise the booted instance.\n */\nfunction analyticsForConsentState(app: FirebaseApp): Analytics | undefined {\n if (!analyticsMayEmit()) return undefined\n if (!analyticsConsentAllows()) return undefined\n return bootAnalytics(app)\n}\n\nexport interface FirebaseServicesProviderProps {\n firebaseConfig: FirebaseOptions\n appName: string\n /**\n * How much of *anything* this origin may keep on disk. Defaults to\n * `durable`, which is what every host serving this provider today is:\n * `*.aglyn.com` and `*.aglyn.app`, whose DNS we own and whose 14-day\n * session is the product.\n *\n * A **custom console domain** (AGL-1099c) must pass `ephemeral`. That is\n * the whole reason this is a prop rather than a constant: the class differs\n * per origin, not per build, and the decision belongs to whoever knows\n * which origin is being served.\n *\n * **It governs two things, and the name only names one.** It selects the\n * `Auth` persistence class (AGL-1379) *and* the Firestore `localCache`\n * (AGL-1456) — because both answer the same question about the same origin,\n * and splitting them into two props is precisely how the cache came to be\n * unconditional while the credential was hardened. See `firestore-cache.ts`.\n */\n authPersistence?: AuthPersistenceClass\n children?: ReactNode\n}\n\n/** The services every surface boots, before the per-document analytics verdict. */\ninterface FirebaseCoreServices {\n app: FirebaseApp\n firestore: Firestore\n auth: Auth\n remoteConfig: RemoteConfig\n}\n\n/**\n * Create (or adopt) the app, Auth, Firestore, App Check and Remote Config for\n * one Firebase app: everything the provider used to build in its first render\n * except Analytics, whose consent verdict belongs to the render that reads it.\n */\nfunction bootFirebaseCore(\n firebaseConfig: FirebaseOptions,\n appName: string,\n authPersistence: AuthPersistenceClass,\n): FirebaseCoreServices {\n const app =\n getApps().find((existing) => existing.name === appName) ??\n initializeApp(firebaseConfig, appName)\n // `durable` resolves to the same `getAuth(app)` this always called.\n const auth = createAuthInstance(app, authPersistence)\n\n // The origin's class, unless this tab fell back to the memory cache\n // because another tab locked the shared one (AGL-3428). Firestore only:\n // `auth` above keeps the origin's class.\n const firestoreCacheClass = firestoreCacheClassFor(authPersistence)\n if (!firestoreInitialized.has(appName)) {\n try {\n // Under the emulator (dev/e2e only): force long-polling and skip\n // the persistent multi-tab cache. The emulator's WebChannel\n // streaming misbehaves in automated Chrome — listeners serve the\n // initial empty from-cache snapshot and the server sync never\n // arrives, which looks like \"empty pages with zero errors\"\n // (the AGL-217 mystery). Real traffic keeps the default transport\n // and persistent cache — except a WebDriver-controlled Chrome\n // against a real backend, which forces long-polling for the same\n // reason without giving up the cache (see `isAutomatedChromeSession`\n // below).\n //\n // ⚠️ CONSEQUENCE, and it will cost you hours if you do not know it\n // (AGL-1066): every stale-session/stale-cache fault is UNREPRODUCIBLE\n // locally through the app, because the cache that causes them is off\n // in exactly the configuration you would reach for to reproduce one.\n // A listener that keeps serving arbitrarily-old data while the server\n // refuses it, a `noDocument` tombstone that 404s a live host, the\n // retry budget that never spends because a cached emission resets it\n // — none of them can happen here. Test that behaviour with the unit\n // seams instead (`use-firestore-collection-cached-retry.spec.ts`\n // drives cached emissions and denials directly), or against a\n // deployed environment. Do NOT \"fix\" it by turning the cache on for\n // the emulator: the AGL-217 empty-pages-with-zero-errors mystery is\n // what that produces.\n initializeFirestore(\n app,\n FIREBASE_FIRESTORE_EMULATOR_ENABLED\n ? { experimentalForceLongPolling: true }\n : {\n // NOT unconditional (AGL-1456). `persistentLocalCache` writes\n // document bodies to this origin's IndexedDB, so on a custom\n // console domain it is the same exposure D6 removed from the\n // refresh token — see `firestore-cache.ts` for why one\n // declaration governs both.\n localCache: localCacheFor(firestoreCacheClass),\n // The real-backend half of the AGL-217 mitigation above: force\n // long-polling for a WebDriver-controlled Chrome even when it\n // is NOT talking to the emulator, because the WebChannel wedge\n // is a property of automated Chrome, not of which backend it\n // is automating against. Never true for a real visitor, so\n // production traffic keeps the SDK's own auto-detected\n // transport untouched.\n ...(isAutomatedChromeSession()\n ? { experimentalForceLongPolling: true }\n : {}),\n },\n )\n if (FIREBASE_FIRESTORE_EMULATOR_ENABLED) {\n // Where the server was told the emulator is, rather than a default\n // port another session's stack may hold (AGL-2834).\n const emulator = firestoreEmulatorHost()\n connectFirestoreEmulator(\n getFirestore(app),\n emulator.host,\n emulator.port,\n )\n } else {\n // The SDK never sweeps the multi-tab records the durable cache\n // strands in localStorage, and a full localStorage fails the whole\n // Firestore client — see `firestore-shared-client-state.ts`.\n void pruneSharedClientStateFor(firestoreCacheClass, app)\n // Only the durable multi-tab cache can be locked by another tab,\n // so only it gets the lease check after a stalled recovery.\n if (firestoreCacheClass === 'durable' && app.options?.projectId) {\n const prefix = firestorePersistencePrefix(app.name, app.options.projectId)\n markMultiTabFirestore(getFirestore(app), `${prefix}main`)\n // A visible tab opened while a background tab holds the shared\n // cache's lease takes the lease at boot instead of routing every\n // query through the background tab until its lease lapses\n // (AGL-3660). In initializeFirestore's task: its first step\n // snapshots the other tabs before this tab's client can start.\n void startFirestoreTabHandoff(getFirestore(app), prefix)\n }\n }\n } catch {\n // already initialized (e.g. HMR reset the module flag) — getFirestore() returns the existing instance\n } finally {\n firestoreInitialized.add(appName)\n }\n }\n const firestore = getFirestore(app)\n\n if (!connectedAuth) {\n try {\n if (FIREBASE_AUTH_EMULATOR_ENABLED) {\n // The emulator the server was started with (AGL-2834).\n connectAuthEmulator(auth, authEmulatorUrl())\n }\n connectedAuth = true\n } catch (error) {\n console.error(error)\n }\n }\n // App Check must be skipped under the emulators: there is no App\n // Check emulator, so ReCaptcha would hit the real backend and its\n // 403s break emulator auth (the AGL-216 emulator sessions hit this).\n if (\n !FIREBASE_AUTH_EMULATOR_ENABLED &&\n !FIREBASE_FIRESTORE_EMULATOR_ENABLED\n ) {\n // No site key means no provider (AGL-2049). Registering one built on\n // `undefined` does not throw — it fails asynchronously inside the SDK,\n // where the catch below cannot see it — so this has to be a pre-check.\n const siteKey = appCheckSiteKey()\n if (!siteKey) {\n console.warn(APP_CHECK_KEY_MISSING_MESSAGE)\n } else {\n try {\n initializeAppCheck(app, {\n provider: new ReCaptchaV3Provider(siteKey),\n isTokenAutoRefreshEnabled: true,\n })\n // The widget renders into the SDK's <body> div only once Google's\n // script loads; a body cleared before then (hydration fallback,\n // global-error) made that render throw unhandled.\n keepAppCheckRecaptchaContainer(app.name)\n } catch (error) {\n console.error(error)\n }\n }\n }\n // Remote Config (AGL-228): release-flag delivery. Browser-only like\n // analytics; consumers set defaultConfig before their first getValue so\n // gating never blocks on the network.\n let remoteConfig: RemoteConfig\n try {\n remoteConfig = getRemoteConfigInstance(app)\n remoteConfig.settings.minimumFetchIntervalMillis =\n process.env.NODE_ENV === 'production' ? 3_600_000 : 60_000\n } catch (error) {\n console.error(error)\n }\n\n return { app, firestore, auth, remoteConfig }\n}\n\n/**\n * Services booted before React rendered anything, waiting for the provider to\n * adopt them (AGL-3660).\n *\n * The provider builds its services in its first render, and on the console\n * that render comes late: after every chunk of the route has executed, after\n * hydration, and after the `NoSsr` boundary above it has committed its\n * fallback and rendered again. Auth's persistence read and `accounts:lookup`,\n * and App Check's reCAPTCHA chain (script, script, iframe, token exchange),\n * which Auth AND Firestore requests both wait on whenever the cached App Check\n * token has expired, only started then. None of it depends on React, so a\n * surface that knows its config at module scope can start it there.\n *\n * Taken once, by the first provider that renders with the same app name and\n * persistence class; any later mount boots exactly as it always did. A\n * mismatched key is never adopted, so a preboot cannot hand a provider a\n * different persistence class than the one it was given.\n */\nconst prebooted = new Map<string, FirebaseCoreServices>()\nconst prebootKey = (appName: string, authPersistence: AuthPersistenceClass) =>\n `${appName}|${authPersistence}`\n\n/**\n * Start Firebase for this document now rather than at the provider's first\n * render. Browser only, once per app name and persistence class, and it never\n * throws: a failure here leaves the provider to boot as it always did.\n *\n * The Analytics consent gate is NOT read here (the provider reads it), so the\n * gate's module-scope registration does not have to precede this call.\n */\nexport function prebootFirebaseServices(options: {\n firebaseConfig: FirebaseOptions\n appName: string\n authPersistence?: AuthPersistenceClass\n}): void {\n if (typeof window === 'undefined') return\n const { firebaseConfig, appName, authPersistence = 'durable' } = options\n const key = prebootKey(appName, authPersistence)\n if (prebooted.has(key)) return\n try {\n prebooted.set(key, bootFirebaseCore(firebaseConfig, appName, authPersistence))\n } catch (error) {\n console.error(error)\n }\n}\n\nfunction takePrebootedFirebaseCore(\n appName: string,\n authPersistence: AuthPersistenceClass,\n): FirebaseCoreServices | undefined {\n const key = prebootKey(appName, authPersistence)\n const core = prebooted.get(key)\n if (core) prebooted.delete(key)\n return core\n}\n\nexport function FirebaseServicesProvider(props: FirebaseServicesProviderProps) {\n const { firebaseConfig, appName, authPersistence = 'durable', children } = props\n const servicesRef = useRef<FirebaseServices | undefined>(undefined)\n\n if (!servicesRef.current) {\n const core =\n takePrebootedFirebaseCore(appName, authPersistence) ??\n bootFirebaseCore(firebaseConfig, appName, authPersistence)\n // `initializeAnalytics`, not `getAnalytics`, for exactly one reason: it is\n // the only form that can pass `config`, and `send_page_view: false` is the\n // only way to stop the SDK's own startup `page_view` (AGL-1643).\n //\n // Booting Analytics issues `gtag('config', <id>, configProperties)`, and\n // the vendored SDK's own comment on that line reads \"This will trigger a\n // page_view event unless 'send_page_view' is set to false in\n // configProperties\". `getAnalytics(app)` passes no config at all, so the\n // key was never there and the hit always fired — on top of the manual\n // `page_view` the console's layout sends on mount. Two hits, one page.\n //\n // The SDK's is the one suppressed rather than the layout's, and the\n // direction is the whole decision: the SDK fires ONCE per document load,\n // while the layout fires on mount AND on every `usePathname` change. In a\n // client-routed app the layout's is a superset — killing it instead would\n // have left every in-app navigation unreported, halving console pageviews\n // rather than correcting them, and the reports would have looked fine.\n //\n // Nothing else about attribution moves: the surviving hit is sent from the\n // same document at mount, so `document.referrer` — which gtag resolves for\n // `page_referrer` itself, and which is what carries marketing traffic\n // source into the session — is still the external referrer at that moment.\n //\n // Re-entry is safe *only* while the options match. `initializeAnalytics`\n // returns the existing instance when they deep-equal the first call's and\n // throws `already-initialized` otherwise — and the options can be made to\n // differ by things no call site here controls (AGL-1979):\n //\n // * anything that touches the analytics provider WITHOUT options first\n // initializes it with `{}`. `getAnalytics()` is the obvious one, but\n // `@firebase/remote-config` reaches the same code path on its own —\n // `addExperimentToAnalytics` calls `analyticsProvider.getImmediate({\n // optional: true })`, and `optional` only suppresses the throw, it\n // does not stop the initialization.\n // * in dev, a module instance carrying a PREVIOUS version of these\n // options (this config changed under AGL-1857) surviving a Fast\n // Refresh alongside the new one.\n //\n // Once that happens the conflict is permanent for the document: every\n // later call throws, so without the fallback below `analytics` stayed\n // undefined forever and the next consumer — `logEvent(analytics, …)` —\n // read `.app` off it and threw a TypeError two frames away, which is how\n // this surfaced (top Cloud Error Reporting group, and reproduced live on\n // app.aglyn.com).\n //\n // `getAnalytics(app)` is the remedy the SDK's own error message names. It\n // cannot cost us the config on a healthy document: it only runs after\n // `initializeAnalytics` threw, and on a first init that throw can only be\n // `no-app-id` / `no-api-key` / `already-exists`, none of which\n // `getAnalytics` can rescue either — it re-throws and we fall through to\n // an undefined instance, which every consumer must now tolerate.\n // `content_group: 'console'` (AGL-1857) rides the same config: it is the\n // first-class GA4 axis that separates console traffic from `marketing`\n // (stamped by the tenant runtime on aglyn.com's own tag) and `docs`\n // (stamped by the Docusaurus head snippet) in standard reports, without\n // reaching for the Hostname dimension. The console is this provider's\n // only consumer, verified by grep — the tenant runtime builds its tag in\n // `site-analytics.tsx` and never passes through here.\n //\n // AND NOT AT ALL outside a real production deployment (AGL-2067). This\n // provider used to boot Analytics unconditionally, while\n // `apps/console/.env.development.local` points at the PRODUCTION\n // measurement id — so every `next dev` session and every Vercel preview\n // build produced genuine `session_start` / `first_visit` / `page_view`\n // hits in the live property. Not a theory: the archived Marketing\n // property's entire year-to-date history is 30 views / 6 users, ~24 of\n // them `/signin` on preview urls (docs/ANALYTICS.md).\n //\n // Not initialized rather than initialized-and-suppressed, because a\n // resident tag reports on its own — AGL-1608 is the same lesson from the\n // consent side. `useAnalytics()` returning undefined is already a\n // supported state everywhere (AGL-1979), which is what makes this a\n // two-line gate instead of an audit.\n //\n // `analyticsMayEmit` leans LOUD on an unknown deployment: a self-hosted\n // build points at the operator's own Firebase project and their own GA\n // property, and silencing a customer's analytics to protect ours is the\n // worse failure. See the module for the escape hatch.\n //\n // AND NOT while a registered consent gate says no (AGL-1498 posture,\n // applied to Aglyn's own console). `analyticsForConsentState` is where\n // both conditions and the boot itself now live, because the boot has to\n // be repeatable: a visitor whose region has not resolved yet is undecided\n // at this instant and may be granted a moment later, and a visitor who\n // withdraws mid-session has to lose the instance without the page\n // reloading. See the effect below.\n //\n // SCOPE. This is the console's tag and only the console's:\n // `CONSOLE_ANALYTICS_OPTIONS`, `content_group: 'console'`, and this\n // provider's analytics branch has no other consumer (the tenant runtime\n // builds its tag in `site-analytics.tsx` and never passes through here).\n // Customer sites keep the AGL-1498 gate and the host's own\n // `consent.mode`; nothing here reaches them.\n const initialAnalytics = analyticsForConsentState(core.app)\n\n servicesRef.current = {\n ...core,\n analytics: initialAnalytics,\n authPersistence,\n }\n }\n\n const services = servicesRef.current\n\n // The consent gate's other half: the verdict can CHANGE within one document,\n // in both directions, and everything else in this provider is built exactly\n // once by design.\n //\n // Deferred grant. A visitor with no stored record is undecided at the\n // instant this provider first renders, because resolving them needs the\n // region endpoint and that is a network call. Outside the prior-consent\n // regions the answer is implied consent, and it lands a few hundred\n // milliseconds later — same document, so `document.referrer` is still the\n // external referrer and the pageview's attribution is unaffected. Without\n // this, \"the gate is synchronous\" would have quietly meant \"the rest of the\n // world is never measured on a first visit\".\n //\n // Withdrawal. Dropping the instance unmounts every binding hung off it,\n // which is what unregisters the analytics transport — after that\n // `deliver()` falls through to `window.gtag`, and the shared consent writer\n // has already set `ga-disable-<id>` and sent a denied `consent update` on\n // that exact tag (AGL-1608). The resident tag cannot be unloaded; it can be\n // made to send nothing, and both halves are needed because either alone\n // leaks — enhanced measurement fires with no call site at all.\n const [analytics, setAnalytics] = useState<Analytics | undefined>(\n () => services.analytics,\n )\n useEffect(() => {\n const sync = () => setAnalytics(analyticsForConsentState(services.app))\n // Once on mount as well as on the event: the priming pass can resolve a\n // visitor between this provider's render and this effect, and the event it\n // dispatched then had no listener yet.\n sync()\n window.addEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n return () =>\n window.removeEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n }, [services])\n\n // Identity changes only when the tag comes or goes, so a consent-stable\n // document re-renders nothing.\n const value = useMemo(\n () =>\n services.analytics === analytics ? services : { ...services, analytics },\n [services, analytics],\n )\n\n return (\n <FirebaseServicesContext.Provider value={value}>\n {children}\n </FirebaseServicesContext.Provider>\n )\n}\nFirebaseServicesProvider.displayName = 'FirebaseServicesProvider'\n\nfunction useFirebaseServices(): FirebaseServices {\n const services = useContext(FirebaseServicesContext)\n if (!services) {\n throw new Error(\n 'Firebase hooks must be used within a <FirebaseServicesProvider>',\n )\n }\n return services\n}\n\nexport function useFirebaseApp(): FirebaseApp {\n return useFirebaseServices().app\n}\nexport function useFirestore(): Firestore {\n return useFirebaseServices().firestore\n}\nexport function useAuth(): Auth {\n return useFirebaseServices().auth\n}\n/**\n * The persistence class the provider's `Auth` was created with (AGL-1379).\n *\n * Read it — never re-derive it — when building a **second** Firebase app on\n * the same origin, so the second instance cannot end up more persistent than\n * the first. `apps/console/hooks/use-presence.ts` is the existing case.\n */\nexport function useAuthPersistence(): AuthPersistenceClass {\n return useFirebaseServices().authPersistence\n}\nexport function useAnalytics(): Analytics {\n return useFirebaseServices().analytics\n}\nexport function useRemoteConfig(): RemoteConfig {\n return useFirebaseServices().remoteConfig\n}\n\n/**\n * Replaces reactfire's `useUser()`. Reactfire subscribes via\n * `onIdTokenChanged` rather than `onAuthStateChanged` so fields like\n * `emailVerified` stay live across token refresh (e.g. after\n * `user.reload()` post-email-verification), not just sign-in/out — matched\n * here for behavioral parity.\n *\n * THREE states, not two (AGL-1261): `undefined` = auth has not resolved yet,\n * `null` = resolved, signed OUT, a `User` = resolved, signed in. This used to\n * collapse the emitted `null` into `undefined`, which made \"signed out\" and\n * \"still loading\" the same value — a state that, for a signed-out visitor,\n * never left \"loading\". `useSessionCookie` gates its whole body on\n * `user === undefined`, so its silent restore from the shared `__session`\n * cookie could never run: the cookie exchange returned a valid custom token\n * and nothing ever asked for it.\n */\nexport function useUser(): { data: User | null | undefined } {\n const auth = useAuth()\n const [user, setUser] = useState<User | null | undefined>(\n auth.currentUser ?? undefined,\n )\n\n useEffect(() => {\n return onIdTokenChanged(auth, (nextUser) => {\n // `?? null`, never `?? undefined` — see the note above.\n setUser(nextUser ?? null)\n })\n }, [auth])\n\n return { data: user }\n}\n\nexport interface SigninCheckResult {\n signedIn: boolean\n user: User | null\n}\n\n/** Replaces reactfire's `useSigninCheck()` — same `onIdTokenChanged` basis as `useUser()`. */\nexport function useSigninCheck(): {\n status: 'loading' | 'success' | 'error'\n data: SigninCheckResult | undefined\n error: Error | undefined\n} {\n const auth = useAuth()\n const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')\n const [data, setData] = useState<SigninCheckResult | undefined>(undefined)\n const [error, setError] = useState<Error | undefined>(undefined)\n\n useEffect(() => {\n return onIdTokenChanged(\n auth,\n (user) => {\n setStatus('success')\n setError(undefined)\n setData({ signedIn: !!user, user: user ?? null })\n },\n (err) => {\n setStatus('error')\n setError(err)\n },\n )\n }, [auth])\n\n return { status, data, error }\n}\n"],"names":["getApps","initializeApp","getAnalytics","getAnalyticsInstance","initializeAnalytics","initializeAnalyticsInstance","initializeAppCheck","ReCaptchaV3Provider","analyticsMayEmit","pushPlatformConsentDefault","VISITOR_CONSENT_CHANGED_EVENT","connectAuthEmulator","onIdTokenChanged","connectFirestoreEmulator","getFirestore","initializeFirestore","getRemoteConfig","getRemoteConfigInstance","createContext","useContext","useEffect","useMemo","useRef","useState","FIREBASE_AUTH_EMULATOR_ENABLED","FIREBASE_FIRESTORE_EMULATOR_ENABLED","APP_CHECK_KEY_MISSING_MESSAGE","appCheckSiteKey","authEmulatorUrl","firestoreEmulatorHost","createAuthInstance","firestoreCacheClassFor","localCacheFor","pruneSharedClientStateFor","keepAppCheckRecaptchaContainer","markMultiTabFirestore","firestorePersistencePrefix","startFirestoreTabHandoff","FirebaseServicesContext","undefined","firestoreInitialized","Set","connectedAuth","isAutomatedChromeSession","navigator","webdriver","CONSOLE_ANALYTICS_OPTIONS","config","send_page_view","content_group","analyticsConsentGate","setAnalyticsConsentGate","gate","analyticsConsentAllows","analyticsByApp","WeakMap","bootAnalytics","app","existing","get","analytics","window","error","console","fallbackError","set","analyticsForConsentState","bootFirebaseCore","firebaseConfig","appName","authPersistence","find","name","auth","firestoreCacheClass","has","experimentalForceLongPolling","localCache","emulator","host","port","options","projectId","prefix","add","firestore","siteKey","warn","provider","isTokenAutoRefreshEnabled","remoteConfig","settings","minimumFetchIntervalMillis","process","env","NODE_ENV","prebooted","Map","prebootKey","prebootFirebaseServices","key","takePrebootedFirebaseCore","core","delete","FirebaseServicesProvider","props","children","servicesRef","current","initialAnalytics","services","setAnalytics","sync","addEventListener","removeEventListener","value","Provider","displayName","useFirebaseServices","Error","useFirebaseApp","useFirestore","useAuth","useAuthPersistence","useAnalytics","useRemoteConfig","useUser","user","setUser","currentUser","nextUser","data","useSigninCheck","status","setStatus","setData","setError","signedIn","err"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firebase-services.tsx"],"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 {\n type FirebaseApp,\n type FirebaseOptions,\n getApps,\n initializeApp,\n} from 'firebase/app'\nimport {\n type Analytics,\n getAnalytics as getAnalyticsInstance,\n initializeAnalytics as initializeAnalyticsInstance,\n} from 'firebase/analytics'\nimport { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check'\nimport { analyticsMayEmit } from '@aglyn/aglyn/app-utils/analytics-environment'\nimport { pushPlatformConsentDefault } from '@aglyn/aglyn/app-utils/platform-consent-default'\nimport { VISITOR_CONSENT_CHANGED_EVENT } from '@aglyn/aglyn/app-utils/visitor-consent'\nimport {\n type Auth,\n connectAuthEmulator,\n onIdTokenChanged,\n type User,\n} from 'firebase/auth'\nimport {\n type Firestore,\n connectFirestoreEmulator,\n getFirestore,\n initializeFirestore,\n} from 'firebase/firestore'\nimport {\n type RemoteConfig,\n getRemoteConfig as getRemoteConfigInstance,\n} from 'firebase/remote-config'\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react'\nimport {\n FIREBASE_AUTH_EMULATOR_ENABLED,\n FIREBASE_FIRESTORE_EMULATOR_ENABLED,\n} from '@aglyn/shared-data-enums'\nimport {\n APP_CHECK_KEY_MISSING_MESSAGE,\n appCheckSiteKey,\n} from '../../constants/firebase-config'\nimport {\n authEmulatorUrl,\n firestoreEmulatorHost,\n} from '../../constants/firebase-emulator-hosts'\nimport {\n type AuthPersistenceClass,\n createAuthInstance,\n} from './auth-persistence'\nimport {\n firestoreCacheClassFor,\n localCacheFor,\n pruneSharedClientStateFor,\n} from './firestore-cache'\nimport { keepAppCheckRecaptchaContainer } from './app-check-recaptcha-container'\nimport {\n confirmSharedCacheLock,\n markMultiTabFirestore,\n} from './firestore-multitab-wedge'\nimport { firestorePersistencePrefix } from './firestore-shared-client-state'\nimport { startFirestoreTabHandoff } from './firestore-tab-handoff'\n\n/**\n * Drop-in replacement for reactfire's `ObservableStatus<T>` — reactfire is\n * unmaintained against firebase majors past v9, but its shape is preserved\n * here so `helpers/use-doc.ts`'s already-battle-tested onSnapshot+retry\n * implementation (AGL-216/223) didn't need to change.\n */\nexport interface ObservableStatus<T> {\n status: 'loading' | 'error' | 'success'\n hasEmitted: boolean\n isComplete: boolean\n data: T\n error: Error | undefined\n firstValuePromise: Promise<void>\n /**\n * The emitted `data` includes local writes the server has NOT\n * acknowledged (`snapshot.metadata.hasPendingWrites`).\n *\n * Carried rather than dropped because a caller cannot otherwise tell \"the\n * store holds this\" from \"my browser holds this\". `persistentLocalCache`\n * replays a queued write into the very first snapshot after a reload, so a\n * caller that treats every snapshot as authoritative will adopt its own\n * unacknowledged edit as the saved state — which is how the besigner ended\n * up unable to save real changes (AGL-1262).\n */\n hasPendingWrites: boolean\n /**\n * The emitted `data` came from the local cache and the server has NOT\n * confirmed it (`snapshot.metadata.fromCache`).\n *\n * Carried because it is the only per-listener ground truth about\n * freshness the SDK offers, and the console needs it (AGL-1066). Under\n * `persistentLocalCache` a listener whose server listen is being REFUSED\n * still emits cached snapshots and still reports `status: 'success'` —\n * the retry budget is reset by each cached emission and never spends, so\n * `status` can never reveal the fault (see\n * `use-firestore-collection-cached-retry.spec.ts`).\n *\n * Prefer this over `session-health`'s `staleSession` for anything that\n * asks \"is what I am looking at live?\". `staleSession` is a heuristic\n * needing two denied collections inside a window, and it is unreachable\n * on listener-only pages; this is a fact about THIS listener, costs\n * nothing, and clears itself the instant a server snapshot lands.\n */\n fromCache: boolean\n /**\n * The server has REFUSED this listen for longer than the retry budget, and\n * no server snapshot has arrived since (AGL-1066).\n *\n * `fromCache` says \"unconfirmed\", which is also true offline and true for\n * the first snapshot of a perfectly healthy load. This says \"refused\" —\n * only `permission-denied` counts it, and it takes a whole budget's worth\n * in a row, so it cannot fire offline and cannot fire on the AGL-216/217\n * post-sign-in token race.\n *\n * It is what `status` would say if the retry budget were spendable. It is\n * carried separately because correcting `status` in place would blank\n * every surface that renders cached data through an `error` branch — see\n * the note on `attempt = 0` in `use-firestore-collection`.\n */\n serverDenied: boolean\n}\n\n/** Replaces reactfire's `ReactFireOptions<T>` — drops the `suspense` field, which was never read. */\nexport interface FirestoreDocOptions<T> {\n idField?: string\n initialData?: T\n}\n\n/**\n * The Firebase products this provider builds, and therefore the ones every\n * page that renders it downloads.\n *\n * ⛔ NO Realtime Database and NO Cloud Storage (AGL-2706). Both were built\n * here and published as `useDatabase()` / `useStorage()`, and neither hook had\n * a single caller anywhere in the repo — so `@firebase/database` (125.9 KB\n * raw) and `@firebase/storage` (13.9 KB) were in the first load of every\n * console route to satisfy a context field nothing read.\n *\n * Realtime Database IS used, by `apps/console/hooks/use-presence.ts` and\n * `use-coediting.ts` — on their own Firebase app, with their own\n * `getDatabase`, behind the editor routes that need presence. That is the\n * shape to copy if a surface needs one of these: reach for the product where\n * the feature is, not in the provider every page mounts. A field here is paid\n * for by every route, used or not.\n */\ninterface FirebaseServices {\n app: FirebaseApp\n firestore: Firestore\n auth: Auth\n analytics: Analytics\n remoteConfig: RemoteConfig\n /**\n * The persistence class `auth` was actually created with (AGL-1379).\n *\n * Published so a **second** auth instance derives its class from the\n * primary rather than re-deciding it. `apps/console/hooks/use-presence.ts`\n * builds its own Firebase app and its own `Auth`; before this it took the\n * SDK default unconditionally, so configuring only the provider would have\n * left presence persisting a refresh token on an origin the provider had\n * just been careful not to persist one on.\n */\n authPersistence: AuthPersistenceClass\n}\n\nconst FirebaseServicesContext = createContext<FirebaseServices | undefined>(undefined)\n\n// Module-scope guards so HMR / remounts don't re-run emulator connection\n// (which throws if called twice) or double-initialize Firestore's\n// persistent cache.\n//\n// Firestore's is keyed by app NAME, not a bare boolean (AGL-1456): Firestore\n// initialization is once-per-app, so a module-global flag made the FIRST app\n// to mount decide for every later one — a second app would silently skip\n// `initializeFirestore` and take SDK defaults. That mattered the moment the\n// cache stopped being the same on every host.\nconst firestoreInitialized = new Set<string>()\nlet connectedAuth = false\n\n/**\n * True when THIS Chrome instance is under WebDriver automation control\n * (Puppeteer, Playwright, Selenium, or a raw CDP client) — never true for a\n * person's own browser.\n *\n * The `initializeFirestore` call below already documents the fault this\n * exists for: \"the emulator's WebChannel streaming misbehaves in automated\n * Chrome — listeners serve the initial empty from-cache snapshot and the\n * server sync never arrives\" (AGL-217). That mitigation only reaches\n * `FIREBASE_FIRESTORE_EMULATOR_ENABLED`, so an automated session against a\n * REAL, deployed backend — a browser-automation agent driving a live\n * console, which is exactly what `navigator.webdriver` detects — stayed\n * exposed. `getDocs`/`getDoc` and `onSnapshot` all resolve over the same\n * Watch stream internally, so a one-shot read wedges exactly like a listener\n * does: no error, nothing to catch, `loading` simply never gets its\n * settling event. That is the Media Library grid spinning on \"Loading\n * media…\" indefinitely with a perfectly healthy account and a perfectly\n * healthy network.\n *\n * `undefined` during SSR reads as `false` — `navigator` does not exist in\n * Node, and a server render can only ever produce the auto-detected\n * transport, never the forced one.\n */\nconst isAutomatedChromeSession = (): boolean =>\n typeof navigator !== 'undefined' && navigator.webdriver === true\n\n/**\n * The console tag's gtag config — ONE object, at module scope, deliberately\n * (AGL-1979).\n *\n * `initializeAnalytics(app, options)` throws `already-initialized` unless the\n * options `deepEqual` the ones the instance was first created with, and the\n * provider re-enters this block on every mount, every StrictMode double\n * invoke and every Fast Refresh. A fresh object literal per call survives\n * that only by *value*; a shared constant survives it by *identity*, which is\n * the first branch of the SDK's own comparison and cannot drift when someone\n * edits one call site and not another.\n *\n * ⚠️ The SDK MUTATES this object: `_initializeAnalytics` reads `options.config`\n * and writes `origin`, `update` and the installation id onto it in place.\n * That is precisely why sharing it is safe rather than dangerous — the object\n * the provider holds and the object the SDK stored are the same one, so they\n * cannot disagree.\n */\nconst CONSOLE_ANALYTICS_OPTIONS = {\n config: { send_page_view: false, content_group: 'console' },\n}\n\n/**\n * A surface's answer to \"may an analytics tag exist for this visitor at all?\".\n *\n * Registered from the app, at module scope, in the same shape as\n * `setFirestoreSessionReporters` and `setStaleSessionCheck` — and for a\n * sharper version of the same reason. This is consulted during the provider's\n * FIRST render, before any effect anywhere has run, so a gate installed from\n * an effect would arrive after the decision it exists to make.\n *\n * Answering `false` means the SDK is never asked to initialize: `gtag.js` is\n * never fetched, `window.gtag` never comes into existence, and the shared\n * `deliver()` in `analytics-events.ts` — whose fallback IS `window.gtag` —\n * therefore has nowhere to put a hit. That is the difference between a gate\n * and a suppression, and it is the difference AGL-1608 paid for: a resident\n * tag reports on its own, through enhanced measurement, whatever any flag\n * says afterwards.\n */\nexport type AnalyticsConsentGate = () => boolean\n\nlet analyticsConsentGate: AnalyticsConsentGate | null = null\n\n/**\n * Install the consent gate for this document, or clear it with `null`.\n *\n * UNREGISTERED MEANS ALLOWED, which is deliberate and is the only default that\n * is safe here. The tenant runtime does not use this provider's analytics\n * branch at all (it builds its own tag in `site-analytics.tsx`, behind the\n * AGL-1498 gate), and a self-hosted console points at the operator's own GA\n * property — silencing an operator's analytics because Aglyn's own surface\n * grew a gate is a failure they cannot diagnose. The surface that needs the\n * gate is the one that registers it.\n */\nexport function setAnalyticsConsentGate(\n gate: AnalyticsConsentGate | null,\n): void {\n analyticsConsentGate = gate\n}\n\n/** The gate's verdict, with an unregistered gate reading as \"allowed\". */\nfunction analyticsConsentAllows(): boolean {\n if (analyticsConsentGate === null) return true\n try {\n return analyticsConsentGate() === true\n } catch {\n // A gate that throws cannot be read as consent. Storage can throw in\n // private mode, and \"we could not tell\" is the one state this whole\n // mechanism resolves to \"no\".\n return false\n }\n}\n\n/**\n * The Analytics instance per Firebase app, so a re-boot after a consent change\n * hands back the tag that is already resident rather than asking the SDK for a\n * second one.\n *\n * `initializeAnalytics` is idempotent while the options match, so this is not\n * what makes re-entry safe — `CONSOLE_ANALYTICS_OPTIONS` being a shared\n * constant is. What this avoids is the noisy path: once anything has\n * initialized the provider with different options, every later call throws and\n * is logged, and a withdrawal followed by a re-grant would log it again.\n */\nconst analyticsByApp = new WeakMap<FirebaseApp, Analytics>()\n\n/**\n * Boot the console's Analytics tag, or return the one already booted.\n *\n * Extracted from the provider body so that \"when is the tag created\" and \"may\n * it be created\" are separable: the consent gate can defer this call to a\n * later render without the ordering below moving.\n */\nfunction bootAnalytics(app: FirebaseApp): Analytics | undefined {\n const existing = analyticsByApp.get(app)\n if (existing) return existing\n let analytics: Analytics\n // The region-conditional consent default (AGL-1597), declared BEFORE the\n // SDK boots the tag. Ordering is the whole of it: a `default` read after\n // `config` is not a default, and the SDK issues `config` inside\n // `initializeAnalytics` on the next line. Pushing onto `dataLayer` here\n // works even though gtag.js is not loaded yet — the queue is what makes the\n // ordering expressible.\n //\n // Still declared even though the gate above has already said yes, and the\n // two are not redundant. The gate answers \"may a tag exist\"; the default\n // answers \"what may the tag that does exist do\", per region, from its very\n // first hit. Ad storage stays denied everywhere on this surface.\n pushPlatformConsentDefault(\n typeof window === 'undefined' ? null : (window as never),\n )\n try {\n analytics = initializeAnalyticsInstance(app, CONSOLE_ANALYTICS_OPTIONS)\n } catch (error) {\n console.error(error)\n try {\n // Already initialized by someone else, with someone else's options:\n // take THAT instance rather than leaving consumers with nothing. The\n // tag it is attached to was configured without `send_page_view: false`\n // and without `content_group`, so this session reports a duplicate\n // startup page_view and an unstamped surface — degraded, and loudly so,\n // instead of silently dead.\n analytics = getAnalyticsInstance(app)\n } catch (fallbackError) {\n console.error(fallbackError)\n }\n }\n if (analytics) analyticsByApp.set(app, analytics)\n return analytics\n}\n\n/**\n * The tag this document may have right now: none outside a real production\n * deployment, none while consent is absent, otherwise the booted instance.\n */\nfunction analyticsForConsentState(app: FirebaseApp): Analytics | undefined {\n if (!analyticsMayEmit()) return undefined\n if (!analyticsConsentAllows()) return undefined\n return bootAnalytics(app)\n}\n\nexport interface FirebaseServicesProviderProps {\n firebaseConfig: FirebaseOptions\n appName: string\n /**\n * How much of *anything* this origin may keep on disk. Defaults to\n * `durable`, which is what every host serving this provider today is:\n * `*.aglyn.com` and `*.aglyn.app`, whose DNS we own and whose 14-day\n * session is the product.\n *\n * A **custom console domain** (AGL-1099c) must pass `ephemeral`. That is\n * the whole reason this is a prop rather than a constant: the class differs\n * per origin, not per build, and the decision belongs to whoever knows\n * which origin is being served.\n *\n * **It governs two things, and the name only names one.** It selects the\n * `Auth` persistence class (AGL-1379) *and* the Firestore `localCache`\n * (AGL-1456) — because both answer the same question about the same origin,\n * and splitting them into two props is precisely how the cache came to be\n * unconditional while the credential was hardened. See `firestore-cache.ts`.\n */\n authPersistence?: AuthPersistenceClass\n children?: ReactNode\n}\n\n/** The services every surface boots, before the per-document analytics verdict. */\ninterface FirebaseCoreServices {\n app: FirebaseApp\n firestore: Firestore\n auth: Auth\n remoteConfig: RemoteConfig\n}\n\n/**\n * Create (or adopt) the app, Auth, Firestore, App Check and Remote Config for\n * one Firebase app: everything the provider used to build in its first render\n * except Analytics, whose consent verdict belongs to the render that reads it.\n */\nfunction bootFirebaseCore(\n firebaseConfig: FirebaseOptions,\n appName: string,\n authPersistence: AuthPersistenceClass,\n): FirebaseCoreServices {\n const app =\n getApps().find((existing) => existing.name === appName) ??\n initializeApp(firebaseConfig, appName)\n // `durable` resolves to the same `getAuth(app)` this always called.\n const auth = createAuthInstance(app, authPersistence)\n\n // The durable cache's IndexedDB database, `<persistence prefix>main`.\n const persistencePrefix = app.options?.projectId\n ? firestorePersistencePrefix(app.name, app.options.projectId)\n : undefined\n const databaseName = persistencePrefix && `${persistencePrefix}main`\n // The origin's class, unless this tab fell back to the memory cache\n // because another tab locked the shared one (AGL-3428), or a tab has just\n // seen that lock (AGL-4090). Firestore only: `auth` above keeps the\n // origin's class.\n const firestoreCacheClass = firestoreCacheClassFor(authPersistence, databaseName)\n if (!firestoreInitialized.has(appName)) {\n try {\n // Under the emulator (dev/e2e only): force long-polling and skip\n // the persistent multi-tab cache. The emulator's WebChannel\n // streaming misbehaves in automated Chrome — listeners serve the\n // initial empty from-cache snapshot and the server sync never\n // arrives, which looks like \"empty pages with zero errors\"\n // (the AGL-217 mystery). Real traffic keeps the default transport\n // and persistent cache — except a WebDriver-controlled Chrome\n // against a real backend, which forces long-polling for the same\n // reason without giving up the cache (see `isAutomatedChromeSession`\n // below).\n //\n // ⚠️ CONSEQUENCE, and it will cost you hours if you do not know it\n // (AGL-1066): every stale-session/stale-cache fault is UNREPRODUCIBLE\n // locally through the app, because the cache that causes them is off\n // in exactly the configuration you would reach for to reproduce one.\n // A listener that keeps serving arbitrarily-old data while the server\n // refuses it, a `noDocument` tombstone that 404s a live host, the\n // retry budget that never spends because a cached emission resets it\n // — none of them can happen here. Test that behaviour with the unit\n // seams instead (`use-firestore-collection-cached-retry.spec.ts`\n // drives cached emissions and denials directly), or against a\n // deployed environment. Do NOT \"fix\" it by turning the cache on for\n // the emulator: the AGL-217 empty-pages-with-zero-errors mystery is\n // what that produces.\n initializeFirestore(\n app,\n FIREBASE_FIRESTORE_EMULATOR_ENABLED\n ? { experimentalForceLongPolling: true }\n : {\n // NOT unconditional (AGL-1456). `persistentLocalCache` writes\n // document bodies to this origin's IndexedDB, so on a custom\n // console domain it is the same exposure D6 removed from the\n // refresh token — see `firestore-cache.ts` for why one\n // declaration governs both.\n localCache: localCacheFor(firestoreCacheClass),\n // The real-backend half of the AGL-217 mitigation above: force\n // long-polling for a WebDriver-controlled Chrome even when it\n // is NOT talking to the emulator, because the WebChannel wedge\n // is a property of automated Chrome, not of which backend it\n // is automating against. Never true for a real visitor, so\n // production traffic keeps the SDK's own auto-detected\n // transport untouched.\n ...(isAutomatedChromeSession()\n ? { experimentalForceLongPolling: true }\n : {}),\n },\n )\n if (FIREBASE_FIRESTORE_EMULATOR_ENABLED) {\n // Where the server was told the emulator is, rather than a default\n // port another session's stack may hold (AGL-2834).\n const emulator = firestoreEmulatorHost()\n connectFirestoreEmulator(\n getFirestore(app),\n emulator.host,\n emulator.port,\n )\n } else {\n // The SDK never sweeps the multi-tab records the durable cache\n // strands in localStorage, and a full localStorage fails the whole\n // Firestore client — see `firestore-shared-client-state.ts`.\n void pruneSharedClientStateFor(firestoreCacheClass, app)\n // Only the durable multi-tab cache can be locked by another tab,\n // so only it gets the lease check after a stalled recovery.\n if (firestoreCacheClass === 'durable' && persistencePrefix && databaseName) {\n markMultiTabFirestore(getFirestore(app), databaseName)\n // A visible tab opened while a background tab holds the shared\n // cache's lease takes the lease at boot instead of routing every\n // query through the background tab until its lease lapses\n // (AGL-3660). In initializeFirestore's task: its first step\n // snapshots the other tabs before this tab's client can start.\n void startFirestoreTabHandoff(getFirestore(app), persistencePrefix)\n } else if (authPersistence === 'durable' && databaseName) {\n // A durable origin's tab on the memory cache. If that is because a\n // tab saw the shared cache locked, read the lease once, read-only,\n // so the record lasts only as long as the lock (AGL-4090).\n void confirmSharedCacheLock(databaseName)\n }\n }\n } catch {\n // already initialized (e.g. HMR reset the module flag) — getFirestore() returns the existing instance\n } finally {\n firestoreInitialized.add(appName)\n }\n }\n const firestore = getFirestore(app)\n\n if (!connectedAuth) {\n try {\n if (FIREBASE_AUTH_EMULATOR_ENABLED) {\n // The emulator the server was started with (AGL-2834).\n connectAuthEmulator(auth, authEmulatorUrl())\n }\n connectedAuth = true\n } catch (error) {\n console.error(error)\n }\n }\n // App Check must be skipped under the emulators: there is no App\n // Check emulator, so ReCaptcha would hit the real backend and its\n // 403s break emulator auth (the AGL-216 emulator sessions hit this).\n if (\n !FIREBASE_AUTH_EMULATOR_ENABLED &&\n !FIREBASE_FIRESTORE_EMULATOR_ENABLED\n ) {\n // No site key means no provider (AGL-2049). Registering one built on\n // `undefined` does not throw — it fails asynchronously inside the SDK,\n // where the catch below cannot see it — so this has to be a pre-check.\n const siteKey = appCheckSiteKey()\n if (!siteKey) {\n console.warn(APP_CHECK_KEY_MISSING_MESSAGE)\n } else {\n try {\n initializeAppCheck(app, {\n provider: new ReCaptchaV3Provider(siteKey),\n isTokenAutoRefreshEnabled: true,\n })\n // The widget renders into the SDK's <body> div only once Google's\n // script loads; a body cleared before then (hydration fallback,\n // global-error) made that render throw unhandled.\n keepAppCheckRecaptchaContainer(app.name)\n } catch (error) {\n console.error(error)\n }\n }\n }\n // Remote Config (AGL-228): release-flag delivery. Browser-only like\n // analytics; consumers set defaultConfig before their first getValue so\n // gating never blocks on the network.\n let remoteConfig: RemoteConfig\n try {\n remoteConfig = getRemoteConfigInstance(app)\n remoteConfig.settings.minimumFetchIntervalMillis =\n process.env.NODE_ENV === 'production' ? 3_600_000 : 60_000\n } catch (error) {\n console.error(error)\n }\n\n return { app, firestore, auth, remoteConfig }\n}\n\n/**\n * Services booted before React rendered anything, waiting for the provider to\n * adopt them (AGL-3660).\n *\n * The provider builds its services in its first render, and on the console\n * that render comes late: after every chunk of the route has executed, after\n * hydration, and after the `NoSsr` boundary above it has committed its\n * fallback and rendered again. Auth's persistence read and `accounts:lookup`,\n * and App Check's reCAPTCHA chain (script, script, iframe, token exchange),\n * which Auth AND Firestore requests both wait on whenever the cached App Check\n * token has expired, only started then. None of it depends on React, so a\n * surface that knows its config at module scope can start it there.\n *\n * Taken once, by the first provider that renders with the same app name and\n * persistence class; any later mount boots exactly as it always did. A\n * mismatched key is never adopted, so a preboot cannot hand a provider a\n * different persistence class than the one it was given.\n */\nconst prebooted = new Map<string, FirebaseCoreServices>()\nconst prebootKey = (appName: string, authPersistence: AuthPersistenceClass) =>\n `${appName}|${authPersistence}`\n\n/**\n * Start Firebase for this document now rather than at the provider's first\n * render. Browser only, once per app name and persistence class, and it never\n * throws: a failure here leaves the provider to boot as it always did.\n *\n * The Analytics consent gate is NOT read here (the provider reads it), so the\n * gate's module-scope registration does not have to precede this call.\n */\nexport function prebootFirebaseServices(options: {\n firebaseConfig: FirebaseOptions\n appName: string\n authPersistence?: AuthPersistenceClass\n}): void {\n if (typeof window === 'undefined') return\n const { firebaseConfig, appName, authPersistence = 'durable' } = options\n const key = prebootKey(appName, authPersistence)\n if (prebooted.has(key)) return\n try {\n prebooted.set(key, bootFirebaseCore(firebaseConfig, appName, authPersistence))\n } catch (error) {\n console.error(error)\n }\n}\n\nfunction takePrebootedFirebaseCore(\n appName: string,\n authPersistence: AuthPersistenceClass,\n): FirebaseCoreServices | undefined {\n const key = prebootKey(appName, authPersistence)\n const core = prebooted.get(key)\n if (core) prebooted.delete(key)\n return core\n}\n\nexport function FirebaseServicesProvider(props: FirebaseServicesProviderProps) {\n const { firebaseConfig, appName, authPersistence = 'durable', children } = props\n const servicesRef = useRef<FirebaseServices | undefined>(undefined)\n\n if (!servicesRef.current) {\n const core =\n takePrebootedFirebaseCore(appName, authPersistence) ??\n bootFirebaseCore(firebaseConfig, appName, authPersistence)\n // `initializeAnalytics`, not `getAnalytics`, for exactly one reason: it is\n // the only form that can pass `config`, and `send_page_view: false` is the\n // only way to stop the SDK's own startup `page_view` (AGL-1643).\n //\n // Booting Analytics issues `gtag('config', <id>, configProperties)`, and\n // the vendored SDK's own comment on that line reads \"This will trigger a\n // page_view event unless 'send_page_view' is set to false in\n // configProperties\". `getAnalytics(app)` passes no config at all, so the\n // key was never there and the hit always fired — on top of the manual\n // `page_view` the console's layout sends on mount. Two hits, one page.\n //\n // The SDK's is the one suppressed rather than the layout's, and the\n // direction is the whole decision: the SDK fires ONCE per document load,\n // while the layout fires on mount AND on every `usePathname` change. In a\n // client-routed app the layout's is a superset — killing it instead would\n // have left every in-app navigation unreported, halving console pageviews\n // rather than correcting them, and the reports would have looked fine.\n //\n // Nothing else about attribution moves: the surviving hit is sent from the\n // same document at mount, so `document.referrer` — which gtag resolves for\n // `page_referrer` itself, and which is what carries marketing traffic\n // source into the session — is still the external referrer at that moment.\n //\n // Re-entry is safe *only* while the options match. `initializeAnalytics`\n // returns the existing instance when they deep-equal the first call's and\n // throws `already-initialized` otherwise — and the options can be made to\n // differ by things no call site here controls (AGL-1979):\n //\n // * anything that touches the analytics provider WITHOUT options first\n // initializes it with `{}`. `getAnalytics()` is the obvious one, but\n // `@firebase/remote-config` reaches the same code path on its own —\n // `addExperimentToAnalytics` calls `analyticsProvider.getImmediate({\n // optional: true })`, and `optional` only suppresses the throw, it\n // does not stop the initialization.\n // * in dev, a module instance carrying a PREVIOUS version of these\n // options (this config changed under AGL-1857) surviving a Fast\n // Refresh alongside the new one.\n //\n // Once that happens the conflict is permanent for the document: every\n // later call throws, so without the fallback below `analytics` stayed\n // undefined forever and the next consumer — `logEvent(analytics, …)` —\n // read `.app` off it and threw a TypeError two frames away, which is how\n // this surfaced (top Cloud Error Reporting group, and reproduced live on\n // app.aglyn.com).\n //\n // `getAnalytics(app)` is the remedy the SDK's own error message names. It\n // cannot cost us the config on a healthy document: it only runs after\n // `initializeAnalytics` threw, and on a first init that throw can only be\n // `no-app-id` / `no-api-key` / `already-exists`, none of which\n // `getAnalytics` can rescue either — it re-throws and we fall through to\n // an undefined instance, which every consumer must now tolerate.\n // `content_group: 'console'` (AGL-1857) rides the same config: it is the\n // first-class GA4 axis that separates console traffic from `marketing`\n // (stamped by the tenant runtime on aglyn.com's own tag) and `docs`\n // (stamped by the Docusaurus head snippet) in standard reports, without\n // reaching for the Hostname dimension. The console is this provider's\n // only consumer, verified by grep — the tenant runtime builds its tag in\n // `site-analytics.tsx` and never passes through here.\n //\n // AND NOT AT ALL outside a real production deployment (AGL-2067). This\n // provider used to boot Analytics unconditionally, while\n // `apps/console/.env.development.local` points at the PRODUCTION\n // measurement id — so every `next dev` session and every Vercel preview\n // build produced genuine `session_start` / `first_visit` / `page_view`\n // hits in the live property. Not a theory: the archived Marketing\n // property's entire year-to-date history is 30 views / 6 users, ~24 of\n // them `/signin` on preview urls (docs/ANALYTICS.md).\n //\n // Not initialized rather than initialized-and-suppressed, because a\n // resident tag reports on its own — AGL-1608 is the same lesson from the\n // consent side. `useAnalytics()` returning undefined is already a\n // supported state everywhere (AGL-1979), which is what makes this a\n // two-line gate instead of an audit.\n //\n // `analyticsMayEmit` leans LOUD on an unknown deployment: a self-hosted\n // build points at the operator's own Firebase project and their own GA\n // property, and silencing a customer's analytics to protect ours is the\n // worse failure. See the module for the escape hatch.\n //\n // AND NOT while a registered consent gate says no (AGL-1498 posture,\n // applied to Aglyn's own console). `analyticsForConsentState` is where\n // both conditions and the boot itself now live, because the boot has to\n // be repeatable: a visitor whose region has not resolved yet is undecided\n // at this instant and may be granted a moment later, and a visitor who\n // withdraws mid-session has to lose the instance without the page\n // reloading. See the effect below.\n //\n // SCOPE. This is the console's tag and only the console's:\n // `CONSOLE_ANALYTICS_OPTIONS`, `content_group: 'console'`, and this\n // provider's analytics branch has no other consumer (the tenant runtime\n // builds its tag in `site-analytics.tsx` and never passes through here).\n // Customer sites keep the AGL-1498 gate and the host's own\n // `consent.mode`; nothing here reaches them.\n const initialAnalytics = analyticsForConsentState(core.app)\n\n servicesRef.current = {\n ...core,\n analytics: initialAnalytics,\n authPersistence,\n }\n }\n\n const services = servicesRef.current\n\n // The consent gate's other half: the verdict can CHANGE within one document,\n // in both directions, and everything else in this provider is built exactly\n // once by design.\n //\n // Deferred grant. A visitor with no stored record is undecided at the\n // instant this provider first renders, because resolving them needs the\n // region endpoint and that is a network call. Outside the prior-consent\n // regions the answer is implied consent, and it lands a few hundred\n // milliseconds later — same document, so `document.referrer` is still the\n // external referrer and the pageview's attribution is unaffected. Without\n // this, \"the gate is synchronous\" would have quietly meant \"the rest of the\n // world is never measured on a first visit\".\n //\n // Withdrawal. Dropping the instance unmounts every binding hung off it,\n // which is what unregisters the analytics transport — after that\n // `deliver()` falls through to `window.gtag`, and the shared consent writer\n // has already set `ga-disable-<id>` and sent a denied `consent update` on\n // that exact tag (AGL-1608). The resident tag cannot be unloaded; it can be\n // made to send nothing, and both halves are needed because either alone\n // leaks — enhanced measurement fires with no call site at all.\n const [analytics, setAnalytics] = useState<Analytics | undefined>(\n () => services.analytics,\n )\n useEffect(() => {\n const sync = () => setAnalytics(analyticsForConsentState(services.app))\n // Once on mount as well as on the event: the priming pass can resolve a\n // visitor between this provider's render and this effect, and the event it\n // dispatched then had no listener yet.\n sync()\n window.addEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n return () =>\n window.removeEventListener(VISITOR_CONSENT_CHANGED_EVENT, sync)\n }, [services])\n\n // Identity changes only when the tag comes or goes, so a consent-stable\n // document re-renders nothing.\n const value = useMemo(\n () =>\n services.analytics === analytics ? services : { ...services, analytics },\n [services, analytics],\n )\n\n return (\n <FirebaseServicesContext.Provider value={value}>\n {children}\n </FirebaseServicesContext.Provider>\n )\n}\nFirebaseServicesProvider.displayName = 'FirebaseServicesProvider'\n\nfunction useFirebaseServices(): FirebaseServices {\n const services = useContext(FirebaseServicesContext)\n if (!services) {\n throw new Error(\n 'Firebase hooks must be used within a <FirebaseServicesProvider>',\n )\n }\n return services\n}\n\nexport function useFirebaseApp(): FirebaseApp {\n return useFirebaseServices().app\n}\nexport function useFirestore(): Firestore {\n return useFirebaseServices().firestore\n}\nexport function useAuth(): Auth {\n return useFirebaseServices().auth\n}\n/**\n * The persistence class the provider's `Auth` was created with (AGL-1379).\n *\n * Read it — never re-derive it — when building a **second** Firebase app on\n * the same origin, so the second instance cannot end up more persistent than\n * the first. `apps/console/hooks/use-presence.ts` is the existing case.\n */\nexport function useAuthPersistence(): AuthPersistenceClass {\n return useFirebaseServices().authPersistence\n}\nexport function useAnalytics(): Analytics {\n return useFirebaseServices().analytics\n}\nexport function useRemoteConfig(): RemoteConfig {\n return useFirebaseServices().remoteConfig\n}\n\n/**\n * Replaces reactfire's `useUser()`. Reactfire subscribes via\n * `onIdTokenChanged` rather than `onAuthStateChanged` so fields like\n * `emailVerified` stay live across token refresh (e.g. after\n * `user.reload()` post-email-verification), not just sign-in/out — matched\n * here for behavioral parity.\n *\n * THREE states, not two (AGL-1261): `undefined` = auth has not resolved yet,\n * `null` = resolved, signed OUT, a `User` = resolved, signed in. This used to\n * collapse the emitted `null` into `undefined`, which made \"signed out\" and\n * \"still loading\" the same value — a state that, for a signed-out visitor,\n * never left \"loading\". `useSessionCookie` gates its whole body on\n * `user === undefined`, so its silent restore from the shared `__session`\n * cookie could never run: the cookie exchange returned a valid custom token\n * and nothing ever asked for it.\n */\nexport function useUser(): { data: User | null | undefined } {\n const auth = useAuth()\n const [user, setUser] = useState<User | null | undefined>(\n auth.currentUser ?? undefined,\n )\n\n useEffect(() => {\n return onIdTokenChanged(auth, (nextUser) => {\n // `?? null`, never `?? undefined` — see the note above.\n setUser(nextUser ?? null)\n })\n }, [auth])\n\n return { data: user }\n}\n\nexport interface SigninCheckResult {\n signedIn: boolean\n user: User | null\n}\n\n/** Replaces reactfire's `useSigninCheck()` — same `onIdTokenChanged` basis as `useUser()`. */\nexport function useSigninCheck(): {\n status: 'loading' | 'success' | 'error'\n data: SigninCheckResult | undefined\n error: Error | undefined\n} {\n const auth = useAuth()\n const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')\n const [data, setData] = useState<SigninCheckResult | undefined>(undefined)\n const [error, setError] = useState<Error | undefined>(undefined)\n\n useEffect(() => {\n return onIdTokenChanged(\n auth,\n (user) => {\n setStatus('success')\n setError(undefined)\n setData({ signedIn: !!user, user: user ?? null })\n },\n (err) => {\n setStatus('error')\n setError(err)\n },\n )\n }, [auth])\n\n return { status, data, error }\n}\n"],"names":["getApps","initializeApp","getAnalytics","getAnalyticsInstance","initializeAnalytics","initializeAnalyticsInstance","initializeAppCheck","ReCaptchaV3Provider","analyticsMayEmit","pushPlatformConsentDefault","VISITOR_CONSENT_CHANGED_EVENT","connectAuthEmulator","onIdTokenChanged","connectFirestoreEmulator","getFirestore","initializeFirestore","getRemoteConfig","getRemoteConfigInstance","createContext","useContext","useEffect","useMemo","useRef","useState","FIREBASE_AUTH_EMULATOR_ENABLED","FIREBASE_FIRESTORE_EMULATOR_ENABLED","APP_CHECK_KEY_MISSING_MESSAGE","appCheckSiteKey","authEmulatorUrl","firestoreEmulatorHost","createAuthInstance","firestoreCacheClassFor","localCacheFor","pruneSharedClientStateFor","keepAppCheckRecaptchaContainer","confirmSharedCacheLock","markMultiTabFirestore","firestorePersistencePrefix","startFirestoreTabHandoff","FirebaseServicesContext","undefined","firestoreInitialized","Set","connectedAuth","isAutomatedChromeSession","navigator","webdriver","CONSOLE_ANALYTICS_OPTIONS","config","send_page_view","content_group","analyticsConsentGate","setAnalyticsConsentGate","gate","analyticsConsentAllows","analyticsByApp","WeakMap","bootAnalytics","app","existing","get","analytics","window","error","console","fallbackError","set","analyticsForConsentState","bootFirebaseCore","firebaseConfig","appName","authPersistence","find","name","auth","persistencePrefix","options","projectId","databaseName","firestoreCacheClass","has","experimentalForceLongPolling","localCache","emulator","host","port","add","firestore","siteKey","warn","provider","isTokenAutoRefreshEnabled","remoteConfig","settings","minimumFetchIntervalMillis","process","env","NODE_ENV","prebooted","Map","prebootKey","prebootFirebaseServices","key","takePrebootedFirebaseCore","core","delete","FirebaseServicesProvider","props","children","servicesRef","current","initialAnalytics","services","setAnalytics","sync","addEventListener","removeEventListener","value","Provider","displayName","useFirebaseServices","Error","useFirebaseApp","useFirestore","useAuth","useAuthPersistence","useAnalytics","useRemoteConfig","useUser","user","setUser","currentUser","nextUser","data","useSigninCheck","status","setStatus","setData","setError","signedIn","err"],"mappings":"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"}
@@ -111,7 +111,8 @@ export declare const DURABLE_CACHE_SIZE_BYTES: number;
111
111
  export declare function localCacheFor(originClass: AuthPersistenceClass): FirestoreLocalCache;
112
112
  /**
113
113
  * The cache class THIS TAB's Firestore runs, which is the origin's own class
114
- * unless the tab has fallen back to the memory cache (AGL-3428).
114
+ * unless the tab has fallen back to the memory cache (AGL-3428), or another
115
+ * tab has just seen the shared one locked (AGL-4090).
115
116
  *
116
117
  * A durable origin's tab that found the shared IndexedDB locked by another
117
118
  * tab records a fallback in its `sessionStorage` and reloads; this is where
@@ -119,9 +120,14 @@ export declare function localCacheFor(originClass: AuthPersistenceClass): Firest
119
120
  * ephemeral cache is exactly what the tab needs — memory only, shared with no
120
121
  * other tab — and nothing else keys off this answer: the AUTH persistence of
121
122
  * the tab keeps the origin's class, so the session survives the reload.
122
- * Every other tab keeps the durable cache. See `firestore-multitab-wedge.ts`.
123
+ *
124
+ * When what that tab found was a LOCK on `databaseName`, it also leaves a
125
+ * short-lived record in `localStorage`, and a tab opened while it is fresh
126
+ * starts on the memory cache here instead of hanging on the same lock until
127
+ * it finds out for itself. Tabs already running keep the durable cache. See
128
+ * `firestore-multitab-wedge.ts`.
123
129
  */
124
- export declare function firestoreCacheClassFor(originClass: AuthPersistenceClass, fallbackActive?: () => boolean): AuthPersistenceClass;
130
+ export declare function firestoreCacheClassFor(originClass: AuthPersistenceClass, databaseName?: string, fallbackActive?: () => boolean): AuthPersistenceClass;
125
131
  /**
126
132
  * Prunes the multi-tab records the durable cache strands in `localStorage`
127
133
  * (AGL-2845), for an app whose Firestore this page has just initialized.
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { memoryLocalCache, memoryLruGarbageCollector, persistentLocalCache, persistentMultipleTabManager } from "firebase/firestore";
18
- import { memoryCacheFallbackActive } from "./firestore-multitab-wedge.js";
18
+ import { memoryCacheFallbackActive, sharedCacheLockActive } from "./firestore-multitab-wedge.js";
19
19
  import { pruneBrowserSharedClientState } from "./firestore-shared-client-state.js";
20
20
  /**
21
21
  * The durable cache's size bound: the SDK's own default, 40 MB, written down.
@@ -134,7 +134,8 @@ import { pruneBrowserSharedClientState } from "./firestore-shared-client-state.j
134
134
  }
135
135
  /**
136
136
  * The cache class THIS TAB's Firestore runs, which is the origin's own class
137
- * unless the tab has fallen back to the memory cache (AGL-3428).
137
+ * unless the tab has fallen back to the memory cache (AGL-3428), or another
138
+ * tab has just seen the shared one locked (AGL-4090).
138
139
  *
139
140
  * A durable origin's tab that found the shared IndexedDB locked by another
140
141
  * tab records a fallback in its `sessionStorage` and reloads; this is where
@@ -142,8 +143,13 @@ import { pruneBrowserSharedClientState } from "./firestore-shared-client-state.j
142
143
  * ephemeral cache is exactly what the tab needs — memory only, shared with no
143
144
  * other tab — and nothing else keys off this answer: the AUTH persistence of
144
145
  * the tab keeps the origin's class, so the session survives the reload.
145
- * Every other tab keeps the durable cache. See `firestore-multitab-wedge.ts`.
146
- */ export function firestoreCacheClassFor(originClass, fallbackActive = ()=>memoryCacheFallbackActive()) {
146
+ *
147
+ * When what that tab found was a LOCK on `databaseName`, it also leaves a
148
+ * short-lived record in `localStorage`, and a tab opened while it is fresh
149
+ * starts on the memory cache here instead of hanging on the same lock until
150
+ * it finds out for itself. Tabs already running keep the durable cache. See
151
+ * `firestore-multitab-wedge.ts`.
152
+ */ export function firestoreCacheClassFor(originClass, databaseName, fallbackActive = ()=>memoryCacheFallbackActive() || sharedCacheLockActive(databaseName)) {
147
153
  if (originClass === 'durable' && fallbackActive()) return 'ephemeral';
148
154
  return originClass;
149
155
  }
@@ -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 { 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"}
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 memoryCacheFallbackActive,\n sharedCacheLockActive,\n} 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), or another\n * tab has just seen the shared one locked (AGL-4090).\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 *\n * When what that tab found was a LOCK on `databaseName`, it also leaves a\n * short-lived record in `localStorage`, and a tab opened while it is fresh\n * starts on the memory cache here instead of hanging on the same lock until\n * it finds out for itself. Tabs already running keep the durable cache. See\n * `firestore-multitab-wedge.ts`.\n */\nexport function firestoreCacheClassFor(\n originClass: AuthPersistenceClass,\n databaseName?: string,\n fallbackActive: () => boolean = () =>\n memoryCacheFallbackActive() || sharedCacheLockActive(databaseName),\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","sharedCacheLockActive","pruneBrowserSharedClientState","DURABLE_CACHE_SIZE_BYTES","localCacheFor","originClass","garbageCollector","tabManager","cacheSizeBytes","firestoreCacheClassFor","databaseName","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,SACEC,yBAAyB,EACzBC,qBAAqB,QAChB,gCAA4B;AACnC,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,OAAOT,iBAAiB;YAAEU,kBAAkBT;QAA4B;IAC1E;IACA,OAAOC,qBAAqB;QAC1BS,YAAYR;QACZS,gBAAgBL;IAClB;AACF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASM,uBACdJ,WAAiC,EACjCK,YAAqB,EACrBC,iBAAgC,IAC9BX,+BAA+BC,sBAAsBS,aAAa;IAEpE,IAAIL,gBAAgB,aAAaM,kBAAkB,OAAO;IAC1D,OAAON;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASO,0BACdP,WAAiC,EACjCQ,GAAgB;QAG+BA;IAD/C,IAAIR,gBAAgB,aAAa,OAAOS,QAAQC,OAAO,CAACC;IACxD,OAAOd,8BAA8BW,IAAII,IAAI,GAAEJ,eAAAA,IAAIK,OAAO,qBAAXL,aAAaM,SAAS;AACvE"}
@@ -60,8 +60,8 @@ import { type Firestore } from 'firebase/firestore';
60
60
  * The tab records a {@link MemoryCacheFallback} in `sessionStorage` and
61
61
  * reloads. `firestoreCacheClassFor` reads it at startup and gives this tab the
62
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
63
+ * cannot reach it. `sessionStorage` is per tab: tabs already running keep the
64
+ * durable cache, and the fallback leaves when this tab closes or after
65
65
  * {@link MEMORY_CACHE_FALLBACK_TTL_MS}.
66
66
  *
67
67
  * A reload, not an in-place swap, because nothing in place can work: the
@@ -70,6 +70,45 @@ import { type Firestore } from 'firebase/firestore';
70
70
  * hundreds of readers hold the instance from context and from closures.
71
71
  * Nothing that could be saved is lost — the wedge had already stopped every
72
72
  * write in the tab from leaving it.
73
+ *
74
+ * ## The tabs that open next (AGL-4090)
75
+ *
76
+ * Every tab used to find the lock on its own, so each NEW tab spent the whole
77
+ * path above — the 8 s stall, the cycle's two 5 s steps, the settle, the 4 s
78
+ * read, the reload — on an empty page: about half a minute, per tab. So a tab
79
+ * whose read found the store LOCKED also leaves a {@link SharedCacheLock} in
80
+ * `localStorage`, and `firestoreCacheClassFor` starts every tab that opens
81
+ * while it is fresh on the memory cache at once: no stall, no reload.
82
+ *
83
+ * - **Only a lock is shared.** A locked store hangs every tab that opens the
84
+ * database, a new one included. A stale lease does not: no transaction
85
+ * holds the store, and a freshly started tab wins a lapsed lease at its
86
+ * first election (AGL-3373). That fallback stays per tab.
87
+ * - **Each tab that starts from it reads the lease again**, read-only, once
88
+ * ({@link confirmSharedCacheLock}). Still locked, it re-stamps the record;
89
+ * anything else clears it, so the first tab after the lock lifts is the
90
+ * last one moved. The record lasts as long as the lock while tabs keep
91
+ * opening, and {@link SHARED_CACHE_LOCK_TTL_MS} past the last tab that saw it.
92
+ * - **Nothing here writes to IndexedDB or touches the lease.** A tab on the
93
+ * memory cache takes no part in the multi-tab protocol, so it can never be
94
+ * a second primary over the shared cache (AGL-3788).
95
+ *
96
+ * The cost is the memory cache's, and it is why the fallback was per tab: a
97
+ * write made offline in such a tab is lost if the tab closes first, and the
98
+ * tab shares no cache with the others. But while the store is locked, the
99
+ * durable cache in a new tab can do none of that either: its persistence
100
+ * never starts, so it reads nothing and no write leaves it. The cost is real
101
+ * only for a record that has outlived its lock, and the re-read clears that
102
+ * at the first tab that starts from it.
103
+ *
104
+ * Not chosen: a lease read at boot in every tab, in place of the record. The
105
+ * cache is fixed when `initializeFirestore` runs, synchronously in the
106
+ * provider's first render, and IndexedDB answers asynchronously, so a boot
107
+ * read can shorten the wait to the read plus a reload but never skip it. It
108
+ * would also add a read to every healthy tab's boot, and at boot the store can
109
+ * be held for seconds for healthy reasons (the primary's LRU collection, a
110
+ * schema upgrade) that the stall and the cycle rule out today, so it would
111
+ * reload healthy tabs.
73
112
  */
74
113
  /** `sessionStorage` key of this tab's memory-cache fallback. */
75
114
  export declare const MEMORY_CACHE_FALLBACK_KEY = "aglyn:firestore-memory-cache-fallback";
@@ -80,6 +119,15 @@ export declare const MEMORY_CACHE_FALLBACK_KEY = "aglyn:firestore-memory-cache-f
80
119
  * cache again, and falls back again if it has to.
81
120
  */
82
121
  export declare const MEMORY_CACHE_FALLBACK_TTL_MS: number;
122
+ /** `localStorage` key of the lock a tab saw, for the tabs that open next (AGL-4090). */
123
+ export declare const SHARED_CACHE_LOCK_KEY = "aglyn:firestore-shared-cache-locked";
124
+ /**
125
+ * How long a lock one tab saw starts new tabs on the memory cache, unless a
126
+ * tab that starts from it sees the lock again. Long enough for the tabs a
127
+ * person opens together (a page, then its preview); short, because a record
128
+ * that outlives its lock moves every new tab, not one.
129
+ */
130
+ export declare const SHARED_CACHE_LOCK_TTL_MS: number;
83
131
  /**
84
132
  * A lease older than this, read from a visible online tab right after a
85
133
  * forced election, means no election is completing. The SDK treats a lease as
@@ -130,6 +178,24 @@ export type WedgeEvidence = {
130
178
  export type MemoryCacheFallback = WedgeEvidence & {
131
179
  at: number;
132
180
  };
181
+ /** A lock one tab saw, recorded for the tabs that open next (AGL-4090). */
182
+ export interface SharedCacheLock {
183
+ reason: 'lease-locked';
184
+ /** The database whose store was locked: `<persistence prefix>main`. */
185
+ databaseName: string;
186
+ /** When a tab last saw it locked. */
187
+ at: number;
188
+ }
189
+ /** What a tab that started from a {@link SharedCacheLock} found when it read the lease again. */
190
+ export type SharedCacheLockCheck =
191
+ /** There was no fresh record for this database: nothing was read. */
192
+ 'absent'
193
+ /** The store is still locked; the record was re-stamped. */
194
+ | 'still-locked'
195
+ /** The store answered; the record is gone, so the next tab gets the durable cache. */
196
+ | 'cleared'
197
+ /** Storage could not be read or written; the record is left to expire. */
198
+ | 'failed';
133
199
  /**
134
200
  * Reads the SDK's `owner` row from `databaseName`.
135
201
  *
@@ -155,8 +221,37 @@ export declare function wedgeEvidenceFrom(probe: PrimaryLeaseProbe, staleMs?: nu
155
221
  * it does not parse.
156
222
  */
157
223
  export declare function readMemoryCacheFallback(storage: Pick<Storage, 'getItem'> | undefined, now: number, ttlMs?: number): MemoryCacheFallback | undefined;
224
+ /**
225
+ * The lock a tab saw on `databaseName`, or `undefined` when there is none, it
226
+ * names another database, it has expired, or it does not parse.
227
+ */
228
+ export declare function readSharedCacheLock(storage: Pick<Storage, 'getItem'> | undefined, databaseName: string | undefined, now: number, ttlMs?: number): SharedCacheLock | undefined;
158
229
  /** Whether this browser tab is running on its memory-cache fallback. */
159
230
  export declare function memoryCacheFallbackActive(now?: number): boolean;
231
+ /**
232
+ * Whether another tab has just seen the store of `databaseName` locked, so
233
+ * this tab should start on the memory cache rather than wait to find out.
234
+ */
235
+ export declare function sharedCacheLockActive(databaseName: string | undefined, now?: number): boolean;
236
+ /**
237
+ * Records, for the tabs that open next, that the store of `databaseName` is
238
+ * locked now. Never throws: without the record, the next tab finds the lock
239
+ * itself, as it always has.
240
+ */
241
+ export declare function recordSharedCacheLock(databaseName: string, { storage, now, }?: {
242
+ storage?: Pick<Storage, 'setItem'>;
243
+ now?: () => number;
244
+ }): void;
245
+ /**
246
+ * Run once by a tab that started on the memory cache: reads the lease of
247
+ * `databaseName` again and keeps another tab's lock record only while the
248
+ * store is still locked. Never creates a record and never rejects.
249
+ */
250
+ export declare function confirmSharedCacheLock(databaseName: string, { probe, storage, now, }?: {
251
+ probe?: () => Promise<PrimaryLeaseProbe>;
252
+ storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
253
+ now?: () => number;
254
+ }): Promise<SharedCacheLockCheck>;
160
255
  export interface WedgeEscalationDeps {
161
256
  /** Reads the lease. */
162
257
  probe: () => Promise<PrimaryLeaseProbe>;
@@ -180,9 +275,16 @@ export declare function createWedgeEscalation(deps: WedgeEscalationDeps): () =>
180
275
  * Records the fallback for this tab and reloads it onto the memory cache.
181
276
  * When the record cannot be written it does not reload: without it, the
182
277
  * reloaded tab would open the same locked database and hang again.
278
+ *
279
+ * A lock is also recorded for the tabs that open next (AGL-4090), first and
280
+ * whatever happens to this tab's own record: they would hang on it too.
183
281
  */
184
- export declare function fallBackToMemoryCache(evidence: WedgeEvidence, { storage, reload, now, }?: {
282
+ export declare function fallBackToMemoryCache(evidence: WedgeEvidence, { storage, sharedStorage, databaseName, reload, now, }?: {
185
283
  storage?: Pick<Storage, 'setItem'>;
284
+ /** Where the tabs that open next look for a lock this tab saw. */
285
+ sharedStorage?: Pick<Storage, 'setItem'>;
286
+ /** The locked database. Without it, nothing is shared. */
287
+ databaseName?: string;
186
288
  reload?: () => void;
187
289
  now?: () => number;
188
290
  }): void;
@@ -59,8 +59,8 @@
59
59
  * The tab records a {@link MemoryCacheFallback} in `sessionStorage` and
60
60
  * reloads. `firestoreCacheClassFor` reads it at startup and gives this tab the
61
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
62
+ * cannot reach it. `sessionStorage` is per tab: tabs already running keep the
63
+ * durable cache, and the fallback leaves when this tab closes or after
64
64
  * {@link MEMORY_CACHE_FALLBACK_TTL_MS}.
65
65
  *
66
66
  * A reload, not an in-place swap, because nothing in place can work: the
@@ -69,6 +69,45 @@
69
69
  * hundreds of readers hold the instance from context and from closures.
70
70
  * Nothing that could be saved is lost — the wedge had already stopped every
71
71
  * write in the tab from leaving it.
72
+ *
73
+ * ## The tabs that open next (AGL-4090)
74
+ *
75
+ * Every tab used to find the lock on its own, so each NEW tab spent the whole
76
+ * path above — the 8 s stall, the cycle's two 5 s steps, the settle, the 4 s
77
+ * read, the reload — on an empty page: about half a minute, per tab. So a tab
78
+ * whose read found the store LOCKED also leaves a {@link SharedCacheLock} in
79
+ * `localStorage`, and `firestoreCacheClassFor` starts every tab that opens
80
+ * while it is fresh on the memory cache at once: no stall, no reload.
81
+ *
82
+ * - **Only a lock is shared.** A locked store hangs every tab that opens the
83
+ * database, a new one included. A stale lease does not: no transaction
84
+ * holds the store, and a freshly started tab wins a lapsed lease at its
85
+ * first election (AGL-3373). That fallback stays per tab.
86
+ * - **Each tab that starts from it reads the lease again**, read-only, once
87
+ * ({@link confirmSharedCacheLock}). Still locked, it re-stamps the record;
88
+ * anything else clears it, so the first tab after the lock lifts is the
89
+ * last one moved. The record lasts as long as the lock while tabs keep
90
+ * opening, and {@link SHARED_CACHE_LOCK_TTL_MS} past the last tab that saw it.
91
+ * - **Nothing here writes to IndexedDB or touches the lease.** A tab on the
92
+ * memory cache takes no part in the multi-tab protocol, so it can never be
93
+ * a second primary over the shared cache (AGL-3788).
94
+ *
95
+ * The cost is the memory cache's, and it is why the fallback was per tab: a
96
+ * write made offline in such a tab is lost if the tab closes first, and the
97
+ * tab shares no cache with the others. But while the store is locked, the
98
+ * durable cache in a new tab can do none of that either: its persistence
99
+ * never starts, so it reads nothing and no write leaves it. The cost is real
100
+ * only for a record that has outlived its lock, and the re-read clears that
101
+ * at the first tab that starts from it.
102
+ *
103
+ * Not chosen: a lease read at boot in every tab, in place of the record. The
104
+ * cache is fixed when `initializeFirestore` runs, synchronously in the
105
+ * provider's first render, and IndexedDB answers asynchronously, so a boot
106
+ * read can shorten the wait to the read plus a reload but never skip it. It
107
+ * would also add a read to every healthy tab's boot, and at boot the store can
108
+ * be held for seconds for healthy reasons (the primary's LRU collection, a
109
+ * schema upgrade) that the stall and the cycle rule out today, so it would
110
+ * reload healthy tabs.
72
111
  */ /** `sessionStorage` key of this tab's memory-cache fallback. */ export const MEMORY_CACHE_FALLBACK_KEY = 'aglyn:firestore-memory-cache-fallback';
73
112
  /**
74
113
  * How long a tab keeps the memory cache after falling back. The SDK's own
@@ -76,6 +115,13 @@
76
115
  * stuck no longer counts as a client; past it, a reload tries the durable
77
116
  * cache again, and falls back again if it has to.
78
117
  */ export const MEMORY_CACHE_FALLBACK_TTL_MS = 30 * 60 * 1000;
118
+ /** `localStorage` key of the lock a tab saw, for the tabs that open next (AGL-4090). */ export const SHARED_CACHE_LOCK_KEY = 'aglyn:firestore-shared-cache-locked';
119
+ /**
120
+ * How long a lock one tab saw starts new tabs on the memory cache, unless a
121
+ * tab that starts from it sees the lock again. Long enough for the tabs a
122
+ * person opens together (a page, then its preview); short, because a record
123
+ * that outlives its lock moves every new tab, not one.
124
+ */ export const SHARED_CACHE_LOCK_TTL_MS = 5 * 60 * 1000;
79
125
  /**
80
126
  * A lease older than this, read from a visible online tab right after a
81
127
  * forced election, means no election is completing. The SDK treats a lease as
@@ -217,13 +263,13 @@ const OWNER_KEY = 'owner';
217
263
  return undefined;
218
264
  }
219
265
  /**
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) {
266
+ * The object stored under `key`, when it parses and its `at` is within
267
+ * `ttlMs` of `now` and not ahead of it.
268
+ */ function readStampedRecord(storage, key, now, ttlMs) {
223
269
  let raw;
224
270
  try {
225
271
  var _ref;
226
- raw = (_ref = storage == null ? void 0 : storage.getItem(MEMORY_CACHE_FALLBACK_KEY)) != null ? _ref : null;
272
+ raw = (_ref = storage == null ? void 0 : storage.getItem(key)) != null ? _ref : null;
227
273
  } catch (unused) {
228
274
  return undefined;
229
275
  }
@@ -240,6 +286,23 @@ const OWNER_KEY = 'owner';
240
286
  if (now - record.at >= ttlMs || record.at > now) return undefined;
241
287
  return record;
242
288
  }
289
+ /**
290
+ * This tab's fallback, or `undefined` when there is none, it has expired, or
291
+ * it does not parse.
292
+ */ export function readMemoryCacheFallback(storage, now, ttlMs = MEMORY_CACHE_FALLBACK_TTL_MS) {
293
+ return readStampedRecord(storage, MEMORY_CACHE_FALLBACK_KEY, now, ttlMs);
294
+ }
295
+ /**
296
+ * The lock a tab saw on `databaseName`, or `undefined` when there is none, it
297
+ * names another database, it has expired, or it does not parse.
298
+ */ export function readSharedCacheLock(storage, databaseName, now, ttlMs = SHARED_CACHE_LOCK_TTL_MS) {
299
+ if (databaseName === undefined) return undefined;
300
+ const record = readStampedRecord(storage, SHARED_CACHE_LOCK_KEY, now, ttlMs);
301
+ if ((record == null ? void 0 : record.reason) !== 'lease-locked' || record.databaseName !== databaseName) {
302
+ return undefined;
303
+ }
304
+ return record;
305
+ }
243
306
  const browserSessionStorage = ()=>{
244
307
  try {
245
308
  return typeof window === 'undefined' ? undefined : window.sessionStorage;
@@ -248,9 +311,72 @@ const browserSessionStorage = ()=>{
248
311
  return undefined;
249
312
  }
250
313
  };
314
+ const browserLocalStorage = ()=>{
315
+ try {
316
+ return typeof window === 'undefined' ? undefined : window.localStorage;
317
+ } catch (unused) {
318
+ // Site data blocked: reading `window.localStorage` throws.
319
+ return undefined;
320
+ }
321
+ };
322
+ const browserIndexedDb = ()=>{
323
+ try {
324
+ return typeof window === 'undefined' ? undefined : window.indexedDB;
325
+ } catch (unused) {
326
+ return undefined;
327
+ }
328
+ };
251
329
  /** Whether this browser tab is running on its memory-cache fallback. */ export function memoryCacheFallbackActive(now = Date.now()) {
252
330
  return readMemoryCacheFallback(browserSessionStorage(), now) !== undefined;
253
331
  }
332
+ /**
333
+ * Whether another tab has just seen the store of `databaseName` locked, so
334
+ * this tab should start on the memory cache rather than wait to find out.
335
+ */ export function sharedCacheLockActive(databaseName, now = Date.now()) {
336
+ return readSharedCacheLock(browserLocalStorage(), databaseName, now) !== undefined;
337
+ }
338
+ /**
339
+ * Records, for the tabs that open next, that the store of `databaseName` is
340
+ * locked now. Never throws: without the record, the next tab finds the lock
341
+ * itself, as it always has.
342
+ */ export function recordSharedCacheLock(databaseName, { storage = browserLocalStorage(), now = Date.now } = {}) {
343
+ const record = {
344
+ reason: 'lease-locked',
345
+ databaseName,
346
+ at: now()
347
+ };
348
+ try {
349
+ storage == null ? void 0 : storage.setItem(SHARED_CACHE_LOCK_KEY, JSON.stringify(record));
350
+ } catch (unused) {
351
+ // Full or blocked.
352
+ }
353
+ }
354
+ /**
355
+ * Run once by a tab that started on the memory cache: reads the lease of
356
+ * `databaseName` again and keeps another tab's lock record only while the
357
+ * store is still locked. Never creates a record and never rejects.
358
+ */ export async function confirmSharedCacheLock(databaseName, { probe = ()=>probePrimaryLease(browserIndexedDb(), databaseName), storage = browserLocalStorage(), now = Date.now } = {}) {
359
+ if (!storage) return 'absent';
360
+ const seen = readSharedCacheLock(storage, databaseName, now());
361
+ if (!seen) return 'absent';
362
+ try {
363
+ if ((await probe()).state === 'locked') {
364
+ recordSharedCacheLock(databaseName, {
365
+ storage,
366
+ now
367
+ });
368
+ return 'still-locked';
369
+ }
370
+ const current = readSharedCacheLock(storage, databaseName, now());
371
+ // Another tab saw the lock again while this read ran; its record is
372
+ // newer than this read, so it stands.
373
+ if (current && current.at !== seen.at) return 'still-locked';
374
+ if (current) storage.removeItem(SHARED_CACHE_LOCK_KEY);
375
+ return 'cleared';
376
+ } catch (unused) {
377
+ return 'failed';
378
+ }
379
+ }
254
380
  const browserOnline = ()=>typeof navigator === 'undefined' || navigator.onLine !== false;
255
381
  const documentVisible = ()=>typeof document === 'undefined' || document.visibilityState !== 'hidden';
256
382
  /**
@@ -278,7 +404,16 @@ const documentVisible = ()=>typeof document === 'undefined' || document.visibili
278
404
  * Records the fallback for this tab and reloads it onto the memory cache.
279
405
  * When the record cannot be written it does not reload: without it, the
280
406
  * 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 } = {}) {
407
+ *
408
+ * A lock is also recorded for the tabs that open next (AGL-4090), first and
409
+ * whatever happens to this tab's own record: they would hang on it too.
410
+ */ export function fallBackToMemoryCache(evidence, { storage = browserSessionStorage(), sharedStorage = browserLocalStorage(), databaseName, reload = ()=>window.location.reload(), now = Date.now } = {}) {
411
+ if (evidence.reason === 'lease-locked' && databaseName !== undefined) {
412
+ recordSharedCacheLock(databaseName, {
413
+ storage: sharedStorage,
414
+ now
415
+ });
416
+ }
282
417
  if (!storage) return;
283
418
  const record = _extends({}, evidence, {
284
419
  at: now()
@@ -309,16 +444,10 @@ const documentVisible = ()=>typeof document === 'undefined' || document.visibili
309
444
  const databaseName = multiTabDatabases.get(firestore);
310
445
  if (databaseName === undefined) return undefined;
311
446
  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)
447
+ probe: ()=>probePrimaryLease(browserIndexedDb(), databaseName),
448
+ fallBack: (evidence)=>fallBackToMemoryCache(evidence, {
449
+ databaseName
450
+ })
322
451
  });
323
452
  }
324
453
 
@@ -1 +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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"}
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: tabs already running keep the\n * durable 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 * ## The tabs that open next (AGL-4090)\n *\n * Every tab used to find the lock on its own, so each NEW tab spent the whole\n * path above — the 8 s stall, the cycle's two 5 s steps, the settle, the 4 s\n * read, the reload — on an empty page: about half a minute, per tab. So a tab\n * whose read found the store LOCKED also leaves a {@link SharedCacheLock} in\n * `localStorage`, and `firestoreCacheClassFor` starts every tab that opens\n * while it is fresh on the memory cache at once: no stall, no reload.\n *\n * - **Only a lock is shared.** A locked store hangs every tab that opens the\n * database, a new one included. A stale lease does not: no transaction\n * holds the store, and a freshly started tab wins a lapsed lease at its\n * first election (AGL-3373). That fallback stays per tab.\n * - **Each tab that starts from it reads the lease again**, read-only, once\n * ({@link confirmSharedCacheLock}). Still locked, it re-stamps the record;\n * anything else clears it, so the first tab after the lock lifts is the\n * last one moved. The record lasts as long as the lock while tabs keep\n * opening, and {@link SHARED_CACHE_LOCK_TTL_MS} past the last tab that saw it.\n * - **Nothing here writes to IndexedDB or touches the lease.** A tab on the\n * memory cache takes no part in the multi-tab protocol, so it can never be\n * a second primary over the shared cache (AGL-3788).\n *\n * The cost is the memory cache's, and it is why the fallback was per tab: a\n * write made offline in such a tab is lost if the tab closes first, and the\n * tab shares no cache with the others. But while the store is locked, the\n * durable cache in a new tab can do none of that either: its persistence\n * never starts, so it reads nothing and no write leaves it. The cost is real\n * only for a record that has outlived its lock, and the re-read clears that\n * at the first tab that starts from it.\n *\n * Not chosen: a lease read at boot in every tab, in place of the record. The\n * cache is fixed when `initializeFirestore` runs, synchronously in the\n * provider's first render, and IndexedDB answers asynchronously, so a boot\n * read can shorten the wait to the read plus a reload but never skip it. It\n * would also add a read to every healthy tab's boot, and at boot the store can\n * be held for seconds for healthy reasons (the primary's LRU collection, a\n * schema upgrade) that the stall and the cycle rule out today, so it would\n * reload healthy tabs.\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/** `localStorage` key of the lock a tab saw, for the tabs that open next (AGL-4090). */\nexport const SHARED_CACHE_LOCK_KEY = 'aglyn:firestore-shared-cache-locked'\n\n/**\n * How long a lock one tab saw starts new tabs on the memory cache, unless a\n * tab that starts from it sees the lock again. Long enough for the tabs a\n * person opens together (a page, then its preview); short, because a record\n * that outlives its lock moves every new tab, not one.\n */\nexport const SHARED_CACHE_LOCK_TTL_MS = 5 * 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/** A lock one tab saw, recorded for the tabs that open next (AGL-4090). */\nexport interface SharedCacheLock {\n reason: 'lease-locked'\n /** The database whose store was locked: `<persistence prefix>main`. */\n databaseName: string\n /** When a tab last saw it locked. */\n at: number\n}\n\n/** What a tab that started from a {@link SharedCacheLock} found when it read the lease again. */\nexport type SharedCacheLockCheck =\n /** There was no fresh record for this database: nothing was read. */\n | 'absent'\n /** The store is still locked; the record was re-stamped. */\n | 'still-locked'\n /** The store answered; the record is gone, so the next tab gets the durable cache. */\n | 'cleared'\n /** Storage could not be read or written; the record is left to expire. */\n | 'failed'\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 * The object stored under `key`, when it parses and its `at` is within\n * `ttlMs` of `now` and not ahead of it.\n */\nfunction readStampedRecord(\n storage: Pick<Storage, 'getItem'> | undefined,\n key: string,\n now: number,\n ttlMs: number,\n): ({ at: number } & Record<string, unknown>) | undefined {\n let raw: string | null\n try {\n raw = storage?.getItem(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 { at?: unknown } & Record<string, unknown>\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 { at: number } & Record<string, unknown>\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 return readStampedRecord(storage, MEMORY_CACHE_FALLBACK_KEY, now, ttlMs) as\n | MemoryCacheFallback\n | undefined\n}\n\n/**\n * The lock a tab saw on `databaseName`, or `undefined` when there is none, it\n * names another database, it has expired, or it does not parse.\n */\nexport function readSharedCacheLock(\n storage: Pick<Storage, 'getItem'> | undefined,\n databaseName: string | undefined,\n now: number,\n ttlMs = SHARED_CACHE_LOCK_TTL_MS,\n): SharedCacheLock | undefined {\n if (databaseName === undefined) return undefined\n const record = readStampedRecord(storage, SHARED_CACHE_LOCK_KEY, now, ttlMs)\n if (record?.reason !== 'lease-locked' || record.databaseName !== databaseName) {\n return undefined\n }\n return record as unknown as SharedCacheLock\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\nconst browserLocalStorage = (): Storage | undefined => {\n try {\n return typeof window === 'undefined' ? undefined : window.localStorage\n } catch {\n // Site data blocked: reading `window.localStorage` throws.\n return undefined\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\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\n/**\n * Whether another tab has just seen the store of `databaseName` locked, so\n * this tab should start on the memory cache rather than wait to find out.\n */\nexport function sharedCacheLockActive(\n databaseName: string | undefined,\n now = Date.now(),\n): boolean {\n return readSharedCacheLock(browserLocalStorage(), databaseName, now) !== undefined\n}\n\n/**\n * Records, for the tabs that open next, that the store of `databaseName` is\n * locked now. Never throws: without the record, the next tab finds the lock\n * itself, as it always has.\n */\nexport function recordSharedCacheLock(\n databaseName: string,\n {\n storage = browserLocalStorage(),\n now = Date.now,\n }: { storage?: Pick<Storage, 'setItem'>; now?: () => number } = {},\n): void {\n const record: SharedCacheLock = { reason: 'lease-locked', databaseName, at: now() }\n try {\n storage?.setItem(SHARED_CACHE_LOCK_KEY, JSON.stringify(record))\n } catch {\n // Full or blocked.\n }\n}\n\n/**\n * Run once by a tab that started on the memory cache: reads the lease of\n * `databaseName` again and keeps another tab's lock record only while the\n * store is still locked. Never creates a record and never rejects.\n */\nexport async function confirmSharedCacheLock(\n databaseName: string,\n {\n probe = () => probePrimaryLease(browserIndexedDb(), databaseName),\n storage = browserLocalStorage(),\n now = Date.now,\n }: {\n probe?: () => Promise<PrimaryLeaseProbe>\n storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>\n now?: () => number\n } = {},\n): Promise<SharedCacheLockCheck> {\n if (!storage) return 'absent'\n const seen = readSharedCacheLock(storage, databaseName, now())\n if (!seen) return 'absent'\n try {\n if ((await probe()).state === 'locked') {\n recordSharedCacheLock(databaseName, { storage, now })\n return 'still-locked'\n }\n const current = readSharedCacheLock(storage, databaseName, now())\n // Another tab saw the lock again while this read ran; its record is\n // newer than this read, so it stands.\n if (current && current.at !== seen.at) return 'still-locked'\n if (current) storage.removeItem(SHARED_CACHE_LOCK_KEY)\n return 'cleared'\n } catch {\n return 'failed'\n }\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 *\n * A lock is also recorded for the tabs that open next (AGL-4090), first and\n * whatever happens to this tab's own record: they would hang on it too.\n */\nexport function fallBackToMemoryCache(\n evidence: WedgeEvidence,\n {\n storage = browserSessionStorage(),\n sharedStorage = browserLocalStorage(),\n databaseName,\n reload = () => window.location.reload(),\n now = Date.now,\n }: {\n storage?: Pick<Storage, 'setItem'>\n /** Where the tabs that open next look for a lock this tab saw. */\n sharedStorage?: Pick<Storage, 'setItem'>\n /** The locked database. Without it, nothing is shared. */\n databaseName?: string\n reload?: () => void\n now?: () => number\n } = {},\n): void {\n if (evidence.reason === 'lease-locked' && databaseName !== undefined) {\n recordSharedCacheLock(databaseName, { storage: sharedStorage, now })\n }\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: () => probePrimaryLease(browserIndexedDb(), databaseName),\n fallBack: (evidence) => fallBackToMemoryCache(evidence, { databaseName }),\n })\n}\n"],"names":["MEMORY_CACHE_FALLBACK_KEY","MEMORY_CACHE_FALLBACK_TTL_MS","SHARED_CACHE_LOCK_KEY","SHARED_CACHE_LOCK_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","readStampedRecord","storage","key","ttlMs","raw","getItem","parsed","JSON","parse","record","at","Number","isFinite","readMemoryCacheFallback","readSharedCacheLock","browserSessionStorage","window","sessionStorage","browserLocalStorage","localStorage","browserIndexedDb","indexedDB","memoryCacheFallbackActive","sharedCacheLockActive","recordSharedCacheLock","setItem","stringify","confirmSharedCacheLock","seen","current","removeItem","browserOnline","navigator","onLine","documentVisible","document","visibilityState","createWedgeEscalation","deps","fallBack","isFallbackActive","isOnline","isVisible","wait","ms","settleMs","evidence","fallBackToMemoryCache","sharedStorage","reload","location","console","warn","multiTabDatabases","WeakMap","markMultiTabFirestore","firestore","set","browserWedgeEscalationFor"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8FC,GAED,8DAA8D,GAC9D,OAAO,MAAMA,4BAA4B,wCAAuC;AAEhF;;;;;CAKC,GACD,OAAO,MAAMC,+BAA+B,KAAK,KAAK,KAAI;AAE1D,sFAAsF,GACtF,OAAO,MAAMC,wBAAwB,sCAAqC;AAE1E;;;;;CAKC,GACD,OAAO,MAAMC,2BAA2B,IAAI,KAAK,KAAI;AAErD;;;;;;CAMC,GACD,OAAO,MAAMC,yBAAyB,MAAM;AAE5C;;;CAGC,GACD,OAAO,MAAMC,yBAAyB,KAAK;AAE3C;;;CAGC,GACD,OAAO,MAAMC,kBAAkB,KAAK;AAEpC,mGAAmG,GACnG,MAAMC,cAAc;AACpB,MAAMC,YAAY;AAyClB;;;;;;;;;CASC,GACD,OAAO,eAAeC,kBACpBC,OAA+B,EAC/BC,YAAoB,EACpB,EACEC,YAAYP,sBAAsB,EAClCQ,MAAMC,KAAKD,GAAG,EAC6B,GAAG,CAAC,CAAC;IAElD,IAAI,CAACH,WAAW,OAAOA,QAAQK,SAAS,KAAK,YAAY,OAAO;QAAEC,OAAO;IAAU;IACnF,IAAI;QACF,MAAMD,YAAY,MAAML,QAAQK,SAAS;QACzC,IAAI,CAACA,UAAUE,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,KAAKR,eAAe;YACjE,OAAO;gBAAEK,OAAO;YAAU;QAC5B;IACF,EAAE,eAAM;QACN,OAAO;YAAEA,OAAO;QAAU;IAC5B;IAEA,OAAO,IAAII,QAAQ,CAACC;QAClB,IAAIC,UAAU;QACd,IAAIJ;QACJ,MAAMK,SAAS,CAACC;YACd,IAAIF,SAAS;YACbA,UAAU;YACVG,aAAaC;YACbL,QAAQG;QACV;QACA,MAAMG,QAAQ;YACZ,IAAI;gBACFT,4BAAAA,SAAUS,KAAK;YACjB,EAAE,eAAM;YACN,kBAAkB;YACpB;QACF;QACA,wEAAwE;QACxE,yCAAyC;QACzC,MAAMD,QAAQE,WAAW,IAAML,OAAO;gBAAEP,OAAO;YAAS,IAAIJ;QAE5D,IAAIiB;QACJ,IAAI;YACFA,UAAUnB,QAAQoB,IAAI,CAACnB;QACzB,EAAE,eAAM;YACNY,OAAO;gBAAEP,OAAO;YAAU;YAC1B;QACF;QACA,yEAAyE;QACzE,kEAAkE;QAClEa,QAAQE,eAAe,GAAG;gBAAMF;oBAAAA,uBAAAA,QAAQG,WAAW,qBAAnBH,qBAAqBI,KAAK;;QAC1DJ,QAAQK,OAAO,GAAG,IAAMX,OAAO;gBAAEP,OAAO;YAAU;QAClDa,QAAQM,SAAS,GAAG;YAClBjB,WAAWW,QAAQO,MAAM;YACzBlB,SAASmB,eAAe,GAAGV;YAC3B,IAAIL,WAAW,CAACJ,SAASoB,gBAAgB,CAACC,QAAQ,CAAChC,cAAc;gBAC/DoB;gBACAJ,OAAO;oBAAEP,OAAO;gBAAU;gBAC1B;YACF;YACA,IAAI;gBACF,MAAMwB,OAAOtB,SACVc,WAAW,CAACzB,aAAa,YACzBkC,WAAW,CAAClC,aACZmC,GAAG,CAAClC;gBACPgC,KAAKL,SAAS,GAAG;oBACfR;oBACA,MAAMgB,MAAMH,KAAKJ,MAAM;oBAGvB,IACE,CAACO,OACD,OAAOA,IAAIC,OAAO,KAAK,YACvB,OAAOD,IAAIE,gBAAgB,KAAK,UAChC;wBACAtB,OAAO;4BAAEP,OAAO;wBAAS;wBACzB;oBACF;oBACAO,OAAO;wBACLP,OAAO;wBACP4B,SAASD,IAAIC,OAAO;wBACpBE,OAAOjC,QAAQ8B,IAAIE,gBAAgB;oBACrC;gBACF;gBACAL,KAAKN,OAAO,GAAG;oBACbP;oBACAJ,OAAO;wBAAEP,OAAO;oBAAU;gBAC5B;YACF,EAAE,eAAM;gBACNW;gBACAJ,OAAO;oBAAEP,OAAO;gBAAU;YAC5B;QACF;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,SAAS+B,kBACdvB,KAAwB,EACxBwB,UAAU5C,sBAAsB;IAEhC,IAAIoB,MAAMR,KAAK,KAAK,UAAU,OAAO;QAAEiC,QAAQ;IAAe;IAC9D,IAAIzB,MAAMR,KAAK,KAAK,YAAYQ,MAAMsB,KAAK,IAAIE,SAAS;QACtD,OAAO;YAAEC,QAAQ;YAAeL,SAASpB,MAAMoB,OAAO;YAAEE,OAAOtB,MAAMsB,KAAK;QAAC;IAC7E;IACA,OAAOI;AACT;AAEA;;;CAGC,GACD,SAASC,kBACPC,OAA6C,EAC7CC,GAAW,EACXxC,GAAW,EACXyC,KAAa;IAEb,IAAIC;IACJ,IAAI;;QACFA,cAAMH,2BAAAA,QAASI,OAAO,CAACH,uBAAQ;IACjC,EAAE,eAAM;QACN,OAAOH;IACT;IACA,IAAIK,QAAQ,MAAM,OAAOL;IACzB,IAAIO;IACJ,IAAI;QACFA,SAASC,KAAKC,KAAK,CAACJ;IACtB,EAAE,eAAM;QACN,OAAOL;IACT;IACA,IAAI,OAAOO,WAAW,YAAYA,WAAW,MAAM,OAAOP;IAC1D,MAAMU,SAASH;IACf,IAAI,OAAOG,OAAOC,EAAE,KAAK,YAAY,CAACC,OAAOC,QAAQ,CAACH,OAAOC,EAAE,GAAG,OAAOX;IACzE,IAAIrC,MAAM+C,OAAOC,EAAE,IAAIP,SAASM,OAAOC,EAAE,GAAGhD,KAAK,OAAOqC;IACxD,OAAOU;AACT;AAEA;;;CAGC,GACD,OAAO,SAASI,wBACdZ,OAA6C,EAC7CvC,GAAW,EACXyC,QAAQrD,4BAA4B;IAEpC,OAAOkD,kBAAkBC,SAASpD,2BAA2Ba,KAAKyC;AAGpE;AAEA;;;CAGC,GACD,OAAO,SAASW,oBACdb,OAA6C,EAC7CzC,YAAgC,EAChCE,GAAW,EACXyC,QAAQnD,wBAAwB;IAEhC,IAAIQ,iBAAiBuC,WAAW,OAAOA;IACvC,MAAMU,SAAST,kBAAkBC,SAASlD,uBAAuBW,KAAKyC;IACtE,IAAIM,CAAAA,0BAAAA,OAAQX,MAAM,MAAK,kBAAkBW,OAAOjD,YAAY,KAAKA,cAAc;QAC7E,OAAOuC;IACT;IACA,OAAOU;AACT;AAEA,MAAMM,wBAAwB;IAC5B,IAAI;QACF,OAAO,OAAOC,WAAW,cAAcjB,YAAYiB,OAAOC,cAAc;IAC1E,EAAE,eAAM;QACN,6DAA6D;QAC7D,OAAOlB;IACT;AACF;AAEA,MAAMmB,sBAAsB;IAC1B,IAAI;QACF,OAAO,OAAOF,WAAW,cAAcjB,YAAYiB,OAAOG,YAAY;IACxE,EAAE,eAAM;QACN,2DAA2D;QAC3D,OAAOpB;IACT;AACF;AAEA,MAAMqB,mBAAmB;IACvB,IAAI;QACF,OAAO,OAAOJ,WAAW,cAAcjB,YAAYiB,OAAOK,SAAS;IACrE,EAAE,eAAM;QACN,OAAOtB;IACT;AACF;AAEA,sEAAsE,GACtE,OAAO,SAASuB,0BAA0B5D,MAAMC,KAAKD,GAAG,EAAE;IACxD,OAAOmD,wBAAwBE,yBAAyBrD,SAASqC;AACnE;AAEA;;;CAGC,GACD,OAAO,SAASwB,sBACd/D,YAAgC,EAChCE,MAAMC,KAAKD,GAAG,EAAE;IAEhB,OAAOoD,oBAAoBI,uBAAuB1D,cAAcE,SAASqC;AAC3E;AAEA;;;;CAIC,GACD,OAAO,SAASyB,sBACdhE,YAAoB,EACpB,EACEyC,UAAUiB,qBAAqB,EAC/BxD,MAAMC,KAAKD,GAAG,EAC6C,GAAG,CAAC,CAAC;IAElE,MAAM+C,SAA0B;QAAEX,QAAQ;QAAgBtC;QAAckD,IAAIhD;IAAM;IAClF,IAAI;QACFuC,2BAAAA,QAASwB,OAAO,CAAC1E,uBAAuBwD,KAAKmB,SAAS,CAACjB;IACzD,EAAE,eAAM;IACN,mBAAmB;IACrB;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAekB,uBACpBnE,YAAoB,EACpB,EACEa,QAAQ,IAAMf,kBAAkB8D,oBAAoB5D,aAAa,EACjEyC,UAAUiB,qBAAqB,EAC/BxD,MAAMC,KAAKD,GAAG,EAKf,GAAG,CAAC,CAAC;IAEN,IAAI,CAACuC,SAAS,OAAO;IACrB,MAAM2B,OAAOd,oBAAoBb,SAASzC,cAAcE;IACxD,IAAI,CAACkE,MAAM,OAAO;IAClB,IAAI;QACF,IAAI,AAAC,CAAA,MAAMvD,OAAM,EAAGR,KAAK,KAAK,UAAU;YACtC2D,sBAAsBhE,cAAc;gBAAEyC;gBAASvC;YAAI;YACnD,OAAO;QACT;QACA,MAAMmE,UAAUf,oBAAoBb,SAASzC,cAAcE;QAC3D,oEAAoE;QACpE,sCAAsC;QACtC,IAAImE,WAAWA,QAAQnB,EAAE,KAAKkB,KAAKlB,EAAE,EAAE,OAAO;QAC9C,IAAImB,SAAS5B,QAAQ6B,UAAU,CAAC/E;QAChC,OAAO;IACT,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAgBA,MAAMgF,gBAAgB,IACpB,OAAOC,cAAc,eAAeA,UAAUC,MAAM,KAAK;AAE3D,MAAMC,kBAAkB,IACtB,OAAOC,aAAa,eAAeA,SAASC,eAAe,KAAK;AAElE;;;;CAIC,GACD,OAAO,SAASC,sBACdC,IAAyB;IAEzB,MAAM,EACJjE,KAAK,EACLkE,QAAQ,EACRC,mBAAmB,IAAMlB,2BAA2B,EACpDmB,WAAWV,aAAa,EACxBW,YAAYR,eAAe,EAC3BS,OAAO,CAACC,KAAO,IAAI3E,QAAc,CAACC,UAAYO,WAAWP,SAAS0E,IAAI,EACtEC,WAAW1F,eAAe,EAC3B,GAAGmF;IACJ,OAAO;QACL,IAAI;YACF,IAAIE,oBAAoB,OAAO;YAC/B,MAAMG,KAAKE;YACX,uEAAuE;YACvE,IAAI,CAACJ,cAAc,CAACC,aAAa,OAAO;YACxC,MAAMI,WAAWlD,kBAAkB,MAAMvB;YACzC,IAAI,CAACyE,UAAU,OAAO;YACtBP,SAASO;YACT,OAAO;QACT,EAAE,eAAM;YACN,OAAO;QACT;IACF;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,sBACdD,QAAuB,EACvB,EACE7C,UAAUc,uBAAuB,EACjCiC,gBAAgB9B,qBAAqB,EACrC1D,YAAY,EACZyF,SAAS,IAAMjC,OAAOkC,QAAQ,CAACD,MAAM,EAAE,EACvCvF,MAAMC,KAAKD,GAAG,EASf,GAAG,CAAC,CAAC;IAEN,IAAIoF,SAAShD,MAAM,KAAK,kBAAkBtC,iBAAiBuC,WAAW;QACpEyB,sBAAsBhE,cAAc;YAAEyC,SAAS+C;YAAetF;QAAI;IACpE;IACA,IAAI,CAACuC,SAAS;IACd,MAAMQ,SAA8B,aAAKqC;QAAUpC,IAAIhD;;IACvD,IAAI;QACFuC,QAAQwB,OAAO,CAAC5E,2BAA2B0D,KAAKmB,SAAS,CAACjB;IAC5D,EAAE,eAAM;QACN;IACF;IACA0C,QAAQC,IAAI,CACV,gHACA3C;IAEFwC;AACF;AAEA,+EAA+E,GAC/E,MAAMI,oBAAoB,IAAIC;AAE9B;;;;CAIC,GACD,OAAO,SAASC,sBAAsBC,SAAoB,EAAEhG,YAAoB;IAC9E,IAAIgG,aAAa,OAAOA,cAAc,UAAU;QAC9CH,kBAAkBI,GAAG,CAACD,WAAWhG;IACnC;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASkG,0BACdF,SAAoB;IAEpB,MAAMhG,eAAe6F,kBAAkB9D,GAAG,CAACiE;IAC3C,IAAIhG,iBAAiBuC,WAAW,OAAOA;IACvC,OAAOsC,sBAAsB;QAC3BhE,OAAO,IAAMf,kBAAkB8D,oBAAoB5D;QACnD+E,UAAU,CAACO,WAAaC,sBAAsBD,UAAU;gBAAEtF;YAAa;IACzE;AACF"}