@aglyn/tenant-feature-instance 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +3 -0
- package/jest.config.d.ts +36 -0
- package/package.json +45 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
- package/src/lib/components/duplicate-resource-dialog.js +217 -0
- package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
- package/src/lib/constants/firebase-config.d.ts +68 -0
- package/src/lib/constants/firebase-config.js +155 -0
- package/src/lib/constants/firebase-config.js.map +1 -0
- package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
- package/src/lib/constants/firebase-emulator-hosts.js +81 -0
- package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
- package/src/lib/constants/mock-data.d.ts +24 -0
- package/src/lib/constants/mock-data.js +49 -0
- package/src/lib/constants/mock-data.js.map +1 -0
- package/src/lib/constants/site-paths.d.ts +47 -0
- package/src/lib/constants/site-paths.js +66 -0
- package/src/lib/constants/site-paths.js.map +1 -0
- package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
- package/src/lib/hooks/firebase/auth-persistence.js +117 -0
- package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
- package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
- package/src/lib/hooks/firebase/firebase-services.js +522 -0
- package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
- package/src/lib/hooks/firebase/firestore-cache.js +152 -0
- package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
- package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
- package/src/lib/hooks/firestore-denial-reporter.js +293 -0
- package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
- package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
- package/src/lib/hooks/helpers/listen-options.js +92 -0
- package/src/lib/hooks/helpers/listen-options.js.map +1 -0
- package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
- package/src/lib/hooks/helpers/use-doc.js +213 -0
- package/src/lib/hooks/helpers/use-doc.js.map +1 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
- package/src/lib/hooks/host-collection-queries.d.ts +125 -0
- package/src/lib/hooks/host-collection-queries.js +126 -0
- package/src/lib/hooks/host-collection-queries.js.map +1 -0
- package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
- package/src/lib/hooks/list-filter-constraints.js +288 -0
- package/src/lib/hooks/list-filter-constraints.js.map +1 -0
- package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
- package/src/lib/hooks/sorted-collection-window.js +150 -0
- package/src/lib/hooks/sorted-collection-window.js.map +1 -0
- package/src/lib/hooks/use-component-version.d.ts +43 -0
- package/src/lib/hooks/use-component-version.js +73 -0
- package/src/lib/hooks/use-component-version.js.map +1 -0
- package/src/lib/hooks/use-component.d.ts +39 -0
- package/src/lib/hooks/use-component.js +55 -0
- package/src/lib/hooks/use-component.js.map +1 -0
- package/src/lib/hooks/use-console-host-route.d.ts +24 -0
- package/src/lib/hooks/use-console-host-route.js +77 -0
- package/src/lib/hooks/use-console-host-route.js.map +1 -0
- package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
- package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
- package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
- package/src/lib/hooks/use-firestore-collection.js +307 -0
- package/src/lib/hooks/use-firestore-collection.js.map +1 -0
- package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
- package/src/lib/hooks/use-firestore-doc.js +192 -0
- package/src/lib/hooks/use-firestore-doc.js.map +1 -0
- package/src/lib/hooks/use-form-version.d.ts +44 -0
- package/src/lib/hooks/use-form-version.js +73 -0
- package/src/lib/hooks/use-form-version.js.map +1 -0
- package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
- package/src/lib/hooks/use-host-activity-logger.js +96 -0
- package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
- package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
- package/src/lib/hooks/use-host-campaigns.js +93 -0
- package/src/lib/hooks/use-host-campaigns.js.map +1 -0
- package/src/lib/hooks/use-host-org-id.d.ts +96 -0
- package/src/lib/hooks/use-host-org-id.js +154 -0
- package/src/lib/hooks/use-host-org-id.js.map +1 -0
- package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-host-resource-api.js +60 -0
- package/src/lib/hooks/use-host-resource-api.js.map +1 -0
- package/src/lib/hooks/use-host-template.d.ts +44 -0
- package/src/lib/hooks/use-host-template.js +82 -0
- package/src/lib/hooks/use-host-template.js.map +1 -0
- package/src/lib/hooks/use-host-version-api.d.ts +59 -0
- package/src/lib/hooks/use-host-version-api.js +69 -0
- package/src/lib/hooks/use-host-version-api.js.map +1 -0
- package/src/lib/hooks/use-host.d.ts +101 -0
- package/src/lib/hooks/use-host.js +45 -0
- package/src/lib/hooks/use-host.js.map +1 -0
- package/src/lib/hooks/use-layout-version.d.ts +33 -0
- package/src/lib/hooks/use-layout-version.js +64 -0
- package/src/lib/hooks/use-layout-version.js.map +1 -0
- package/src/lib/hooks/use-layout.d.ts +41 -0
- package/src/lib/hooks/use-layout.js +49 -0
- package/src/lib/hooks/use-layout.js.map +1 -0
- package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
- package/src/lib/hooks/use-live-artifact-count.js +102 -0
- package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +57 -0
- package/src/lib/hooks/use-org-member-options.js +112 -0
- package/src/lib/hooks/use-org-member-options.js.map +1 -0
- package/src/lib/hooks/use-org-plan.d.ts +46 -0
- package/src/lib/hooks/use-org-plan.js +86 -0
- package/src/lib/hooks/use-org-plan.js.map +1 -0
- package/src/lib/hooks/use-paged-collection.d.ts +95 -0
- package/src/lib/hooks/use-paged-collection.js +130 -0
- package/src/lib/hooks/use-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-plugin-config.d.ts +38 -0
- package/src/lib/hooks/use-plugin-config.js +120 -0
- package/src/lib/hooks/use-plugin-config.js.map +1 -0
- package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
- package/src/lib/hooks/use-scope-tokens.js +100 -0
- package/src/lib/hooks/use-scope-tokens.js.map +1 -0
- package/src/lib/hooks/use-screen-version.d.ts +33 -0
- package/src/lib/hooks/use-screen-version.js +65 -0
- package/src/lib/hooks/use-screen-version.js.map +1 -0
- package/src/lib/hooks/use-screen.d.ts +68 -0
- package/src/lib/hooks/use-screen.js +50 -0
- package/src/lib/hooks/use-screen.js.map +1 -0
- package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
- package/src/lib/hooks/use-sending-identity-api.js +95 -0
- package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
- package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
- package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
- package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
- package/src/lib/hooks/use-switcher-collection.js +275 -0
- package/src/lib/hooks/use-switcher-collection.js.map +1 -0
- package/src/lib/hooks/use-user-name.d.ts +51 -0
- package/src/lib/hooks/use-user-name.js +90 -0
- package/src/lib/hooks/use-user-name.js.map +1 -0
- package/src/lib/hooks/use-user-photo.d.ts +50 -0
- package/src/lib/hooks/use-user-photo.js +87 -0
- package/src/lib/hooks/use-user-photo.js.map +1 -0
- package/src/lib/tenant.d.ts +61 -0
- package/src/lib/tenant.js +62 -0
- package/src/lib/tenant.js.map +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type FirebaseApp } from 'firebase/app';
|
|
2
|
+
import { type FirestoreLocalCache } from 'firebase/firestore';
|
|
3
|
+
import { type AuthPersistenceClass } from './auth-persistence';
|
|
4
|
+
import { type SharedClientStatePrunePlan } from './firestore-shared-client-state';
|
|
5
|
+
/**
|
|
6
|
+
* The durable cache's size bound: the SDK's own default, 40 MB, written down.
|
|
7
|
+
* See "The durable cache's size" on {@link localCacheFor}.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DURABLE_CACHE_SIZE_BYTES: number;
|
|
10
|
+
/**
|
|
11
|
+
* The Firestore `localCache` an origin of this persistence class may use
|
|
12
|
+
* (AGL-1456).
|
|
13
|
+
*
|
|
14
|
+
* ## Why this keys off the *auth* persistence class
|
|
15
|
+
*
|
|
16
|
+
* The name is historical — the class predates this file (AGL-1379) — but the
|
|
17
|
+
* question it answers was never auth-specific. `AuthPersistenceClass` is
|
|
18
|
+
* documented as *"how much of a session an origin is allowed to keep"*, and
|
|
19
|
+
* AGL-1099a's D6 justified `ephemeral` on the grounds that **"the customer can
|
|
20
|
+
* re-point the DNS and read this origin."**
|
|
21
|
+
*
|
|
22
|
+
* That reasoning does not stop at the credential. It is equally true of every
|
|
23
|
+
* document body Firestore caches in IndexedDB on that origin — measured, not
|
|
24
|
+
* argued: the PoC (`docs/design/agl-1099a-poc-findings.md` §5) dumped every
|
|
25
|
+
* object store on the origin and found full document contents under
|
|
26
|
+
* `firestore/<app>/<project>/main → remoteDocumentsV14`, alongside
|
|
27
|
+
* `firestore_clients_*` / `firestore_online_state_*` /
|
|
28
|
+
* `firestore_sequence_number_*` in `localStorage`. D6 hardened the key and
|
|
29
|
+
* left the safe behind it open; this closes it.
|
|
30
|
+
*
|
|
31
|
+
* **One declaration, two consequences — deliberately.** A separate
|
|
32
|
+
* `firestoreCache` prop would let a caller declare a custom console domain
|
|
33
|
+
* `ephemeral` for auth and still hand it a persistent cache, which is exactly
|
|
34
|
+
* the split that produced this bug. There is one fact about the origin, so
|
|
35
|
+
* there is one place to state it.
|
|
36
|
+
*
|
|
37
|
+
* ## The two classes
|
|
38
|
+
*
|
|
39
|
+
* - `durable` → `persistentLocalCache` with the multi-tab manager. Unchanged
|
|
40
|
+
* from what every host has always run, and it must stay that way:
|
|
41
|
+
* `*.aglyn.com` / `*.aglyn.app` are ours forever, and this cache is part of
|
|
42
|
+
* why console read volume is what it is (AGL-1440).
|
|
43
|
+
* - `ephemeral` → `memoryLocalCache`, so **nothing** reaches disk on an origin
|
|
44
|
+
* whose DNS the customer can re-point at their own server.
|
|
45
|
+
*
|
|
46
|
+
* ## The durable cache's size
|
|
47
|
+
*
|
|
48
|
+
* `cacheSizeBytes` is spelled out at the SDK default so the bound is stated
|
|
49
|
+
* here and cannot quietly become `CACHE_SIZE_UNLIMITED` if that default moves.
|
|
50
|
+
* Stating it changes nothing at runtime, and what it governs is narrower than
|
|
51
|
+
* the name suggests — measured against `@firebase/firestore` 4.17.1:
|
|
52
|
+
*
|
|
53
|
+
* - LRU collection is on without it: an unset `cacheSizeBytes` resolves to
|
|
54
|
+
* `LruParams.DEFAULT`, the same 40 MB threshold.
|
|
55
|
+
* - It runs only in the tab holding the primary lease, 1 minute after that tab
|
|
56
|
+
* becomes primary and every 5 minutes after.
|
|
57
|
+
* - The threshold is compared with the byte size of **cached documents**
|
|
58
|
+
* (`remoteDocumentsV14`) and nothing else. `targets` and `targetDocuments`
|
|
59
|
+
* rows are collected alongside documents once documents cross it, never
|
|
60
|
+
* before.
|
|
61
|
+
* - It never touches `localStorage`, where the multi-tab manager strands a
|
|
62
|
+
* record per tab and per query target; {@link pruneSharedClientStateFor}
|
|
63
|
+
* handles those.
|
|
64
|
+
*
|
|
65
|
+
* It is deliberately not lowered. A smaller cache evicts exactly the documents
|
|
66
|
+
* that make this cache worth having — a cold load resuming from disk, and
|
|
67
|
+
* listeners painting through a stale session — and a collected target is given
|
|
68
|
+
* a NEW id the next time its query is listened to, which adds a `localStorage`
|
|
69
|
+
* record rather than removing one.
|
|
70
|
+
*
|
|
71
|
+
* ## Why the memory cache gets the LRU collector, not the default
|
|
72
|
+
*
|
|
73
|
+
* `memoryLocalCache()` defaults to `memoryEagerGarbageCollector`, which drops
|
|
74
|
+
* a document the moment no listener references it — so navigating away from a
|
|
75
|
+
* console page and back re-reads its whole working set from the server. The
|
|
76
|
+
* LRU collector keeps documents in the JS heap up to its 40 MB default instead,
|
|
77
|
+
* which recovers most of the intra-session read saving `persistentLocalCache`
|
|
78
|
+
* was giving us.
|
|
79
|
+
*
|
|
80
|
+
* It costs nothing against the property being protected: the LRU collector is
|
|
81
|
+
* still **memory only**. Nothing is written to IndexedDB, nothing survives a
|
|
82
|
+
* tab close, and a page load on a re-pointed origin starts empty — the same
|
|
83
|
+
* bound `inMemoryPersistence` gives the refresh token.
|
|
84
|
+
*
|
|
85
|
+
* The read cost that remains is real and is the stated trade: a **cold** page
|
|
86
|
+
* load on a custom console domain re-reads its working set where a durable
|
|
87
|
+
* origin would have resumed from disk, and multiple tabs no longer share one
|
|
88
|
+
* backend connection through `persistentMultipleTabManager`. That is the right
|
|
89
|
+
* trade for an Enterprise-only feature, but it is a trade.
|
|
90
|
+
*
|
|
91
|
+
* ## `Failed to obtain primary lease for action '…'` is not a bug to fix
|
|
92
|
+
*
|
|
93
|
+
* Exactly one open tab holds the IndexedDB primary lease and applies remote
|
|
94
|
+
* events; the rest read the results it writes. When the lease changes hands —
|
|
95
|
+
* a tab opening, closing, or coming back to the foreground — a transaction
|
|
96
|
+
* already in flight on the losing tab finds itself no longer primary, and the
|
|
97
|
+
* SDK `logError`s that message immediately before throwing a
|
|
98
|
+
* `failed-precondition` that its OWN multi-tab sync engine catches
|
|
99
|
+
* (`ignoreIfPrimaryLeaseLoss`) and downgrades to a debug line. The tab then
|
|
100
|
+
* continues as a secondary. Nothing is dropped and no listener stops.
|
|
101
|
+
*
|
|
102
|
+
* It is therefore noise from a handled handoff, and it scales with how many
|
|
103
|
+
* tabs are churning rather than with anything the console does. The message
|
|
104
|
+
* names a lease, so the obvious reading is that the multi-tab manager is
|
|
105
|
+
* missing and should be added — it is already here, which is why the message
|
|
106
|
+
* appears at all: `persistentSingleTabManager` refuses the second tab's
|
|
107
|
+
* persistence outright instead of sharing it. Confirmed at runtime by the
|
|
108
|
+
* `owner` record in `firestore/<app>/<project>/main`, which carries
|
|
109
|
+
* `allowTabSynchronization: true`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function localCacheFor(originClass: AuthPersistenceClass): FirestoreLocalCache;
|
|
112
|
+
/**
|
|
113
|
+
* Prunes the multi-tab records the durable cache strands in `localStorage`
|
|
114
|
+
* (AGL-2845), for an app whose Firestore this page has just initialized.
|
|
115
|
+
*
|
|
116
|
+
* Keyed off the same origin class as {@link localCacheFor}, because the
|
|
117
|
+
* durable class is the only one running `persistentMultipleTabManager`, the
|
|
118
|
+
* only writer of those records. An ephemeral origin's memory cache writes
|
|
119
|
+
* none, so it resolves without looking.
|
|
120
|
+
*
|
|
121
|
+
* Fire-and-forget: it never rejects and nothing waits on it. What it deletes,
|
|
122
|
+
* and why a live tab's records never are, is in
|
|
123
|
+
* `firestore-shared-client-state.ts`.
|
|
124
|
+
*/
|
|
125
|
+
export declare function pruneSharedClientStateFor(originClass: AuthPersistenceClass, app: FirebaseApp): Promise<SharedClientStatePrunePlan | undefined>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { memoryLocalCache, memoryLruGarbageCollector, persistentLocalCache, persistentMultipleTabManager } from "firebase/firestore";
|
|
18
|
+
import { pruneBrowserSharedClientState } from "./firestore-shared-client-state.js";
|
|
19
|
+
/**
|
|
20
|
+
* The durable cache's size bound: the SDK's own default, 40 MB, written down.
|
|
21
|
+
* See "The durable cache's size" on {@link localCacheFor}.
|
|
22
|
+
*/ export const DURABLE_CACHE_SIZE_BYTES = 40 * 1024 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* The Firestore `localCache` an origin of this persistence class may use
|
|
25
|
+
* (AGL-1456).
|
|
26
|
+
*
|
|
27
|
+
* ## Why this keys off the *auth* persistence class
|
|
28
|
+
*
|
|
29
|
+
* The name is historical — the class predates this file (AGL-1379) — but the
|
|
30
|
+
* question it answers was never auth-specific. `AuthPersistenceClass` is
|
|
31
|
+
* documented as *"how much of a session an origin is allowed to keep"*, and
|
|
32
|
+
* AGL-1099a's D6 justified `ephemeral` on the grounds that **"the customer can
|
|
33
|
+
* re-point the DNS and read this origin."**
|
|
34
|
+
*
|
|
35
|
+
* That reasoning does not stop at the credential. It is equally true of every
|
|
36
|
+
* document body Firestore caches in IndexedDB on that origin — measured, not
|
|
37
|
+
* argued: the PoC (`docs/design/agl-1099a-poc-findings.md` §5) dumped every
|
|
38
|
+
* object store on the origin and found full document contents under
|
|
39
|
+
* `firestore/<app>/<project>/main → remoteDocumentsV14`, alongside
|
|
40
|
+
* `firestore_clients_*` / `firestore_online_state_*` /
|
|
41
|
+
* `firestore_sequence_number_*` in `localStorage`. D6 hardened the key and
|
|
42
|
+
* left the safe behind it open; this closes it.
|
|
43
|
+
*
|
|
44
|
+
* **One declaration, two consequences — deliberately.** A separate
|
|
45
|
+
* `firestoreCache` prop would let a caller declare a custom console domain
|
|
46
|
+
* `ephemeral` for auth and still hand it a persistent cache, which is exactly
|
|
47
|
+
* the split that produced this bug. There is one fact about the origin, so
|
|
48
|
+
* there is one place to state it.
|
|
49
|
+
*
|
|
50
|
+
* ## The two classes
|
|
51
|
+
*
|
|
52
|
+
* - `durable` → `persistentLocalCache` with the multi-tab manager. Unchanged
|
|
53
|
+
* from what every host has always run, and it must stay that way:
|
|
54
|
+
* `*.aglyn.com` / `*.aglyn.app` are ours forever, and this cache is part of
|
|
55
|
+
* why console read volume is what it is (AGL-1440).
|
|
56
|
+
* - `ephemeral` → `memoryLocalCache`, so **nothing** reaches disk on an origin
|
|
57
|
+
* whose DNS the customer can re-point at their own server.
|
|
58
|
+
*
|
|
59
|
+
* ## The durable cache's size
|
|
60
|
+
*
|
|
61
|
+
* `cacheSizeBytes` is spelled out at the SDK default so the bound is stated
|
|
62
|
+
* here and cannot quietly become `CACHE_SIZE_UNLIMITED` if that default moves.
|
|
63
|
+
* Stating it changes nothing at runtime, and what it governs is narrower than
|
|
64
|
+
* the name suggests — measured against `@firebase/firestore` 4.17.1:
|
|
65
|
+
*
|
|
66
|
+
* - LRU collection is on without it: an unset `cacheSizeBytes` resolves to
|
|
67
|
+
* `LruParams.DEFAULT`, the same 40 MB threshold.
|
|
68
|
+
* - It runs only in the tab holding the primary lease, 1 minute after that tab
|
|
69
|
+
* becomes primary and every 5 minutes after.
|
|
70
|
+
* - The threshold is compared with the byte size of **cached documents**
|
|
71
|
+
* (`remoteDocumentsV14`) and nothing else. `targets` and `targetDocuments`
|
|
72
|
+
* rows are collected alongside documents once documents cross it, never
|
|
73
|
+
* before.
|
|
74
|
+
* - It never touches `localStorage`, where the multi-tab manager strands a
|
|
75
|
+
* record per tab and per query target; {@link pruneSharedClientStateFor}
|
|
76
|
+
* handles those.
|
|
77
|
+
*
|
|
78
|
+
* It is deliberately not lowered. A smaller cache evicts exactly the documents
|
|
79
|
+
* that make this cache worth having — a cold load resuming from disk, and
|
|
80
|
+
* listeners painting through a stale session — and a collected target is given
|
|
81
|
+
* a NEW id the next time its query is listened to, which adds a `localStorage`
|
|
82
|
+
* record rather than removing one.
|
|
83
|
+
*
|
|
84
|
+
* ## Why the memory cache gets the LRU collector, not the default
|
|
85
|
+
*
|
|
86
|
+
* `memoryLocalCache()` defaults to `memoryEagerGarbageCollector`, which drops
|
|
87
|
+
* a document the moment no listener references it — so navigating away from a
|
|
88
|
+
* console page and back re-reads its whole working set from the server. The
|
|
89
|
+
* LRU collector keeps documents in the JS heap up to its 40 MB default instead,
|
|
90
|
+
* which recovers most of the intra-session read saving `persistentLocalCache`
|
|
91
|
+
* was giving us.
|
|
92
|
+
*
|
|
93
|
+
* It costs nothing against the property being protected: the LRU collector is
|
|
94
|
+
* still **memory only**. Nothing is written to IndexedDB, nothing survives a
|
|
95
|
+
* tab close, and a page load on a re-pointed origin starts empty — the same
|
|
96
|
+
* bound `inMemoryPersistence` gives the refresh token.
|
|
97
|
+
*
|
|
98
|
+
* The read cost that remains is real and is the stated trade: a **cold** page
|
|
99
|
+
* load on a custom console domain re-reads its working set where a durable
|
|
100
|
+
* origin would have resumed from disk, and multiple tabs no longer share one
|
|
101
|
+
* backend connection through `persistentMultipleTabManager`. That is the right
|
|
102
|
+
* trade for an Enterprise-only feature, but it is a trade.
|
|
103
|
+
*
|
|
104
|
+
* ## `Failed to obtain primary lease for action '…'` is not a bug to fix
|
|
105
|
+
*
|
|
106
|
+
* Exactly one open tab holds the IndexedDB primary lease and applies remote
|
|
107
|
+
* events; the rest read the results it writes. When the lease changes hands —
|
|
108
|
+
* a tab opening, closing, or coming back to the foreground — a transaction
|
|
109
|
+
* already in flight on the losing tab finds itself no longer primary, and the
|
|
110
|
+
* SDK `logError`s that message immediately before throwing a
|
|
111
|
+
* `failed-precondition` that its OWN multi-tab sync engine catches
|
|
112
|
+
* (`ignoreIfPrimaryLeaseLoss`) and downgrades to a debug line. The tab then
|
|
113
|
+
* continues as a secondary. Nothing is dropped and no listener stops.
|
|
114
|
+
*
|
|
115
|
+
* It is therefore noise from a handled handoff, and it scales with how many
|
|
116
|
+
* tabs are churning rather than with anything the console does. The message
|
|
117
|
+
* names a lease, so the obvious reading is that the multi-tab manager is
|
|
118
|
+
* missing and should be added — it is already here, which is why the message
|
|
119
|
+
* appears at all: `persistentSingleTabManager` refuses the second tab's
|
|
120
|
+
* persistence outright instead of sharing it. Confirmed at runtime by the
|
|
121
|
+
* `owner` record in `firestore/<app>/<project>/main`, which carries
|
|
122
|
+
* `allowTabSynchronization: true`.
|
|
123
|
+
*/ export function localCacheFor(originClass) {
|
|
124
|
+
if (originClass === 'ephemeral') {
|
|
125
|
+
return memoryLocalCache({
|
|
126
|
+
garbageCollector: memoryLruGarbageCollector()
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return persistentLocalCache({
|
|
130
|
+
tabManager: persistentMultipleTabManager(),
|
|
131
|
+
cacheSizeBytes: DURABLE_CACHE_SIZE_BYTES
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Prunes the multi-tab records the durable cache strands in `localStorage`
|
|
136
|
+
* (AGL-2845), for an app whose Firestore this page has just initialized.
|
|
137
|
+
*
|
|
138
|
+
* Keyed off the same origin class as {@link localCacheFor}, because the
|
|
139
|
+
* durable class is the only one running `persistentMultipleTabManager`, the
|
|
140
|
+
* only writer of those records. An ephemeral origin's memory cache writes
|
|
141
|
+
* none, so it resolves without looking.
|
|
142
|
+
*
|
|
143
|
+
* Fire-and-forget: it never rejects and nothing waits on it. What it deletes,
|
|
144
|
+
* and why a live tab's records never are, is in
|
|
145
|
+
* `firestore-shared-client-state.ts`.
|
|
146
|
+
*/ export function pruneSharedClientStateFor(originClass, app) {
|
|
147
|
+
var _app_options;
|
|
148
|
+
if (originClass === 'ephemeral') return Promise.resolve(undefined);
|
|
149
|
+
return pruneBrowserSharedClientState(app.name, (_app_options = app.options) == null ? void 0 : _app_options.projectId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=firestore-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/firebase/firestore-cache.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { type FirebaseApp } from 'firebase/app'\nimport {\n type FirestoreLocalCache,\n memoryLocalCache,\n memoryLruGarbageCollector,\n persistentLocalCache,\n persistentMultipleTabManager,\n} from 'firebase/firestore'\n\nimport { type AuthPersistenceClass } from './auth-persistence'\nimport {\n pruneBrowserSharedClientState,\n type SharedClientStatePrunePlan,\n} from './firestore-shared-client-state'\n\n/**\n * The durable cache's size bound: the SDK's own default, 40 MB, written down.\n * See \"The durable cache's size\" on {@link localCacheFor}.\n */\nexport const DURABLE_CACHE_SIZE_BYTES = 40 * 1024 * 1024\n\n/**\n * The Firestore `localCache` an origin of this persistence class may use\n * (AGL-1456).\n *\n * ## Why this keys off the *auth* persistence class\n *\n * The name is historical — the class predates this file (AGL-1379) — but the\n * question it answers was never auth-specific. `AuthPersistenceClass` is\n * documented as *\"how much of a session an origin is allowed to keep\"*, and\n * AGL-1099a's D6 justified `ephemeral` on the grounds that **\"the customer can\n * re-point the DNS and read this origin.\"**\n *\n * That reasoning does not stop at the credential. It is equally true of every\n * document body Firestore caches in IndexedDB on that origin — measured, not\n * argued: the PoC (`docs/design/agl-1099a-poc-findings.md` §5) dumped every\n * object store on the origin and found full document contents under\n * `firestore/<app>/<project>/main → remoteDocumentsV14`, alongside\n * `firestore_clients_*` / `firestore_online_state_*` /\n * `firestore_sequence_number_*` in `localStorage`. D6 hardened the key and\n * left the safe behind it open; this closes it.\n *\n * **One declaration, two consequences — deliberately.** A separate\n * `firestoreCache` prop would let a caller declare a custom console domain\n * `ephemeral` for auth and still hand it a persistent cache, which is exactly\n * the split that produced this bug. There is one fact about the origin, so\n * there is one place to state it.\n *\n * ## The two classes\n *\n * - `durable` → `persistentLocalCache` with the multi-tab manager. Unchanged\n * from what every host has always run, and it must stay that way:\n * `*.aglyn.com` / `*.aglyn.app` are ours forever, and this cache is part of\n * why console read volume is what it is (AGL-1440).\n * - `ephemeral` → `memoryLocalCache`, so **nothing** reaches disk on an origin\n * whose DNS the customer can re-point at their own server.\n *\n * ## The durable cache's size\n *\n * `cacheSizeBytes` is spelled out at the SDK default so the bound is stated\n * here and cannot quietly become `CACHE_SIZE_UNLIMITED` if that default moves.\n * Stating it changes nothing at runtime, and what it governs is narrower than\n * the name suggests — measured against `@firebase/firestore` 4.17.1:\n *\n * - LRU collection is on without it: an unset `cacheSizeBytes` resolves to\n * `LruParams.DEFAULT`, the same 40 MB threshold.\n * - It runs only in the tab holding the primary lease, 1 minute after that tab\n * becomes primary and every 5 minutes after.\n * - The threshold is compared with the byte size of **cached documents**\n * (`remoteDocumentsV14`) and nothing else. `targets` and `targetDocuments`\n * rows are collected alongside documents once documents cross it, never\n * before.\n * - It never touches `localStorage`, where the multi-tab manager strands a\n * record per tab and per query target; {@link pruneSharedClientStateFor}\n * handles those.\n *\n * It is deliberately not lowered. A smaller cache evicts exactly the documents\n * that make this cache worth having — a cold load resuming from disk, and\n * listeners painting through a stale session — and a collected target is given\n * a NEW id the next time its query is listened to, which adds a `localStorage`\n * record rather than removing one.\n *\n * ## Why the memory cache gets the LRU collector, not the default\n *\n * `memoryLocalCache()` defaults to `memoryEagerGarbageCollector`, which drops\n * a document the moment no listener references it — so navigating away from a\n * console page and back re-reads its whole working set from the server. The\n * LRU collector keeps documents in the JS heap up to its 40 MB default instead,\n * which recovers most of the intra-session read saving `persistentLocalCache`\n * was giving us.\n *\n * It costs nothing against the property being protected: the LRU collector is\n * still **memory only**. Nothing is written to IndexedDB, nothing survives a\n * tab close, and a page load on a re-pointed origin starts empty — the same\n * bound `inMemoryPersistence` gives the refresh token.\n *\n * The read cost that remains is real and is the stated trade: a **cold** page\n * load on a custom console domain re-reads its working set where a durable\n * origin would have resumed from disk, and multiple tabs no longer share one\n * backend connection through `persistentMultipleTabManager`. That is the right\n * trade for an Enterprise-only feature, but it is a trade.\n *\n * ## `Failed to obtain primary lease for action '…'` is not a bug to fix\n *\n * Exactly one open tab holds the IndexedDB primary lease and applies remote\n * events; the rest read the results it writes. When the lease changes hands —\n * a tab opening, closing, or coming back to the foreground — a transaction\n * already in flight on the losing tab finds itself no longer primary, and the\n * SDK `logError`s that message immediately before throwing a\n * `failed-precondition` that its OWN multi-tab sync engine catches\n * (`ignoreIfPrimaryLeaseLoss`) and downgrades to a debug line. The tab then\n * continues as a secondary. Nothing is dropped and no listener stops.\n *\n * It is therefore noise from a handled handoff, and it scales with how many\n * tabs are churning rather than with anything the console does. The message\n * names a lease, so the obvious reading is that the multi-tab manager is\n * missing and should be added — it is already here, which is why the message\n * appears at all: `persistentSingleTabManager` refuses the second tab's\n * persistence outright instead of sharing it. Confirmed at runtime by the\n * `owner` record in `firestore/<app>/<project>/main`, which carries\n * `allowTabSynchronization: true`.\n */\nexport function localCacheFor(\n originClass: AuthPersistenceClass,\n): FirestoreLocalCache {\n if (originClass === 'ephemeral') {\n return memoryLocalCache({ garbageCollector: memoryLruGarbageCollector() })\n }\n return persistentLocalCache({\n tabManager: persistentMultipleTabManager(),\n cacheSizeBytes: DURABLE_CACHE_SIZE_BYTES,\n })\n}\n\n/**\n * Prunes the multi-tab records the durable cache strands in `localStorage`\n * (AGL-2845), for an app whose Firestore this page has just initialized.\n *\n * Keyed off the same origin class as {@link localCacheFor}, because the\n * durable class is the only one running `persistentMultipleTabManager`, the\n * only writer of those records. An ephemeral origin's memory cache writes\n * none, so it resolves without looking.\n *\n * Fire-and-forget: it never rejects and nothing waits on it. What it deletes,\n * and why a live tab's records never are, is in\n * `firestore-shared-client-state.ts`.\n */\nexport function pruneSharedClientStateFor(\n originClass: AuthPersistenceClass,\n app: FirebaseApp,\n): Promise<SharedClientStatePrunePlan | undefined> {\n if (originClass === 'ephemeral') return Promise.resolve(undefined)\n return pruneBrowserSharedClientState(app.name, app.options?.projectId)\n}\n"],"names":["memoryLocalCache","memoryLruGarbageCollector","persistentLocalCache","persistentMultipleTabManager","pruneBrowserSharedClientState","DURABLE_CACHE_SIZE_BYTES","localCacheFor","originClass","garbageCollector","tabManager","cacheSizeBytes","pruneSharedClientStateFor","app","Promise","resolve","undefined","name","options","projectId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAEEA,gBAAgB,EAChBC,yBAAyB,EACzBC,oBAAoB,EACpBC,4BAA4B,QACvB,qBAAoB;AAG3B,SACEC,6BAA6B,QAExB,qCAAiC;AAExC;;;CAGC,GACD,OAAO,MAAMC,2BAA2B,KAAK,OAAO,KAAI;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoGC,GACD,OAAO,SAASC,cACdC,WAAiC;IAEjC,IAAIA,gBAAgB,aAAa;QAC/B,OAAOP,iBAAiB;YAAEQ,kBAAkBP;QAA4B;IAC1E;IACA,OAAOC,qBAAqB;QAC1BO,YAAYN;QACZO,gBAAgBL;IAClB;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASM,0BACdJ,WAAiC,EACjCK,GAAgB;QAG+BA;IAD/C,IAAIL,gBAAgB,aAAa,OAAOM,QAAQC,OAAO,CAACC;IACxD,OAAOX,8BAA8BQ,IAAII,IAAI,GAAEJ,eAAAA,IAAIK,OAAO,qBAAXL,aAAaM,SAAS;AACvE"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The multi-tab records the Firestore SDK strands in `localStorage`, and the
|
|
19
|
+
* prune that bounds them (AGL-2845).
|
|
20
|
+
*
|
|
21
|
+
* ## What leaks
|
|
22
|
+
*
|
|
23
|
+
* `persistentMultipleTabManager` shares one IndexedDB cache between tabs and
|
|
24
|
+
* coordinates them through `localStorage` (`WebStorageSharedClientState`).
|
|
25
|
+
* Three of its record families grow without bound, all keyed under the
|
|
26
|
+
* persistence prefix `firestore/<appName>/<projectId>/`:
|
|
27
|
+
*
|
|
28
|
+
* | record | holds |
|
|
29
|
+
* | -- | -- |
|
|
30
|
+
* | `firestore_clients_<prefix>_<clientId>` | the target ids one tab listens to, and when that set last changed |
|
|
31
|
+
* | `firestore_targets_<prefix>_<targetId>` | a target's `current` / `not-current` / `rejected` state, written by the primary tab |
|
|
32
|
+
* | `firestore_zombie_<prefix>_<clientId>` | when a tab closed, so no other tab waits on its lease |
|
|
33
|
+
*
|
|
34
|
+
* Measured against `@firebase/firestore` 4.17.1, and unchanged on the SDK's
|
|
35
|
+
* main branch, a target record is removed in exactly one place:
|
|
36
|
+
* `clearQueryState`, which runs only when the PRIMARY tab unlistens a target no
|
|
37
|
+
* other tab holds. Every other way a target ends leaves its record behind:
|
|
38
|
+
*
|
|
39
|
+
* - a secondary tab unlistens — the primary releases the target and never
|
|
40
|
+
* clears its state;
|
|
41
|
+
* - the backend rejects the listen — `rejected` is written and nothing removes
|
|
42
|
+
* it, so a missing composite index or a `permission-denied` stays on disk;
|
|
43
|
+
* - any tab closes or reloads — `pagehide` removes that tab's own client record
|
|
44
|
+
* and none of its targets.
|
|
45
|
+
*
|
|
46
|
+
* A client record is stranded the same way when a tab ends without `pagehide`
|
|
47
|
+
* (a crash, a discarded tab, a browser quit), and a zombie marker survives
|
|
48
|
+
* whenever its tab's shutdown does not finish and no primary later finds that
|
|
49
|
+
* tab's heartbeat row. Nothing sweeps any of the three. A target id is reused
|
|
50
|
+
* for an identical query, so the pile grows with the number of distinct queries
|
|
51
|
+
* a browser has ever listened to: one production console profile held 2,531
|
|
52
|
+
* target records going back five weeks, against 3 live tabs.
|
|
53
|
+
*
|
|
54
|
+
* ## Why it cannot be left alone
|
|
55
|
+
*
|
|
56
|
+
* `localStorage` holds about 5 MB per origin. Once it is full, the next
|
|
57
|
+
* `setItem` inside the SDK throws `QuotaExceededError` on the SDK's async
|
|
58
|
+
* queue. That marks the queue failed, and every later Firestore operation in
|
|
59
|
+
* the tab dies on `INTERNAL ASSERTION FAILED: Unexpected state (ID: b815)` — the
|
|
60
|
+
* whole Firestore client, not only tab sync. Upstream treats a full
|
|
61
|
+
* `localStorage` as expected under multi-tab (firebase-js-sdk#8305) and closed
|
|
62
|
+
* the leak report without a fix (#9209), so no SDK upgrade removes this.
|
|
63
|
+
*
|
|
64
|
+
* ## What is safe to delete
|
|
65
|
+
*
|
|
66
|
+
* The constraint is absolute: a record a live tab depends on is never removed.
|
|
67
|
+
*
|
|
68
|
+
* - **A target record no surviving client record lists is dead data.** The SDK
|
|
69
|
+
* reads one only when some tab lists that target as active
|
|
70
|
+
* (`addLocalQueryTarget` asks `isActiveQueryTarget` first), and it ignores a
|
|
71
|
+
* storage event reporting one deleted. A record any readable client lists is
|
|
72
|
+
* kept, whether or not that client is live: without it, the next tab to join
|
|
73
|
+
* the target would start it `not-current`.
|
|
74
|
+
* - **A client record is deleted only after the SDK itself has given up on that
|
|
75
|
+
* tab.** Its heartbeat row must be gone from IndexedDB's `clientMetadata`
|
|
76
|
+
* store — a running tab rewrites that row every 4 seconds, and the primary
|
|
77
|
+
* deletes rows 30 minutes stale — AND the record must not have changed for
|
|
78
|
+
* {@link STALE_SHARED_CLIENT_STATE_MS}. Deleting a live tab's client record
|
|
79
|
+
* would make every other tab drop its targets, and the primary would stop
|
|
80
|
+
* watching them. The heartbeat is what separates a live tab left idle (fresh
|
|
81
|
+
* row, old record) from a dead one; the age covers a tab frozen rather than
|
|
82
|
+
* closed, and a tab that started between the heartbeat read and the scan.
|
|
83
|
+
* - **When the heartbeat cannot be read** — no `indexedDB.databases()`, no
|
|
84
|
+
* database yet, a schema without that store — no client record is deleted,
|
|
85
|
+
* and every readable client record keeps pinning its targets.
|
|
86
|
+
* - **A client record that does not parse** pins nothing and is never deleted,
|
|
87
|
+
* matching the SDK, which ignores it the same way.
|
|
88
|
+
* - **A zombie marker older than {@link STALE_SHARED_CLIENT_STATE_MS}** is
|
|
89
|
+
* deleted. The SDK consults one only while the closed tab's heartbeat could
|
|
90
|
+
* still count as fresh, and a day later it cannot.
|
|
91
|
+
*
|
|
92
|
+
* Mutation, online-state, sequence-number and bundle records are never touched,
|
|
93
|
+
* and neither is anything under another app's or another database's prefix.
|
|
94
|
+
*
|
|
95
|
+
* ## When it runs
|
|
96
|
+
*
|
|
97
|
+
* Once per page load, after the provider has initialized the durable cache
|
|
98
|
+
* (`pruneSharedClientStateFor` in `firestore-cache.ts`). Each load clears what
|
|
99
|
+
* earlier sessions stranded, which is what bounds the growth; a one-off cleanup
|
|
100
|
+
* would not, because the SDK strands more on every close.
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* How long a client record or zombie marker must sit unchanged before it can
|
|
104
|
+
* be treated as dead. A day, against the SDK's own 30-minute inactivity
|
|
105
|
+
* horizon, because deleting a live tab's record is the one outcome this must
|
|
106
|
+
* never produce.
|
|
107
|
+
*/
|
|
108
|
+
export declare const STALE_SHARED_CLIENT_STATE_MS: number;
|
|
109
|
+
/**
|
|
110
|
+
* `firestore/<appName>/<projectId>/` — the SDK's `indexedDbStoragePrefix` for
|
|
111
|
+
* the default database, the only one the console opens. Every record family
|
|
112
|
+
* above hangs off it, and so does the IndexedDB database, `<prefix>main`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function firestorePersistencePrefix(appName: string, projectId: string): string;
|
|
115
|
+
export interface SharedClientStateTally {
|
|
116
|
+
kept: number;
|
|
117
|
+
removed: number;
|
|
118
|
+
}
|
|
119
|
+
export interface SharedClientStatePrunePlan {
|
|
120
|
+
/** Every record to delete. */
|
|
121
|
+
remove: string[];
|
|
122
|
+
targets: SharedClientStateTally;
|
|
123
|
+
clients: SharedClientStateTally;
|
|
124
|
+
zombies: SharedClientStateTally;
|
|
125
|
+
/** Whether the heartbeat was readable — without it no client record is eligible. */
|
|
126
|
+
heartbeat: boolean;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Decides what to delete. Reads `storage` and changes nothing.
|
|
130
|
+
*
|
|
131
|
+
* `heartbeatClientIds` is the set of client ids that still have a row in the
|
|
132
|
+
* SDK's `clientMetadata` store, or `undefined` when it could not be read — in
|
|
133
|
+
* which case no client record is eligible.
|
|
134
|
+
*/
|
|
135
|
+
export declare function planSharedClientStatePrune(storage: Pick<Storage, 'length' | 'key' | 'getItem'>, prefix: string, heartbeatClientIds: ReadonlySet<string> | undefined, now: number): SharedClientStatePrunePlan;
|
|
136
|
+
/**
|
|
137
|
+
* The client ids that still have a heartbeat row in the SDK's `clientMetadata`
|
|
138
|
+
* store, or `undefined` when that cannot be established.
|
|
139
|
+
*
|
|
140
|
+
* Opens only a database that already exists, at whatever version it already
|
|
141
|
+
* has: `open(name)` on a missing database would CREATE it at version 1, and the
|
|
142
|
+
* SDK would then try to migrate a schema that was never written. The connection
|
|
143
|
+
* serves one read-only request and is closed — at once, too, on
|
|
144
|
+
* `versionchange`, so it can never hold up the SDK's own upgrade.
|
|
145
|
+
*/
|
|
146
|
+
export declare function readHeartbeatClientIds(factory: IDBFactory | undefined, databaseName: string, timeoutMs?: number): Promise<ReadonlySet<string> | undefined>;
|
|
147
|
+
export interface PruneSharedClientStateOptions {
|
|
148
|
+
storage: Storage;
|
|
149
|
+
prefix: string;
|
|
150
|
+
heartbeatClientIds: () => Promise<ReadonlySet<string> | undefined>;
|
|
151
|
+
now?: () => number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Reads the heartbeat, then plans and deletes in one synchronous pass, so
|
|
155
|
+
* nothing else in this tab can write a record between the scan and the
|
|
156
|
+
* deletes. The heartbeat is read BEFORE the scan: a tab that starts in between
|
|
157
|
+
* shows up with a brand-new record and no heartbeat row, which the age rule
|
|
158
|
+
* keeps.
|
|
159
|
+
*
|
|
160
|
+
* Never rejects. Resolves `undefined` when storage could not be read.
|
|
161
|
+
*/
|
|
162
|
+
export declare function pruneSharedClientState({ storage, prefix, heartbeatClientIds, now, }: PruneSharedClientStateOptions): Promise<SharedClientStatePrunePlan | undefined>;
|
|
163
|
+
/**
|
|
164
|
+
* {@link pruneSharedClientState} against this page's own `localStorage` and
|
|
165
|
+
* IndexedDB. Resolves `undefined` off-browser, without an app name or project
|
|
166
|
+
* id, or where site data is blocked (reading `window.localStorage` throws
|
|
167
|
+
* there).
|
|
168
|
+
*/
|
|
169
|
+
export declare function pruneBrowserSharedClientState(appName: string | undefined, projectId: string | undefined): Promise<SharedClientStatePrunePlan | undefined>;
|