@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.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/jest.config.d.ts +36 -0
  4. package/package.json +45 -0
  5. package/src/index.d.ts +17 -0
  6. package/src/index.js +18 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
  9. package/src/lib/components/duplicate-resource-dialog.js +217 -0
  10. package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
  11. package/src/lib/constants/firebase-config.d.ts +68 -0
  12. package/src/lib/constants/firebase-config.js +155 -0
  13. package/src/lib/constants/firebase-config.js.map +1 -0
  14. package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
  15. package/src/lib/constants/firebase-emulator-hosts.js +81 -0
  16. package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
  17. package/src/lib/constants/mock-data.d.ts +24 -0
  18. package/src/lib/constants/mock-data.js +49 -0
  19. package/src/lib/constants/mock-data.js.map +1 -0
  20. package/src/lib/constants/site-paths.d.ts +47 -0
  21. package/src/lib/constants/site-paths.js +66 -0
  22. package/src/lib/constants/site-paths.js.map +1 -0
  23. package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
  24. package/src/lib/hooks/firebase/auth-persistence.js +117 -0
  25. package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
  26. package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
  27. package/src/lib/hooks/firebase/firebase-services.js +522 -0
  28. package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
  29. package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
  30. package/src/lib/hooks/firebase/firestore-cache.js +152 -0
  31. package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
  32. package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
  33. package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
  34. package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
  35. package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
  36. package/src/lib/hooks/firestore-denial-reporter.js +293 -0
  37. package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
  38. package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
  39. package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
  40. package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
  41. package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
  42. package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
  43. package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
  44. package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
  45. package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
  46. package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
  47. package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
  48. package/src/lib/hooks/helpers/listen-options.js +92 -0
  49. package/src/lib/hooks/helpers/listen-options.js.map +1 -0
  50. package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
  51. package/src/lib/hooks/helpers/use-doc.js +213 -0
  52. package/src/lib/hooks/helpers/use-doc.js.map +1 -0
  53. package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
  54. package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
  55. package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
  56. package/src/lib/hooks/host-collection-queries.d.ts +125 -0
  57. package/src/lib/hooks/host-collection-queries.js +126 -0
  58. package/src/lib/hooks/host-collection-queries.js.map +1 -0
  59. package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
  60. package/src/lib/hooks/list-filter-constraints.js +288 -0
  61. package/src/lib/hooks/list-filter-constraints.js.map +1 -0
  62. package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
  63. package/src/lib/hooks/sorted-collection-window.js +150 -0
  64. package/src/lib/hooks/sorted-collection-window.js.map +1 -0
  65. package/src/lib/hooks/use-component-version.d.ts +43 -0
  66. package/src/lib/hooks/use-component-version.js +73 -0
  67. package/src/lib/hooks/use-component-version.js.map +1 -0
  68. package/src/lib/hooks/use-component.d.ts +39 -0
  69. package/src/lib/hooks/use-component.js +55 -0
  70. package/src/lib/hooks/use-component.js.map +1 -0
  71. package/src/lib/hooks/use-console-host-route.d.ts +24 -0
  72. package/src/lib/hooks/use-console-host-route.js +77 -0
  73. package/src/lib/hooks/use-console-host-route.js.map +1 -0
  74. package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
  75. package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
  76. package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
  77. package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
  78. package/src/lib/hooks/use-firestore-collection.js +307 -0
  79. package/src/lib/hooks/use-firestore-collection.js.map +1 -0
  80. package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
  81. package/src/lib/hooks/use-firestore-doc.js +192 -0
  82. package/src/lib/hooks/use-firestore-doc.js.map +1 -0
  83. package/src/lib/hooks/use-form-version.d.ts +44 -0
  84. package/src/lib/hooks/use-form-version.js +73 -0
  85. package/src/lib/hooks/use-form-version.js.map +1 -0
  86. package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
  87. package/src/lib/hooks/use-host-activity-logger.js +96 -0
  88. package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
  89. package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
  90. package/src/lib/hooks/use-host-campaigns.js +93 -0
  91. package/src/lib/hooks/use-host-campaigns.js.map +1 -0
  92. package/src/lib/hooks/use-host-org-id.d.ts +96 -0
  93. package/src/lib/hooks/use-host-org-id.js +154 -0
  94. package/src/lib/hooks/use-host-org-id.js.map +1 -0
  95. package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
  96. package/src/lib/hooks/use-host-resource-api.js +60 -0
  97. package/src/lib/hooks/use-host-resource-api.js.map +1 -0
  98. package/src/lib/hooks/use-host-template.d.ts +44 -0
  99. package/src/lib/hooks/use-host-template.js +82 -0
  100. package/src/lib/hooks/use-host-template.js.map +1 -0
  101. package/src/lib/hooks/use-host-version-api.d.ts +59 -0
  102. package/src/lib/hooks/use-host-version-api.js +69 -0
  103. package/src/lib/hooks/use-host-version-api.js.map +1 -0
  104. package/src/lib/hooks/use-host.d.ts +101 -0
  105. package/src/lib/hooks/use-host.js +45 -0
  106. package/src/lib/hooks/use-host.js.map +1 -0
  107. package/src/lib/hooks/use-layout-version.d.ts +33 -0
  108. package/src/lib/hooks/use-layout-version.js +64 -0
  109. package/src/lib/hooks/use-layout-version.js.map +1 -0
  110. package/src/lib/hooks/use-layout.d.ts +41 -0
  111. package/src/lib/hooks/use-layout.js +49 -0
  112. package/src/lib/hooks/use-layout.js.map +1 -0
  113. package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
  114. package/src/lib/hooks/use-live-artifact-count.js +102 -0
  115. package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
  116. package/src/lib/hooks/use-org-member-options.d.ts +57 -0
  117. package/src/lib/hooks/use-org-member-options.js +112 -0
  118. package/src/lib/hooks/use-org-member-options.js.map +1 -0
  119. package/src/lib/hooks/use-org-plan.d.ts +46 -0
  120. package/src/lib/hooks/use-org-plan.js +86 -0
  121. package/src/lib/hooks/use-org-plan.js.map +1 -0
  122. package/src/lib/hooks/use-paged-collection.d.ts +95 -0
  123. package/src/lib/hooks/use-paged-collection.js +130 -0
  124. package/src/lib/hooks/use-paged-collection.js.map +1 -0
  125. package/src/lib/hooks/use-plugin-config.d.ts +38 -0
  126. package/src/lib/hooks/use-plugin-config.js +120 -0
  127. package/src/lib/hooks/use-plugin-config.js.map +1 -0
  128. package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
  129. package/src/lib/hooks/use-scope-tokens.js +100 -0
  130. package/src/lib/hooks/use-scope-tokens.js.map +1 -0
  131. package/src/lib/hooks/use-screen-version.d.ts +33 -0
  132. package/src/lib/hooks/use-screen-version.js +65 -0
  133. package/src/lib/hooks/use-screen-version.js.map +1 -0
  134. package/src/lib/hooks/use-screen.d.ts +68 -0
  135. package/src/lib/hooks/use-screen.js +50 -0
  136. package/src/lib/hooks/use-screen.js.map +1 -0
  137. package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
  138. package/src/lib/hooks/use-sending-identity-api.js +95 -0
  139. package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
  140. package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
  141. package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
  142. package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
  143. package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
  144. package/src/lib/hooks/use-switcher-collection.js +275 -0
  145. package/src/lib/hooks/use-switcher-collection.js.map +1 -0
  146. package/src/lib/hooks/use-user-name.d.ts +51 -0
  147. package/src/lib/hooks/use-user-name.js +90 -0
  148. package/src/lib/hooks/use-user-name.js.map +1 -0
  149. package/src/lib/hooks/use-user-photo.d.ts +50 -0
  150. package/src/lib/hooks/use-user-photo.js +87 -0
  151. package/src/lib/hooks/use-user-photo.js.map +1 -0
  152. package/src/lib/tenant.d.ts +61 -0
  153. package/src/lib/tenant.js +62 -0
  154. 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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,iBAAiB,EACjBC,UAAU,QAKL,qBAAoB;AAC3B,SAASC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAA6B,QAAO;AACxE,SAASC,0BAA0B,QAAQ,8BAA0B;AACrE,SACEC,uBAAuB,EACvBC,mBAAmB,EACnBC,qBAAqB,EACrBC,yBAAyB,EACzBC,qBAAqB,EACrBC,6BAA6B,QACxB,iCAA6B;AAEpC,MAAMC,iBAAiB;AACvB,MAAMC,cAAc;AA6DpB;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,uBACdC,UAAwD,EACxDC,IAAoB,EACpBC,UAAyC,CAAC,CAAC;IAE3C,MAAM,CAACC,MAAMC,QAAQ,GAAGf,SAAc,EAAE;IACxC,MAAM,CAACgB,QAAQC,UAAU,GAAGjB,SAAoC;IAChE,MAAM,CAACkB,OAAOC,SAAS,GAAGnB,SAAqCoB;IAC/D,0EAA0E;IAC1E,yDAAyD;IACzD,MAAM,CAACC,WAAWC,aAAa,GAAGtB,SAAS;IAC3C,MAAM,CAACuB,cAAcC,gBAAgB,GAAGxB,SAAS;IACjD,MAAMyB,gBAAgB1B,OAAOY;IAC7Bc,cAAcC,OAAO,GAAGf;IACxB,MAAMgB,UAAUd,QAAQc,OAAO;IAC/B,MAAMC,wBAAwBf,QAAQe,qBAAqB;IAE3D9B,UAAU;QACRmB,UAAU;QACVE,SAASC;QACT,qEAAqE;QACrE,oEAAoE;QACpE,mEAAmE;QACnE,mEAAmE;QACnE,mEAAmE;QACnEL,QAAQ,EAAE;QACVO,aAAa;QACbE,gBAAgB;QAEhB,MAAMK,IAAIJ,cAAcC,OAAO;QAC/B,IAAI,CAACG,GAAG;YACN;QACF;QAEA,IAAIC,YAAY;QAChB,IAAIC,cAAmC;QACvC,IAAIC,QAA8C;QAClD,gEAAgE,GAChE,IAAIC,sBAA2C;QAC/C,IAAIC,UAAU;QACd;;;;;;KAMC,GACD,IAAIC,eAAe;QACnB,+DAA+D,GAC/D,IAAIC,wBAAwB;QAC5B,IAAIC,iBAAiB;QACrB;;;;;;;;;;;;KAYC,GACD,IAAIC,WAAW;QACf,MAAMC,cAAcpC,oBAAoB0B;QACxC,4DAA4D;QAC5D,MAAMW,OAAO,IAAIC;QACjB,MAAMC,gBAAgB,IAAID;QAE1B,MAAME,OAAO,CACXC,MACAC;YAEA,qEAAqE;YACrE,6DAA6D;YAC7D,IAAI,CAACA,QAAQP,WAAW;YACxB,IAAI,CAACA,UAAU;gBACbrB,UAAU;gBACVE,SAASC;YACX;YACAE,aAAauB;YACb9B,QACE6B,KAAKE,GAAG,CAAC,CAACC;gBACR,MAAMC,QAAQ,aAAKD,QAAQjC,IAAI;gBAC/B,IAAIa,SAASqB,KAAK,CAACrB,QAAQ,GAAGoB,QAAQE,EAAE;gBACxC,OAAOD;YACT;QAEJ;QAEA,MAAME,YAAY;YAChBnB,cAAclC,WACZgC,GACA,oEAAoE;YACpE,kEAAkE;YAClE,sEAAsE;YACtE5B,4BACA,CAACkD;gBACC,IAAIrB,WAAW;gBACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAsCC,GACD,8DAA8D;gBAC9D,IAAI,CAACqB,SAASC,QAAQ,CAAC/B,SAAS,EAAE;oBAChCa,UAAU;oBACVC,eAAe;oBACfE,iBAAiB;oBACjBb,gBAAgB;oBAChB,6DAA6D;oBAC7D,2DAA2D;oBAC3D,mDAAmD;oBACnDnB;gBACF,OAAO,IAAI8B,iBAAiB,GAAG;oBAC7B,6DAA6D;oBAC7D,iEAAiE;oBACjE,qCAAqC;oBACrCD,UAAU;gBACZ;gBAEA,IAAIN,uBAAuB;oBACzB,MAAMyB,UAAU,IAAIZ,IAAIU,SAASP,IAAI,CAACE,GAAG,CAAC,CAACQ,IAAMA,EAAEL,EAAE;oBACrD,MAAMM,WAAW;2BAAIf;qBAAK,CAACgB,MAAM,CAC/B,CAACP,KAAO,CAACI,QAAQI,GAAG,CAACR,OAAO,CAACP,cAAce,GAAG,CAACR;oBAEjD,IAAIM,SAASG,MAAM,EAAE;wBACnB,+DAA+D;wBAC/D,+DAA+D;wBAC/D9D,kBAAkBiC,GACf8B,IAAI,CAAC,CAACC;4BACL,IAAI9B,WAAW;4BACf,MAAM+B,WAAW,IAAIpB,IAAImB,MAAMhB,IAAI,CAACE,GAAG,CAAC,CAACQ,IAAMA,EAAEL,EAAE;4BACnD,2DAA2D;4BAC3D,2DAA2D;4BAC3D,wBAAwB;4BACxBM,SAASO,OAAO,CAAC,CAACb;gCAChB,IAAI,CAACY,SAASJ,GAAG,CAACR,KAAKP,cAAcqB,GAAG,CAACd;4BAC3C;4BACAY,SAASC,OAAO,CAAC,CAACb,KAAOT,KAAKuB,GAAG,CAACd;4BAClC,yDAAyD;4BACzDN,KAAKiB,MAAMhB,IAAI,EAAE;wBACnB,GACCoB,KAAK,CAAC;4BACL,wDAAwD;4BACxD,mDAAmD;4BACnD,IAAI,CAAClC,WAAWa,KAAKQ,SAASP,IAAI,EAAEO,SAASC,QAAQ,CAAC/B,SAAS;wBACjE;wBACF;oBACF;oBACA8B,SAASP,IAAI,CAACkB,OAAO,CAAC,CAACR,IAAMd,KAAKuB,GAAG,CAACT,EAAEL,EAAE;gBAC5C;gBAEAN,KAAKQ,SAASP,IAAI,EAAEO,SAASC,QAAQ,CAAC/B,SAAS;YACjD,GACA,CAAC4C;gBACC,IAAInC,WAAW;gBACfC,+BAAAA;gBACA,kEAAkE;gBAClE,kEAAkE;gBAClE,IAAIkC,CAAAA,uBAAAA,IAAKC,IAAI,MAAK,qBAAqB;oBACrC/B,gBAAgB;oBAChB,wDAAwD;oBACxD,8DAA8D;oBAC9D,iEAAiE;oBACjE,IAAIA,iBAAiB,GAAGC,wBAAwB+B,KAAKC,GAAG;oBACxD,IAAIjC,gBAAgBjC,yBAAyB;wBAC3CsB,gBAAgB;wBAChB,IAAI,CAACa,gBAAgB;4BACnBA,iBAAiB;4BACjBjC,sBAAsBmC;wBACxB;oBACF;gBACF;gBACA,IAAIL,UAAUzB,aAAa;oBACzByB,WAAW;oBACX,IAAIC,eAAe1B,aAAa;wBAC9BwB,sBAAsB3B,sBACpB4C,WACAd;oBAEJ,OAAO;wBACLJ,QAAQqC,WAAWnB,WAAW1C;oBAChC;gBACF,OAAO;oBACL8B,WAAW;oBACXrB,UAAU;oBACVE,SAAS8C;oBACT;;;;;;;;;;;;;;;;;aAiBC,GACD,IAAI9B,eAAe1B,aAAa;wBAC9BwB,sBAAsB3B,sBACpB4C,WACAd;oBAEJ;gBACF;YACF;QAEJ;QACAc;QAEA;;;;;;;;;;;;;;;;;;;;;KAqBC,GACD,MAAMoB,kBAAkB/D,8BAA8B;YACpD,IAAIuB,aAAaK,iBAAiB,GAAG;YACrC,IAAIH,OAAO;gBACTuC,aAAavC;gBACbA,QAAQ;YACV;YACAC,uCAAAA;YACAA,sBAAsB;YACtBF,+BAAAA;YACAG,UAAU;YACVG,iBAAiB;YACjBa;QACF;QAEA,OAAO;YACLpB,YAAY;YACZwC;YACA,IAAItC,OAAOuC,aAAavC;YACxBC,uCAAAA;YACAF,+BAAAA;QACF;IACA,uDAAuD;IACzD,GAAGnB;IAEH,OAAO;QAAEE;QAAME;QAAQE;QAAOG;QAAWE;IAAa;AACxD;AAEA,eAAeb,uBAAsB"}