@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,77 @@
|
|
|
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 { doc, getDoc } from "firebase/firestore";
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { useFirestore } from "./firebase/firebase-services.js";
|
|
20
|
+
/**
|
|
21
|
+
* Console route base for a host: `/{orgSlug}/hosts/{subdomain}` (AGL-673).
|
|
22
|
+
*
|
|
23
|
+
* Plugin components are handed a host DOC ID and used to build links as
|
|
24
|
+
* `/{hostDocId}/…`. Console routes have been `/[orgSlug]/hosts/[subdomain]/…`
|
|
25
|
+
* since AGL-621/622, so every one of those links silently began 404ing —
|
|
26
|
+
* found three separate times in different plugins, each looking like a new
|
|
27
|
+
* bug. This is the one resolution, so the next plugin does not become the
|
|
28
|
+
* fourth.
|
|
29
|
+
*
|
|
30
|
+
* `null` until resolved, and `null` forever if the org has no slug. Callers
|
|
31
|
+
* must render plain text rather than a link in that case: a link to nowhere
|
|
32
|
+
* is worse than no link.
|
|
33
|
+
*
|
|
34
|
+
* Reads `hostIndex` (signed-in readable, mirrors `subdomain` and `orgId`)
|
|
35
|
+
* plus the org doc for its slug — both cheap and already cached by the SDK
|
|
36
|
+
* on most screens.
|
|
37
|
+
*/ export function useConsoleHostRoute(hostId) {
|
|
38
|
+
const firestore = useFirestore();
|
|
39
|
+
const [resolved, setResolved] = useState({
|
|
40
|
+
base: null,
|
|
41
|
+
orgSlug: null,
|
|
42
|
+
subdomain: null
|
|
43
|
+
});
|
|
44
|
+
useEffect(()=>{
|
|
45
|
+
if (!hostId) return;
|
|
46
|
+
let active = true;
|
|
47
|
+
void (async ()=>{
|
|
48
|
+
try {
|
|
49
|
+
var _index_get;
|
|
50
|
+
const index = await getDoc(doc(firestore, 'hostIndex', hostId));
|
|
51
|
+
const orgId = index.get('orgId');
|
|
52
|
+
const subdomain = (_index_get = index.get('subdomain')) != null ? _index_get : hostId;
|
|
53
|
+
if (!orgId) return;
|
|
54
|
+
const org = await getDoc(doc(firestore, 'orgs', orgId));
|
|
55
|
+
const orgSlug = org.get('slug');
|
|
56
|
+
if (!active || !orgSlug) return;
|
|
57
|
+
setResolved({
|
|
58
|
+
base: `/${orgSlug}/hosts/${subdomain}`,
|
|
59
|
+
orgSlug,
|
|
60
|
+
subdomain
|
|
61
|
+
});
|
|
62
|
+
} catch (unused) {
|
|
63
|
+
// Unreadable index or org — stay unresolved so callers degrade.
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return ()=>{
|
|
67
|
+
active = false;
|
|
68
|
+
};
|
|
69
|
+
}, [
|
|
70
|
+
firestore,
|
|
71
|
+
hostId
|
|
72
|
+
]);
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
export default useConsoleHostRoute;
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=use-console-host-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-console-host-route.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 { doc, getDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useFirestore } from './firebase/firebase-services'\n\n/**\n * Console route base for a host: `/{orgSlug}/hosts/{subdomain}` (AGL-673).\n *\n * Plugin components are handed a host DOC ID and used to build links as\n * `/{hostDocId}/…`. Console routes have been `/[orgSlug]/hosts/[subdomain]/…`\n * since AGL-621/622, so every one of those links silently began 404ing —\n * found three separate times in different plugins, each looking like a new\n * bug. This is the one resolution, so the next plugin does not become the\n * fourth.\n *\n * `null` until resolved, and `null` forever if the org has no slug. Callers\n * must render plain text rather than a link in that case: a link to nowhere\n * is worse than no link.\n *\n * Reads `hostIndex` (signed-in readable, mirrors `subdomain` and `orgId`)\n * plus the org doc for its slug — both cheap and already cached by the SDK\n * on most screens.\n */\nexport function useConsoleHostRoute(hostId: string | undefined | null): {\n base: string | null\n orgSlug: string | null\n subdomain: string | null\n} {\n const firestore = useFirestore()\n const [resolved, setResolved] = useState<{\n base: string | null\n orgSlug: string | null\n subdomain: string | null\n }>({ base: null, orgSlug: null, subdomain: null })\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void (async () => {\n try {\n const index = await getDoc(doc(firestore, 'hostIndex', hostId))\n const orgId = index.get('orgId') as string | undefined\n const subdomain = (index.get('subdomain') as string | undefined) ?? hostId\n if (!orgId) return\n const org = await getDoc(doc(firestore, 'orgs', orgId))\n const orgSlug = org.get('slug') as string | undefined\n if (!active || !orgSlug) return\n setResolved({\n base: `/${orgSlug}/hosts/${subdomain}`,\n orgSlug,\n subdomain,\n })\n } catch {\n // Unreadable index or org — stay unresolved so callers degrade.\n }\n })()\n return () => {\n active = false\n }\n }, [firestore, hostId])\n\n return resolved\n}\n\nexport default useConsoleHostRoute\n"],"names":["doc","getDoc","useEffect","useState","useFirestore","useConsoleHostRoute","hostId","firestore","resolved","setResolved","base","orgSlug","subdomain","active","index","orgId","get","org"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAChD,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,YAAY,QAAQ,kCAA8B;AAE3D;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,oBAAoBC,MAAiC;IAKnE,MAAMC,YAAYH;IAClB,MAAM,CAACI,UAAUC,YAAY,GAAGN,SAI7B;QAAEO,MAAM;QAAMC,SAAS;QAAMC,WAAW;IAAK;IAEhDV,UAAU;QACR,IAAI,CAACI,QAAQ;QACb,IAAIO,SAAS;QACb,KAAK,AAAC,CAAA;YACJ,IAAI;oBAGiBC;gBAFnB,MAAMA,QAAQ,MAAMb,OAAOD,IAAIO,WAAW,aAAaD;gBACvD,MAAMS,QAAQD,MAAME,GAAG,CAAC;gBACxB,MAAMJ,aAAaE,aAAAA,MAAME,GAAG,CAAC,wBAAVF,aAAiDR;gBACpE,IAAI,CAACS,OAAO;gBACZ,MAAME,MAAM,MAAMhB,OAAOD,IAAIO,WAAW,QAAQQ;gBAChD,MAAMJ,UAAUM,IAAID,GAAG,CAAC;gBACxB,IAAI,CAACH,UAAU,CAACF,SAAS;gBACzBF,YAAY;oBACVC,MAAM,CAAC,CAAC,EAAEC,QAAQ,OAAO,EAAEC,WAAW;oBACtCD;oBACAC;gBACF;YACF,EAAE,eAAM;YACN,gEAAgE;YAClE;QACF,CAAA;QACA,OAAO;YACLC,SAAS;QACX;IACF,GAAG;QAACN;QAAWD;KAAO;IAEtB,OAAOE;AACT;AAEA,eAAeH,oBAAmB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { DuplicableHostResourceKind } from '@aglyn/aglyn/app-utils/duplicate-resource';
|
|
18
|
+
export interface DuplicateHostResourceOptions {
|
|
19
|
+
hostId: string;
|
|
20
|
+
kind: DuplicableHostResourceKind;
|
|
21
|
+
sourceId: string;
|
|
22
|
+
/** The name the copy takes; blank means `Copy of <source>`. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Minted once per dialog, so a double click or a retry after a dropped
|
|
26
|
+
* response makes one copy and answers with it.
|
|
27
|
+
*/
|
|
28
|
+
attemptKey: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DuplicatedHostResource {
|
|
31
|
+
id: string;
|
|
32
|
+
/** The copy's first version, for the kinds that open by version. */
|
|
33
|
+
versionId: string | null;
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Asks the resources door to copy a site resource whole, as a draft
|
|
38
|
+
* (AGL-2936). The same route the create hook posts to, with
|
|
39
|
+
* `action: 'duplicate'`; the server reads the source, so nothing about it
|
|
40
|
+
* crosses the wire but its id.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useDuplicateResourceApi(): (options: DuplicateHostResourceOptions) => Promise<DuplicatedHostResource>;
|
|
43
|
+
export default useDuplicateResourceApi;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
18
|
+
import { useCallback } from "react";
|
|
19
|
+
import { useUser } from "./firebase/firebase-services.js";
|
|
20
|
+
/**
|
|
21
|
+
* Asks the resources door to copy a site resource whole, as a draft
|
|
22
|
+
* (AGL-2936). The same route the create hook posts to, with
|
|
23
|
+
* `action: 'duplicate'`; the server reads the source, so nothing about it
|
|
24
|
+
* crosses the wire but its id.
|
|
25
|
+
*/ export function useDuplicateResourceApi() {
|
|
26
|
+
const { data: user } = useUser();
|
|
27
|
+
return useCallback(async ({ hostId, kind, sourceId, name, attemptKey })=>{
|
|
28
|
+
var _ref, _result_name;
|
|
29
|
+
const response = await authorizedFetch(user, '/api/hosts/resources', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json'
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(_extends({
|
|
35
|
+
hostId,
|
|
36
|
+
resource: kind,
|
|
37
|
+
action: 'duplicate',
|
|
38
|
+
sourceId
|
|
39
|
+
}, name ? {
|
|
40
|
+
name
|
|
41
|
+
} : {}, {
|
|
42
|
+
attemptKey
|
|
43
|
+
}))
|
|
44
|
+
});
|
|
45
|
+
const result = await response.json().catch(()=>({}));
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
var _ref1;
|
|
48
|
+
throw new Error((_ref1 = result == null ? void 0 : result.error) != null ? _ref1 : 'Duplicate failed');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
id: String(result.id),
|
|
52
|
+
versionId: result.versionId ? String(result.versionId) : null,
|
|
53
|
+
name: String((_ref = (_result_name = result.name) != null ? _result_name : name) != null ? _ref : '')
|
|
54
|
+
};
|
|
55
|
+
}, [
|
|
56
|
+
user
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
export default useDuplicateResourceApi;
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=use-duplicate-resource-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-duplicate-resource-api.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 { DuplicableHostResourceKind } from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback } from 'react'\nimport { useUser } from './firebase/firebase-services'\n\nexport interface DuplicateHostResourceOptions {\n hostId: string\n kind: DuplicableHostResourceKind\n sourceId: string\n /** The name the copy takes; blank means `Copy of <source>`. */\n name?: string\n /**\n * Minted once per dialog, so a double click or a retry after a dropped\n * response makes one copy and answers with it.\n */\n attemptKey: string\n}\n\nexport interface DuplicatedHostResource {\n id: string\n /** The copy's first version, for the kinds that open by version. */\n versionId: string | null\n name: string\n}\n\n/**\n * Asks the resources door to copy a site resource whole, as a draft\n * (AGL-2936). The same route the create hook posts to, with\n * `action: 'duplicate'`; the server reads the source, so nothing about it\n * crosses the wire but its id.\n */\nexport function useDuplicateResourceApi(): (\n options: DuplicateHostResourceOptions,\n) => Promise<DuplicatedHostResource> {\n const { data: user } = useUser()\n return useCallback(\n async ({ hostId, kind, sourceId, name, attemptKey }) => {\n const response = await authorizedFetch(user, '/api/hosts/resources', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n resource: kind,\n action: 'duplicate',\n sourceId,\n ...(name ? { name } : {}),\n attemptKey,\n }),\n })\n const result = await response.json().catch(() => ({}))\n if (!response.ok) {\n throw new Error(result?.error ?? 'Duplicate failed')\n }\n return {\n id: String(result.id),\n versionId: result.versionId ? String(result.versionId) : null,\n name: String(result.name ?? name ?? ''),\n }\n },\n [user],\n )\n}\n\nexport default useDuplicateResourceApi\n"],"names":["authorizedFetch","useCallback","useUser","useDuplicateResourceApi","data","user","hostId","kind","sourceId","name","attemptKey","result","response","method","headers","body","JSON","stringify","resource","action","json","catch","ok","Error","error","id","String","versionId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,WAAW,QAAQ,QAAO;AACnC,SAASC,OAAO,QAAQ,kCAA8B;AAsBtD;;;;;CAKC,GACD,OAAO,SAASC;IAGd,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGH;IACvB,OAAOD,YACL,OAAO,EAAEK,MAAM,EAAEC,IAAI,EAAEC,QAAQ,EAAEC,IAAI,EAAEC,UAAU,EAAE;YAoBlCC,MAAAA;QAnBf,MAAMC,WAAW,MAAMZ,gBAAgBK,MAAM,wBAAwB;YACnEQ,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,MAAMC,KAAKC,SAAS,CAAC;gBACnBX;gBACAY,UAAUX;gBACVY,QAAQ;gBACRX;eACIC,OAAO;gBAAEA;YAAK,IAAI,CAAC;gBACvBC;;QAEJ;QACA,MAAMC,SAAS,MAAMC,SAASQ,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACnD,IAAI,CAACT,SAASU,EAAE,EAAE;;YAChB,MAAM,IAAIC,eAAMZ,0BAAAA,OAAQa,KAAK,oBAAI;QACnC;QACA,OAAO;YACLC,IAAIC,OAAOf,OAAOc,EAAE;YACpBE,WAAWhB,OAAOgB,SAAS,GAAGD,OAAOf,OAAOgB,SAAS,IAAI;YACzDlB,MAAMiB,QAAOf,QAAAA,eAAAA,OAAOF,IAAI,YAAXE,eAAeF,gBAAfE,OAAuB;QACtC;IACF,GACA;QAACN;KAAK;AAEV;AAEA,eAAeF,wBAAuB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type DocumentData, type FirestoreError, type Query } from 'firebase/firestore';
|
|
2
|
+
import { type DependencyList } from 'react';
|
|
3
|
+
export type FirestoreCollectionStatus = 'loading' | 'success' | 'error';
|
|
4
|
+
export interface UseFirestoreCollectionOptions {
|
|
5
|
+
idField?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Confirm against the server when a document disappears from a NON-EMPTY
|
|
8
|
+
* result, instead of trusting the live snapshot (AGL-1196).
|
|
9
|
+
*
|
|
10
|
+
* OPT-IN, and only worth setting for a query whose `where` reads a MUTABLE
|
|
11
|
+
* field. Such a document can stop matching mid-session — a scope edit, an
|
|
12
|
+
* un/republish — leave the query target, and get cached as a `noDocument`
|
|
13
|
+
* tombstone at its own path, which is then served to every other reader of
|
|
14
|
+
* that path including single-document reads (AGL-827/929).
|
|
15
|
+
*
|
|
16
|
+
* A query with no predicate, or one keyed on `documentId()`, cannot produce
|
|
17
|
+
* this and must not pay for the extra read. Where the predicate can be
|
|
18
|
+
* dropped entirely, drop it — that removes the mechanism rather than
|
|
19
|
+
* repairing it after the fact (AGL-1190).
|
|
20
|
+
*/
|
|
21
|
+
confirmDisappearances?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface UseFirestoreCollectionResult<T> {
|
|
24
|
+
data: T[];
|
|
25
|
+
status: FirestoreCollectionStatus;
|
|
26
|
+
error: FirestoreError | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* `data` came from the local cache and the server has not confirmed it
|
|
29
|
+
* (AGL-1066). See `ObservableStatus.fromCache` for why this matters and
|
|
30
|
+
* why it is the signal to reach for rather than `staleSession`.
|
|
31
|
+
*/
|
|
32
|
+
fromCache: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The server has REFUSED this listen for longer than the retry budget, and
|
|
35
|
+
* no server snapshot has arrived since (AGL-1066).
|
|
36
|
+
*
|
|
37
|
+
* Kept after the flip rather than folded into `status`, because it says
|
|
38
|
+
* something `status` does not: WHY the listen is failing. `status:
|
|
39
|
+
* 'error'` covers every terminal error; this one counts `permission-denied`
|
|
40
|
+
* alone, so it cannot fire offline, and a single server snapshot clears it.
|
|
41
|
+
* A consumer asking "is what I am rendering cache-only because the session
|
|
42
|
+
* is refused" wants this; one asking "did the read fail at all" wants
|
|
43
|
+
* `status`.
|
|
44
|
+
*/
|
|
45
|
+
serverDenied: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Collection-query counterpart to reactfire's `useFirestoreCollectionData`,
|
|
49
|
+
* but backed by a raw `onSnapshot` listener with its own retry/backoff
|
|
50
|
+
* instead of reactfire's cached Observable. That cache terminates forever on
|
|
51
|
+
* its first error — a transient `permission-denied` right after sign-in
|
|
52
|
+
* (before Firestore's own credential provider has attached the user's ID
|
|
53
|
+
* token) permanently breaks the query for the rest of the session, even
|
|
54
|
+
* across remounts. A fresh `onSnapshot` call each retry reopens a genuinely
|
|
55
|
+
* new listener, so it recovers once the token has propagated (AGL-216/217).
|
|
56
|
+
*
|
|
57
|
+
* `buildQuery` is re-invoked from `deps` the same way a `useEffect`/`useMemo`
|
|
58
|
+
* callback would be — pass the same primitive values (ids, uid, firestore)
|
|
59
|
+
* you'd put in a `useMemo` dependency array, not the `Query` object itself.
|
|
60
|
+
*/
|
|
61
|
+
export declare function useFirestoreCollection<T = DocumentData>(buildQuery: () => Query<DocumentData> | null | undefined, deps: DependencyList, options?: UseFirestoreCollectionOptions): UseFirestoreCollectionResult<T>;
|
|
62
|
+
export default useFirestoreCollection;
|
|
@@ -0,0 +1,307 @@
|
|
|
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 { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { getDocsFromServer, onSnapshot } from "firebase/firestore";
|
|
19
|
+
import { useEffect, useRef, useState } from "react";
|
|
20
|
+
import { CONFIRMABLE_LISTEN_OPTIONS } from "./helpers/listen-options.js";
|
|
21
|
+
import { DENIAL_STREAK_TO_REPORT, denialLabelForQuery, reportFirestoreDenial, reportFirestoreServerRead, scheduleRefusedReopen, subscribeFirestoreSessionHeal } from "./firestore-denial-reporter.js";
|
|
22
|
+
const RETRY_DELAY_MS = 400;
|
|
23
|
+
const MAX_RETRIES = 5;
|
|
24
|
+
/**
|
|
25
|
+
* Collection-query counterpart to reactfire's `useFirestoreCollectionData`,
|
|
26
|
+
* but backed by a raw `onSnapshot` listener with its own retry/backoff
|
|
27
|
+
* instead of reactfire's cached Observable. That cache terminates forever on
|
|
28
|
+
* its first error — a transient `permission-denied` right after sign-in
|
|
29
|
+
* (before Firestore's own credential provider has attached the user's ID
|
|
30
|
+
* token) permanently breaks the query for the rest of the session, even
|
|
31
|
+
* across remounts. A fresh `onSnapshot` call each retry reopens a genuinely
|
|
32
|
+
* new listener, so it recovers once the token has propagated (AGL-216/217).
|
|
33
|
+
*
|
|
34
|
+
* `buildQuery` is re-invoked from `deps` the same way a `useEffect`/`useMemo`
|
|
35
|
+
* callback would be — pass the same primitive values (ids, uid, firestore)
|
|
36
|
+
* you'd put in a `useMemo` dependency array, not the `Query` object itself.
|
|
37
|
+
*/ export function useFirestoreCollection(buildQuery, deps, options = {}) {
|
|
38
|
+
const [data, setData] = useState([]);
|
|
39
|
+
const [status, setStatus] = useState('loading');
|
|
40
|
+
const [error, setError] = useState(undefined);
|
|
41
|
+
// Un-confirmed until a snapshot says otherwise: a hook that has not heard
|
|
42
|
+
// from the server yet must not read as server-confirmed.
|
|
43
|
+
const [fromCache, setFromCache] = useState(true);
|
|
44
|
+
const [serverDenied, setServerDenied] = useState(false);
|
|
45
|
+
const buildQueryRef = useRef(buildQuery);
|
|
46
|
+
buildQueryRef.current = buildQuery;
|
|
47
|
+
const idField = options.idField;
|
|
48
|
+
const confirmDisappearances = options.confirmDisappearances;
|
|
49
|
+
useEffect(()=>{
|
|
50
|
+
setStatus('loading');
|
|
51
|
+
setError(undefined);
|
|
52
|
+
// Clear on EVERY dep change, not only when the query goes null: when
|
|
53
|
+
// the scope moves between two live queries (org A → org B, host A →
|
|
54
|
+
// host B) the old rows would otherwise stay rendered until the new
|
|
55
|
+
// snapshot arrives — the org-switch "remnants" bug (AGL-591). Same
|
|
56
|
+
// hold-nothing-rather-than-show-the-wrong-org rule as useOrgHosts.
|
|
57
|
+
setData([]);
|
|
58
|
+
setFromCache(true);
|
|
59
|
+
setServerDenied(false);
|
|
60
|
+
const q = buildQueryRef.current();
|
|
61
|
+
if (!q) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let cancelled = false;
|
|
65
|
+
let unsubscribe = null;
|
|
66
|
+
let timer = null;
|
|
67
|
+
/** Cancels a pending reopen on the refused cadence (AGL-2944). */ let cancelRefusedReopen = null;
|
|
68
|
+
let attempt = 0;
|
|
69
|
+
/**
|
|
70
|
+
* Refusals since the last SERVER snapshot (AGL-1066).
|
|
71
|
+
*
|
|
72
|
+
* Deliberately not `attempt`: that one is reset by any snapshot,
|
|
73
|
+
* including a cached one, so under `persistentLocalCache` it never
|
|
74
|
+
* reaches its budget and can never report anything.
|
|
75
|
+
*/ let deniedStreak = 0;
|
|
76
|
+
/** Epoch ms of the current streak's first refusal (AGL-1440). */ let deniedStreakStartedAt = 0;
|
|
77
|
+
let denialReported = false;
|
|
78
|
+
/**
|
|
79
|
+
* The retry budget is spent and the last thing the server did was refuse
|
|
80
|
+
* (AGL-1066).
|
|
81
|
+
*
|
|
82
|
+
* This is what makes `status: 'error'` STICK. Every reopened listen emits
|
|
83
|
+
* from the cache before it learns it is still refused, and an
|
|
84
|
+
* unconditional `setStatus('success')` in the success handler would hand
|
|
85
|
+
* the page back to `success` on that emission — so the surface would
|
|
86
|
+
* oscillate error → success → error at the retry cadence, which is worse
|
|
87
|
+
* to look at than either state. Only a snapshot the SERVER answered
|
|
88
|
+
* clears this, for the same reason it is the only thing that clears
|
|
89
|
+
* `deniedStreak`: a cached emission is not evidence about the server.
|
|
90
|
+
*/ let terminal = false;
|
|
91
|
+
const denialLabel = denialLabelForQuery(q);
|
|
92
|
+
// Disappearance tracking (AGL-1196); inert unless opted in.
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const confirmedGone = new Set();
|
|
95
|
+
const emit = (docs, cached)=>{
|
|
96
|
+
// A server-answered emission is the one thing that ends the refusal,
|
|
97
|
+
// including the `getDocsFromServer` confirmation path below.
|
|
98
|
+
if (!cached) terminal = false;
|
|
99
|
+
if (!terminal) {
|
|
100
|
+
setStatus('success');
|
|
101
|
+
setError(undefined);
|
|
102
|
+
}
|
|
103
|
+
setFromCache(cached);
|
|
104
|
+
setData(docs.map((docSnap)=>{
|
|
105
|
+
const value = _extends({}, docSnap.data());
|
|
106
|
+
if (idField) value[idField] = docSnap.id;
|
|
107
|
+
return value;
|
|
108
|
+
}));
|
|
109
|
+
};
|
|
110
|
+
const subscribe = ()=>{
|
|
111
|
+
unsubscribe = onSnapshot(q, // Without this the cache→server confirmation is never delivered and
|
|
112
|
+
// `fromCache` latches true for the life of the listener, refusing
|
|
113
|
+
// every guarded save on the page (AGL-2486). See `listen-options.ts`.
|
|
114
|
+
CONFIRMABLE_LISTEN_OPTIONS, (snapshot)=>{
|
|
115
|
+
if (cancelled) return;
|
|
116
|
+
/**
|
|
117
|
+
* Only a SERVER-answered snapshot refunds the retry budget — the
|
|
118
|
+
* AGL-1066 flip, and the reason it took four commits to become
|
|
119
|
+
* safe.
|
|
120
|
+
*
|
|
121
|
+
* Under `persistentLocalCache` a reopened listen emits from the
|
|
122
|
+
* cache BEFORE it learns the server refused it. Refunding on that
|
|
123
|
+
* emission handed the budget back every cycle, so `attempt` never
|
|
124
|
+
* reached `MAX_RETRIES`, `status` could never become `'error'`, and
|
|
125
|
+
* a dead session looked exactly like a healthy one on every
|
|
126
|
+
* listener-backed page for as long as it stayed dead.
|
|
127
|
+
*
|
|
128
|
+
* Landing this alone would have been a regression, and the three
|
|
129
|
+
* things that made it one are now in place:
|
|
130
|
+
*
|
|
131
|
+
* - the budget is 5 x 400ms, and the error branch used to stop
|
|
132
|
+
* retrying for good, while the heal is an AGL-664 in-place
|
|
133
|
+
* re-auth that takes as long as a human takes to type a
|
|
134
|
+
* password. A resolved re-auth now broadcasts and this listener
|
|
135
|
+
* reopens on it (`subscribeFirestoreSessionHeal` below), and a
|
|
136
|
+
* refusal streak keeps a slow road back for a recovery nobody
|
|
137
|
+
* announced (see the error branch);
|
|
138
|
+
* - `status: 'error'` had to become STICKY, or the cached emission
|
|
139
|
+
* that precedes each refusal would flip it straight back — see
|
|
140
|
+
* `terminal` above;
|
|
141
|
+
* - the surfaces that BLANK on `error` had to learn the
|
|
142
|
+
* difference between "never had data" and "have data, the server
|
|
143
|
+
* refused". They read `hasEmitted` now, so the besigner canvas
|
|
144
|
+
* and the host setup Theme tab keep rendering across a refusal
|
|
145
|
+
* instead of collapsing to "Not found".
|
|
146
|
+
*
|
|
147
|
+
* The withholding is keyed on a REFUSAL having happened, not on the
|
|
148
|
+
* emission being cached, and that is load-bearing: offline stays
|
|
149
|
+
* exactly as inert as it was. A lost network yields `unavailable`
|
|
150
|
+
* (when it yields an error callback at all), which never enters
|
|
151
|
+
* `deniedStreak`, so a cached emission still refunds the budget and
|
|
152
|
+
* an offline listener still never goes terminal. The cache exists
|
|
153
|
+
* for offline; this must not change what happens there.
|
|
154
|
+
*/ // Only the SERVER answering is evidence the session can read.
|
|
155
|
+
if (!snapshot.metadata.fromCache) {
|
|
156
|
+
attempt = 0;
|
|
157
|
+
deniedStreak = 0;
|
|
158
|
+
denialReported = false;
|
|
159
|
+
setServerDenied(false);
|
|
160
|
+
// Proof the session can read — stronger than any accumulated
|
|
161
|
+
// denial, and what stops a scoped collaborator's by-design
|
|
162
|
+
// denials from ever adding up to a re-auth prompt.
|
|
163
|
+
reportFirestoreServerRead();
|
|
164
|
+
} else if (deniedStreak === 0) {
|
|
165
|
+
// Cached, but nothing has been refused since the server last
|
|
166
|
+
// answered — so this is not the AGL-1066 fault and the budget is
|
|
167
|
+
// refunded exactly as it always was.
|
|
168
|
+
attempt = 0;
|
|
169
|
+
}
|
|
170
|
+
if (confirmDisappearances) {
|
|
171
|
+
const present = new Set(snapshot.docs.map((d)=>d.id));
|
|
172
|
+
const vanished = [
|
|
173
|
+
...seen
|
|
174
|
+
].filter((id)=>!present.has(id) && !confirmedGone.has(id));
|
|
175
|
+
if (vanished.length) {
|
|
176
|
+
// A fresh (non-resumed) read ignores the resume token, returns
|
|
177
|
+
// the true set and rewrites the cache, dropping any tombstone.
|
|
178
|
+
getDocsFromServer(q).then((fresh)=>{
|
|
179
|
+
if (cancelled) return;
|
|
180
|
+
const freshIds = new Set(fresh.docs.map((d)=>d.id));
|
|
181
|
+
// Only an absence the SERVER agrees with is real. One that
|
|
182
|
+
// came back was a tombstone and must stay eligible to heal
|
|
183
|
+
// if it vanishes again.
|
|
184
|
+
vanished.forEach((id)=>{
|
|
185
|
+
if (!freshIds.has(id)) confirmedGone.add(id);
|
|
186
|
+
});
|
|
187
|
+
freshIds.forEach((id)=>seen.add(id));
|
|
188
|
+
// `getDocsFromServer` is server-confirmed by definition.
|
|
189
|
+
emit(fresh.docs, false);
|
|
190
|
+
}).catch(()=>{
|
|
191
|
+
// Couldn't confirm: show the live result rather than an
|
|
192
|
+
// error. A possibly-short list beats an empty one.
|
|
193
|
+
if (!cancelled) emit(snapshot.docs, snapshot.metadata.fromCache);
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
snapshot.docs.forEach((d)=>seen.add(d.id));
|
|
198
|
+
}
|
|
199
|
+
emit(snapshot.docs, snapshot.metadata.fromCache);
|
|
200
|
+
}, (err)=>{
|
|
201
|
+
if (cancelled) return;
|
|
202
|
+
unsubscribe == null ? void 0 : unsubscribe();
|
|
203
|
+
// Say so once per outage, and only for a refusal — a lost network
|
|
204
|
+
// produces no error callback at all, so this cannot fire offline.
|
|
205
|
+
if ((err == null ? void 0 : err.code) === 'permission-denied') {
|
|
206
|
+
deniedStreak += 1;
|
|
207
|
+
// When the streak began — the reference point that lets
|
|
208
|
+
// `refusedRetryDelayMs` ask whether the session has read from
|
|
209
|
+
// the server SINCE this listen started being refused (AGL-1440).
|
|
210
|
+
if (deniedStreak === 1) deniedStreakStartedAt = Date.now();
|
|
211
|
+
if (deniedStreak >= DENIAL_STREAK_TO_REPORT) {
|
|
212
|
+
setServerDenied(true);
|
|
213
|
+
if (!denialReported) {
|
|
214
|
+
denialReported = true;
|
|
215
|
+
reportFirestoreDenial(denialLabel);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (attempt < MAX_RETRIES) {
|
|
220
|
+
attempt += 1;
|
|
221
|
+
if (deniedStreak > MAX_RETRIES) {
|
|
222
|
+
cancelRefusedReopen = scheduleRefusedReopen(subscribe, deniedStreakStartedAt);
|
|
223
|
+
} else {
|
|
224
|
+
timer = setTimeout(subscribe, RETRY_DELAY_MS);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
terminal = true;
|
|
228
|
+
setStatus('error');
|
|
229
|
+
setError(err);
|
|
230
|
+
/**
|
|
231
|
+
* A refusal streak keeps a slow road back (AGL-1066).
|
|
232
|
+
*
|
|
233
|
+
* Before the flip this listener reopened forever, which is also
|
|
234
|
+
* what HEALED it — a token that attached late, an App Check
|
|
235
|
+
* hiccup, one of the AGL-1143 transient token-layer denials.
|
|
236
|
+
* None of those broadcast a heal, because none of them involved
|
|
237
|
+
* a re-auth the console asked for, so stopping here would leave
|
|
238
|
+
* every listen in the console terminal until the user reloaded.
|
|
239
|
+
* The status is corrected; the recovery is not taken away.
|
|
240
|
+
*
|
|
241
|
+
* Any OTHER terminal error still stops exactly as before — this
|
|
242
|
+
* is deliberately keyed on the refusal streak, not on `attempt`.
|
|
243
|
+
* How slow the road is depends on whether the refusal looks like
|
|
244
|
+
* the session or the ref (AGL-1440) and on how long it has
|
|
245
|
+
* lasted (AGL-2945), and a hidden tab waits to be looked at
|
|
246
|
+
* (AGL-2944) — see `scheduleRefusedReopen`.
|
|
247
|
+
*/ if (deniedStreak > MAX_RETRIES) {
|
|
248
|
+
cancelRefusedReopen = scheduleRefusedReopen(subscribe, deniedStreakStartedAt);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
subscribe();
|
|
254
|
+
/**
|
|
255
|
+
* The session came back — reopen NOW rather than on the next tick of a
|
|
256
|
+
* cadence tuned for a session that has not (AGL-1066).
|
|
257
|
+
*
|
|
258
|
+
* Gated on this listener actually being refused. A healthy listener has
|
|
259
|
+
* an open subscription and nothing to recover, so it must ignore the
|
|
260
|
+
* broadcast entirely: without this gate a heal would tear down and
|
|
261
|
+
* reopen every listen in the console at once, which is a listener storm
|
|
262
|
+
* dressed up as a fix.
|
|
263
|
+
*
|
|
264
|
+
* What is refunded is `attempt` — the retry BUDGET, which was spent on a
|
|
265
|
+
* fault that is now over. `deniedStreak` is deliberately left alone: it
|
|
266
|
+
* is evidence about the server, and only the server answering may clear
|
|
267
|
+
* it. So `serverDenied` stays true across the heal and goes false on the
|
|
268
|
+
* snapshot that actually proves the read works, and a heal that turns
|
|
269
|
+
* out to be wishful thinking falls straight back to the slow cadence
|
|
270
|
+
* instead of resuming a 400ms storm.
|
|
271
|
+
*
|
|
272
|
+
* `denialReported` IS reset: the outage ended here. If reads are still
|
|
273
|
+
* refused afterwards that is a new one, and it has to be able to raise
|
|
274
|
+
* the AGL-1063 banner again.
|
|
275
|
+
*/ const unsubscribeHeal = subscribeFirestoreSessionHeal(()=>{
|
|
276
|
+
if (cancelled || deniedStreak === 0) return;
|
|
277
|
+
if (timer) {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
timer = null;
|
|
280
|
+
}
|
|
281
|
+
cancelRefusedReopen == null ? void 0 : cancelRefusedReopen();
|
|
282
|
+
cancelRefusedReopen = null;
|
|
283
|
+
unsubscribe == null ? void 0 : unsubscribe();
|
|
284
|
+
attempt = 0;
|
|
285
|
+
denialReported = false;
|
|
286
|
+
subscribe();
|
|
287
|
+
});
|
|
288
|
+
return ()=>{
|
|
289
|
+
cancelled = true;
|
|
290
|
+
unsubscribeHeal();
|
|
291
|
+
if (timer) clearTimeout(timer);
|
|
292
|
+
cancelRefusedReopen == null ? void 0 : cancelRefusedReopen();
|
|
293
|
+
unsubscribe == null ? void 0 : unsubscribe();
|
|
294
|
+
};
|
|
295
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
296
|
+
}, deps);
|
|
297
|
+
return {
|
|
298
|
+
data,
|
|
299
|
+
status,
|
|
300
|
+
error,
|
|
301
|
+
fromCache,
|
|
302
|
+
serverDenied
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
export default useFirestoreCollection;
|
|
306
|
+
|
|
307
|
+
//# sourceMappingURL=use-firestore-collection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-firestore-collection.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 {\n getDocsFromServer,\n onSnapshot,\n type DocumentData,\n type FirestoreError,\n type Query,\n type QueryDocumentSnapshot,\n} from 'firebase/firestore'\nimport { useEffect, useRef, useState, type DependencyList } from 'react'\nimport { CONFIRMABLE_LISTEN_OPTIONS } from './helpers/listen-options'\nimport {\n DENIAL_STREAK_TO_REPORT,\n denialLabelForQuery,\n reportFirestoreDenial,\n reportFirestoreServerRead,\n scheduleRefusedReopen,\n subscribeFirestoreSessionHeal,\n} from './firestore-denial-reporter'\n\nconst RETRY_DELAY_MS = 400\nconst MAX_RETRIES = 5\n\n/*\n * The cadence once a refusal streak has outlived the retry budget lives in\n * `firestore-denial-reporter.ts` (`scheduleRefusedReopen`), shared by all\n * three listener hooks. It starts at 2s while the fault could be the whole\n * session and at 60s once another listener's server answer proves the\n * session reads and this refusal is about the ref (AGL-1440), then grows\n * with the streak's age to a five-minute ceiling (AGL-2945). A hidden tab\n * sets no timer (AGL-2944). The tab becoming visible, the window regaining\n * focus and a heal broadcast (`subscribeFirestoreSessionHeal`) all reopen\n * at once.\n */\n\nexport type FirestoreCollectionStatus = 'loading' | 'success' | 'error'\n\nexport interface UseFirestoreCollectionOptions {\n idField?: string\n /**\n * Confirm against the server when a document disappears from a NON-EMPTY\n * result, instead of trusting the live snapshot (AGL-1196).\n *\n * OPT-IN, and only worth setting for a query whose `where` reads a MUTABLE\n * field. Such a document can stop matching mid-session — a scope edit, an\n * un/republish — leave the query target, and get cached as a `noDocument`\n * tombstone at its own path, which is then served to every other reader of\n * that path including single-document reads (AGL-827/929).\n *\n * A query with no predicate, or one keyed on `documentId()`, cannot produce\n * this and must not pay for the extra read. Where the predicate can be\n * dropped entirely, drop it — that removes the mechanism rather than\n * repairing it after the fact (AGL-1190).\n */\n confirmDisappearances?: boolean\n}\n\nexport interface UseFirestoreCollectionResult<T> {\n data: T[]\n status: FirestoreCollectionStatus\n error: FirestoreError | undefined\n /**\n * `data` came from the local cache and the server has not confirmed it\n * (AGL-1066). See `ObservableStatus.fromCache` for why this matters and\n * why it is the signal to reach for rather than `staleSession`.\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 * Kept after the flip rather than folded into `status`, because it says\n * something `status` does not: WHY the listen is failing. `status:\n * 'error'` covers every terminal error; this one counts `permission-denied`\n * alone, so it cannot fire offline, and a single server snapshot clears it.\n * A consumer asking \"is what I am rendering cache-only because the session\n * is refused\" wants this; one asking \"did the read fail at all\" wants\n * `status`.\n */\n serverDenied: boolean\n}\n\n/**\n * Collection-query counterpart to reactfire's `useFirestoreCollectionData`,\n * but backed by a raw `onSnapshot` listener with its own retry/backoff\n * instead of reactfire's cached Observable. That cache terminates forever on\n * its first error — a transient `permission-denied` right after sign-in\n * (before Firestore's own credential provider has attached the user's ID\n * token) permanently breaks the query for the rest of the session, even\n * across remounts. A fresh `onSnapshot` call each retry reopens a genuinely\n * new listener, so it recovers once the token has propagated (AGL-216/217).\n *\n * `buildQuery` is re-invoked from `deps` the same way a `useEffect`/`useMemo`\n * callback would be — pass the same primitive values (ids, uid, firestore)\n * you'd put in a `useMemo` dependency array, not the `Query` object itself.\n */\nexport function useFirestoreCollection<T = DocumentData>(\n buildQuery: () => Query<DocumentData> | null | undefined,\n deps: DependencyList,\n options: UseFirestoreCollectionOptions = {},\n): UseFirestoreCollectionResult<T> {\n const [data, setData] = useState<T[]>([])\n const [status, setStatus] = useState<FirestoreCollectionStatus>('loading')\n const [error, setError] = useState<FirestoreError | undefined>(undefined)\n // Un-confirmed until a snapshot says otherwise: a hook that has not heard\n // from the server yet must not read as server-confirmed.\n const [fromCache, setFromCache] = useState(true)\n const [serverDenied, setServerDenied] = useState(false)\n const buildQueryRef = useRef(buildQuery)\n buildQueryRef.current = buildQuery\n const idField = options.idField\n const confirmDisappearances = options.confirmDisappearances\n\n useEffect(() => {\n setStatus('loading')\n setError(undefined)\n // Clear on EVERY dep change, not only when the query goes null: when\n // the scope moves between two live queries (org A → org B, host A →\n // host B) the old rows would otherwise stay rendered until the new\n // snapshot arrives — the org-switch \"remnants\" bug (AGL-591). Same\n // hold-nothing-rather-than-show-the-wrong-org rule as useOrgHosts.\n setData([])\n setFromCache(true)\n setServerDenied(false)\n\n const q = buildQueryRef.current()\n if (!q) {\n return\n }\n\n let cancelled = false\n let unsubscribe: (() => void) | null = null\n let timer: ReturnType<typeof setTimeout> | null = null\n /** Cancels a pending reopen on the refused cadence (AGL-2944). */\n let cancelRefusedReopen: (() => void) | null = null\n let attempt = 0\n /**\n * Refusals since the last SERVER snapshot (AGL-1066).\n *\n * Deliberately not `attempt`: that one is reset by any snapshot,\n * including a cached one, so under `persistentLocalCache` it never\n * reaches its budget and can never report anything.\n */\n let deniedStreak = 0\n /** Epoch ms of the current streak's first refusal (AGL-1440). */\n let deniedStreakStartedAt = 0\n let denialReported = false\n /**\n * The retry budget is spent and the last thing the server did was refuse\n * (AGL-1066).\n *\n * This is what makes `status: 'error'` STICK. Every reopened listen emits\n * from the cache before it learns it is still refused, and an\n * unconditional `setStatus('success')` in the success handler would hand\n * the page back to `success` on that emission — so the surface would\n * oscillate error → success → error at the retry cadence, which is worse\n * to look at than either state. Only a snapshot the SERVER answered\n * clears this, for the same reason it is the only thing that clears\n * `deniedStreak`: a cached emission is not evidence about the server.\n */\n let terminal = false\n const denialLabel = denialLabelForQuery(q)\n // Disappearance tracking (AGL-1196); inert unless opted in.\n const seen = new Set<string>()\n const confirmedGone = new Set<string>()\n\n const emit = (\n docs: QueryDocumentSnapshot<DocumentData>[],\n cached: boolean,\n ) => {\n // A server-answered emission is the one thing that ends the refusal,\n // including the `getDocsFromServer` confirmation path below.\n if (!cached) terminal = false\n if (!terminal) {\n setStatus('success')\n setError(undefined)\n }\n setFromCache(cached)\n setData(\n docs.map((docSnap) => {\n const value = { ...docSnap.data() } as Record<string, unknown>\n if (idField) value[idField] = docSnap.id\n return value as T\n }),\n )\n }\n\n const subscribe = () => {\n unsubscribe = onSnapshot(\n q,\n // Without this the cache→server confirmation is never delivered and\n // `fromCache` latches true for the life of the listener, refusing\n // every guarded save on the page (AGL-2486). See `listen-options.ts`.\n CONFIRMABLE_LISTEN_OPTIONS,\n (snapshot) => {\n if (cancelled) return\n /**\n * Only a SERVER-answered snapshot refunds the retry budget — the\n * AGL-1066 flip, and the reason it took four commits to become\n * safe.\n *\n * Under `persistentLocalCache` a reopened listen emits from the\n * cache BEFORE it learns the server refused it. Refunding on that\n * emission handed the budget back every cycle, so `attempt` never\n * reached `MAX_RETRIES`, `status` could never become `'error'`, and\n * a dead session looked exactly like a healthy one on every\n * listener-backed page for as long as it stayed dead.\n *\n * Landing this alone would have been a regression, and the three\n * things that made it one are now in place:\n *\n * - the budget is 5 x 400ms, and the error branch used to stop\n * retrying for good, while the heal is an AGL-664 in-place\n * re-auth that takes as long as a human takes to type a\n * password. A resolved re-auth now broadcasts and this listener\n * reopens on it (`subscribeFirestoreSessionHeal` below), and a\n * refusal streak keeps a slow road back for a recovery nobody\n * announced (see the error branch);\n * - `status: 'error'` had to become STICKY, or the cached emission\n * that precedes each refusal would flip it straight back — see\n * `terminal` above;\n * - the surfaces that BLANK on `error` had to learn the\n * difference between \"never had data\" and \"have data, the server\n * refused\". They read `hasEmitted` now, so the besigner canvas\n * and the host setup Theme tab keep rendering across a refusal\n * instead of collapsing to \"Not found\".\n *\n * The withholding is keyed on a REFUSAL having happened, not on the\n * emission being cached, and that is load-bearing: offline stays\n * exactly as inert as it was. A lost network yields `unavailable`\n * (when it yields an error callback at all), which never enters\n * `deniedStreak`, so a cached emission still refunds the budget and\n * an offline listener still never goes terminal. The cache exists\n * for offline; this must not change what happens there.\n */\n // Only the SERVER answering is evidence the session can read.\n if (!snapshot.metadata.fromCache) {\n attempt = 0\n deniedStreak = 0\n denialReported = false\n setServerDenied(false)\n // Proof the session can read — stronger than any accumulated\n // denial, and what stops a scoped collaborator's by-design\n // denials from ever adding up to a re-auth prompt.\n reportFirestoreServerRead()\n } else if (deniedStreak === 0) {\n // Cached, but nothing has been refused since the server last\n // answered — so this is not the AGL-1066 fault and the budget is\n // refunded exactly as it always was.\n attempt = 0\n }\n\n if (confirmDisappearances) {\n const present = new Set(snapshot.docs.map((d) => d.id))\n const vanished = [...seen].filter(\n (id) => !present.has(id) && !confirmedGone.has(id),\n )\n if (vanished.length) {\n // A fresh (non-resumed) read ignores the resume token, returns\n // the true set and rewrites the cache, dropping any tombstone.\n getDocsFromServer(q)\n .then((fresh) => {\n if (cancelled) return\n const freshIds = new Set(fresh.docs.map((d) => d.id))\n // Only an absence the SERVER agrees with is real. One that\n // came back was a tombstone and must stay eligible to heal\n // if it vanishes again.\n vanished.forEach((id) => {\n if (!freshIds.has(id)) confirmedGone.add(id)\n })\n freshIds.forEach((id) => seen.add(id))\n // `getDocsFromServer` is server-confirmed by definition.\n emit(fresh.docs, false)\n })\n .catch(() => {\n // Couldn't confirm: show the live result rather than an\n // error. A possibly-short list beats an empty one.\n if (!cancelled) emit(snapshot.docs, snapshot.metadata.fromCache)\n })\n return\n }\n snapshot.docs.forEach((d) => seen.add(d.id))\n }\n\n emit(snapshot.docs, snapshot.metadata.fromCache)\n },\n (err) => {\n if (cancelled) return\n unsubscribe?.()\n // Say so once per outage, and only for a refusal — a lost network\n // produces no error callback at all, so this cannot fire offline.\n if (err?.code === 'permission-denied') {\n deniedStreak += 1\n // When the streak began — the reference point that lets\n // `refusedRetryDelayMs` ask whether the session has read from\n // the server SINCE this listen started being refused (AGL-1440).\n if (deniedStreak === 1) deniedStreakStartedAt = Date.now()\n if (deniedStreak >= DENIAL_STREAK_TO_REPORT) {\n setServerDenied(true)\n if (!denialReported) {\n denialReported = true\n reportFirestoreDenial(denialLabel)\n }\n }\n }\n if (attempt < MAX_RETRIES) {\n attempt += 1\n if (deniedStreak > MAX_RETRIES) {\n cancelRefusedReopen = scheduleRefusedReopen(\n subscribe,\n deniedStreakStartedAt,\n )\n } else {\n timer = setTimeout(subscribe, RETRY_DELAY_MS)\n }\n } else {\n terminal = true\n setStatus('error')\n setError(err)\n /**\n * A refusal streak keeps a slow road back (AGL-1066).\n *\n * Before the flip this listener reopened forever, which is also\n * what HEALED it — a token that attached late, an App Check\n * hiccup, one of the AGL-1143 transient token-layer denials.\n * None of those broadcast a heal, because none of them involved\n * a re-auth the console asked for, so stopping here would leave\n * every listen in the console terminal until the user reloaded.\n * The status is corrected; the recovery is not taken away.\n *\n * Any OTHER terminal error still stops exactly as before — this\n * is deliberately keyed on the refusal streak, not on `attempt`.\n * How slow the road is depends on whether the refusal looks like\n * the session or the ref (AGL-1440) and on how long it has\n * lasted (AGL-2945), and a hidden tab waits to be looked at\n * (AGL-2944) — see `scheduleRefusedReopen`.\n */\n if (deniedStreak > MAX_RETRIES) {\n cancelRefusedReopen = scheduleRefusedReopen(\n subscribe,\n deniedStreakStartedAt,\n )\n }\n }\n },\n )\n }\n subscribe()\n\n /**\n * The session came back — reopen NOW rather than on the next tick of a\n * cadence tuned for a session that has not (AGL-1066).\n *\n * Gated on this listener actually being refused. A healthy listener has\n * an open subscription and nothing to recover, so it must ignore the\n * broadcast entirely: without this gate a heal would tear down and\n * reopen every listen in the console at once, which is a listener storm\n * dressed up as a fix.\n *\n * What is refunded is `attempt` — the retry BUDGET, which was spent on a\n * fault that is now over. `deniedStreak` is deliberately left alone: it\n * is evidence about the server, and only the server answering may clear\n * it. So `serverDenied` stays true across the heal and goes false on the\n * snapshot that actually proves the read works, and a heal that turns\n * out to be wishful thinking falls straight back to the slow cadence\n * instead of resuming a 400ms storm.\n *\n * `denialReported` IS reset: the outage ended here. If reads are still\n * refused afterwards that is a new one, and it has to be able to raise\n * the AGL-1063 banner again.\n */\n const unsubscribeHeal = subscribeFirestoreSessionHeal(() => {\n if (cancelled || deniedStreak === 0) return\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n cancelRefusedReopen?.()\n cancelRefusedReopen = null\n unsubscribe?.()\n attempt = 0\n denialReported = false\n subscribe()\n })\n\n return () => {\n cancelled = true\n unsubscribeHeal()\n if (timer) clearTimeout(timer)\n cancelRefusedReopen?.()\n unsubscribe?.()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps)\n\n return { data, status, error, fromCache, serverDenied }\n}\n\nexport default useFirestoreCollection\n"],"names":["getDocsFromServer","onSnapshot","useEffect","useRef","useState","CONFIRMABLE_LISTEN_OPTIONS","DENIAL_STREAK_TO_REPORT","denialLabelForQuery","reportFirestoreDenial","reportFirestoreServerRead","scheduleRefusedReopen","subscribeFirestoreSessionHeal","RETRY_DELAY_MS","MAX_RETRIES","useFirestoreCollection","buildQuery","deps","options","data","setData","status","setStatus","error","setError","undefined","fromCache","setFromCache","serverDenied","setServerDenied","buildQueryRef","current","idField","confirmDisappearances","q","cancelled","unsubscribe","timer","cancelRefusedReopen","attempt","deniedStreak","deniedStreakStartedAt","denialReported","terminal","denialLabel","seen","Set","confirmedGone","emit","docs","cached","map","docSnap","value","id","subscribe","snapshot","metadata","present","d","vanished","filter","has","length","then","fresh","freshIds","forEach","add","catch","err","code","Date","now","setTimeout","unsubscribeHeal","clearTimeout"],"mappings":"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"}
|