@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,38 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 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 DocumentReference } from 'firebase/firestore';
18
+ import type { ObservableStatus, FirestoreDocOptions } from '../firebase/firebase-services';
19
+ import { type UseModifyDocCallback } from './use-modify-doc-callback';
20
+ /**
21
+ * Raw `onSnapshot` listener with its own retry/backoff instead of
22
+ * reactfire's `useFirestoreDocData` — that hook caches the query's RxJS
23
+ * Observable, and once it errors the Observable is *terminated forever*:
24
+ * remounting the component never reopens the Firestore subscription, so a
25
+ * single transient `permission-denied` (e.g. right after sign-in, before
26
+ * Firestore's own credential provider has attached the user's ID token)
27
+ * permanently breaks every caller of `useHost`/`useLayout`/`useScreen`/
28
+ * `useScreenVersion`/`useLayoutVersion` for the rest of the session
29
+ * (AGL-216/217/223). A fresh `onSnapshot` call each retry opens a genuinely
30
+ * new listener, so it recovers once the token has propagated.
31
+ */
32
+ export declare function useDocData<T>(ref: DocumentReference<T>, options?: FirestoreDocOptions<T>): ObservableStatus<T>;
33
+ export type UseDocData<T> = typeof useDocData<T>;
34
+ export declare function useDoc<T>(ref: DocumentReference<T>, options?: FirestoreDocOptions<T>): {
35
+ doc: ReturnType<UseDocData<T>>;
36
+ setDoc: ReturnType<UseModifyDocCallback<T>>;
37
+ };
38
+ export default useDoc;
@@ -0,0 +1,213 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2022 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { onSnapshot } from "firebase/firestore";
18
+ import { useEffect, useRef, useState } from "react";
19
+ import { DENIAL_STREAK_TO_REPORT, noteFirestoreServerRead, scheduleRefusedReopen, subscribeFirestoreSessionHeal } from "../firestore-denial-reporter.js";
20
+ import { CONFIRMABLE_LISTEN_OPTIONS } from "./listen-options.js";
21
+ import useModifyDocCallback from "./use-modify-doc-callback.js";
22
+ const RETRY_DELAY_MS = 400;
23
+ const MAX_RETRIES = 5;
24
+ // The refused cadence is shared policy — see `scheduleRefusedReopen` in
25
+ // `firestore-denial-reporter.ts` (AGL-1066, AGL-1440, AGL-2944, AGL-2945).
26
+ /**
27
+ * Raw `onSnapshot` listener with its own retry/backoff instead of
28
+ * reactfire's `useFirestoreDocData` — that hook caches the query's RxJS
29
+ * Observable, and once it errors the Observable is *terminated forever*:
30
+ * remounting the component never reopens the Firestore subscription, so a
31
+ * single transient `permission-denied` (e.g. right after sign-in, before
32
+ * Firestore's own credential provider has attached the user's ID token)
33
+ * permanently breaks every caller of `useHost`/`useLayout`/`useScreen`/
34
+ * `useScreenVersion`/`useLayoutVersion` for the rest of the session
35
+ * (AGL-216/217/223). A fresh `onSnapshot` call each retry opens a genuinely
36
+ * new listener, so it recovers once the token has propagated.
37
+ */ export function useDocData(ref, options) {
38
+ const idField = options == null ? void 0 : options.idField;
39
+ const initialData = options == null ? void 0 : options.initialData;
40
+ const [status, setStatus] = useState(initialData !== undefined ? 'success' : 'loading');
41
+ const [data, setData] = useState(initialData);
42
+ const [error, setError] = useState(undefined);
43
+ const [hasEmitted, setHasEmitted] = useState(initialData !== undefined);
44
+ const [hasPendingWrites, setHasPendingWrites] = useState(false);
45
+ // Un-confirmed until a snapshot says otherwise (AGL-1066).
46
+ const [fromCache, setFromCache] = useState(true);
47
+ const [serverDenied, setServerDenied] = useState(false);
48
+ const resolveFirstValueRef = useRef(undefined);
49
+ const firstValuePromiseRef = useRef(undefined);
50
+ if (!firstValuePromiseRef.current) {
51
+ firstValuePromiseRef.current = new Promise((resolve)=>{
52
+ resolveFirstValueRef.current = resolve;
53
+ });
54
+ }
55
+ useEffect(()=>{
56
+ setStatus('loading');
57
+ setError(undefined);
58
+ let cancelled = false;
59
+ let unsubscribe = null;
60
+ let timer = null;
61
+ /** Cancels a pending reopen on the refused cadence (AGL-2944). */ let cancelRefusedReopen = null;
62
+ let attempt = 0;
63
+ /**
64
+ * Refusals since the last SERVER snapshot (AGL-1066).
65
+ *
66
+ * Still distinct from `attempt` after the flip: `attempt` is a BUDGET
67
+ * that the heal broadcast may refund, whereas this is EVIDENCE about the
68
+ * server that only a server snapshot may clear. Counted here to publish
69
+ * `serverDenied` and to decide the slow cadence — the `session-health`
70
+ * reporting the other two listener hooks do is a separate decision, and
71
+ * this hook's callers (the besigner document family) are exactly the ones
72
+ * a false verdict would hurt most.
73
+ */ let deniedStreak = 0;
74
+ /** Epoch ms of the current streak's first refusal (AGL-1440). */ let deniedStreakStartedAt = 0;
75
+ /**
76
+ * The retry budget is spent and the server's last word was a refusal —
77
+ * what keeps `status: 'error'` from being undone by the cached emission
78
+ * that precedes every reopened refusal (AGL-1066). See the longer note
79
+ * on the same variable in `use-firestore-collection`.
80
+ */ let terminal = false;
81
+ const subscribe = ()=>{
82
+ unsubscribe = onSnapshot(ref, // Without this the cache→server confirmation is never delivered and
83
+ // `fromCache` latches true for the life of the listener, refusing
84
+ // every guarded save on the page (AGL-2486). See `listen-options.ts`.
85
+ CONFIRMABLE_LISTEN_OPTIONS, (snapshot)=>{
86
+ if (cancelled) return;
87
+ // Only the SERVER answering refunds the retry budget, and only a
88
+ // server snapshot ends a refusal (AGL-1066).
89
+ if (!snapshot.metadata.fromCache) {
90
+ attempt = 0;
91
+ terminal = false;
92
+ deniedStreak = 0;
93
+ setServerDenied(false);
94
+ // Evidence only, deliberately not the reporting call: this hook
95
+ // stays out of `session-health` (see `deniedStreak` above), but
96
+ // its server answers are still proof the session can read, which
97
+ // is what `refusedRetryDelayMs` splits the cadence on (AGL-1440).
98
+ noteFirestoreServerRead();
99
+ } else if (deniedStreak === 0) {
100
+ // Cached, but nothing has been refused since the server last
101
+ // answered — not the AGL-1066 fault, so the budget is refunded
102
+ // exactly as it always was. This is what keeps offline inert:
103
+ // `unavailable` never enters `deniedStreak`.
104
+ attempt = 0;
105
+ }
106
+ if (!terminal) {
107
+ setStatus('success');
108
+ setError(undefined);
109
+ }
110
+ // Always true after ANY emission, cached included: this is what
111
+ // tells a consumer "I have data, the server refused" apart from "I
112
+ // never had data" — the distinction the besigner editors and the
113
+ // host setup Theme tab blank on (AGL-1066).
114
+ setHasEmitted(true);
115
+ // Kept, not dropped: a snapshot that still carries this client's
116
+ // own queued write is not evidence of what the store holds
117
+ // (AGL-1262). Callers that record a "last saved" state need to
118
+ // know the difference.
119
+ setHasPendingWrites(snapshot.metadata.hasPendingWrites);
120
+ setFromCache(snapshot.metadata.fromCache);
121
+ const value = snapshot.exists() ? _extends({}, snapshot.data(), idField ? {
122
+ [idField]: snapshot.id
123
+ } : {}) : undefined;
124
+ setData(value);
125
+ resolveFirstValueRef.current == null ? void 0 : resolveFirstValueRef.current.call(resolveFirstValueRef);
126
+ }, (err)=>{
127
+ if (cancelled) return;
128
+ unsubscribe == null ? void 0 : unsubscribe();
129
+ // Only a refusal — a lost network produces no error callback at
130
+ // all, and anything that does surface one carries `unavailable`,
131
+ // which must never read as a dead session.
132
+ if ((err == null ? void 0 : err.code) === 'permission-denied') {
133
+ deniedStreak += 1;
134
+ if (deniedStreak === 1) deniedStreakStartedAt = Date.now();
135
+ if (deniedStreak >= DENIAL_STREAK_TO_REPORT) setServerDenied(true);
136
+ }
137
+ if (attempt < MAX_RETRIES) {
138
+ attempt += 1;
139
+ if (deniedStreak > MAX_RETRIES) {
140
+ cancelRefusedReopen = scheduleRefusedReopen(subscribe, deniedStreakStartedAt);
141
+ } else {
142
+ timer = setTimeout(subscribe, RETRY_DELAY_MS);
143
+ }
144
+ } else {
145
+ terminal = true;
146
+ setStatus('error');
147
+ setError(err);
148
+ resolveFirstValueRef.current == null ? void 0 : resolveFirstValueRef.current.call(resolveFirstValueRef);
149
+ // A refusal streak keeps a slow road back for a recovery nobody
150
+ // announced — see the same branch in `use-firestore-collection`.
151
+ // The cadence splits on session-vs-ref evidence (AGL-1440), grows
152
+ // with the streak's age (AGL-2945) and waits while the tab is
153
+ // hidden (AGL-2944).
154
+ if (deniedStreak > MAX_RETRIES) {
155
+ cancelRefusedReopen = scheduleRefusedReopen(subscribe, deniedStreakStartedAt);
156
+ }
157
+ }
158
+ });
159
+ };
160
+ subscribe();
161
+ /**
162
+ * Reopen when the session heals — see the long note on the same
163
+ * subscription in `use-firestore-collection` (AGL-1066).
164
+ *
165
+ * This hook is where it counts most: its deps are `[ref.firestore,
166
+ * ref.path]`, neither of which moves when a `stale` re-auth signs the
167
+ * same uid back in, and it is what every besigner editor reads through.
168
+ * Nothing else was ever going to bring those pages back without a reload.
169
+ */ const unsubscribeHeal = subscribeFirestoreSessionHeal(()=>{
170
+ if (cancelled || deniedStreak === 0) return;
171
+ if (timer) {
172
+ clearTimeout(timer);
173
+ timer = null;
174
+ }
175
+ cancelRefusedReopen == null ? void 0 : cancelRefusedReopen();
176
+ cancelRefusedReopen = null;
177
+ unsubscribe == null ? void 0 : unsubscribe();
178
+ attempt = 0;
179
+ subscribe();
180
+ });
181
+ return ()=>{
182
+ cancelled = true;
183
+ unsubscribeHeal();
184
+ if (timer) clearTimeout(timer);
185
+ cancelRefusedReopen == null ? void 0 : cancelRefusedReopen();
186
+ unsubscribe == null ? void 0 : unsubscribe();
187
+ };
188
+ // eslint-disable-next-line react-hooks/exhaustive-deps
189
+ }, [
190
+ ref.firestore,
191
+ ref.path
192
+ ]);
193
+ return {
194
+ status,
195
+ hasEmitted,
196
+ isComplete: false,
197
+ data,
198
+ error,
199
+ firstValuePromise: firstValuePromiseRef.current,
200
+ hasPendingWrites,
201
+ fromCache,
202
+ serverDenied
203
+ };
204
+ }
205
+ export function useDoc(ref, options) {
206
+ return {
207
+ doc: useDocData(ref, options),
208
+ setDoc: useModifyDocCallback(ref)
209
+ };
210
+ }
211
+ export default useDoc;
212
+
213
+ //# sourceMappingURL=use-doc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/helpers/use-doc.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 { onSnapshot, type DocumentReference } from 'firebase/firestore'\nimport { useEffect, useRef, useState } from 'react'\nimport type { ObservableStatus, FirestoreDocOptions } from '../firebase/firebase-services'\nimport {\n DENIAL_STREAK_TO_REPORT,\n noteFirestoreServerRead,\n scheduleRefusedReopen,\n subscribeFirestoreSessionHeal,\n} from '../firestore-denial-reporter'\nimport { CONFIRMABLE_LISTEN_OPTIONS } from './listen-options'\nimport useModifyDocCallback, {\n type UseModifyDocCallback,\n} from './use-modify-doc-callback'\n\nconst RETRY_DELAY_MS = 400\nconst MAX_RETRIES = 5\n// The refused cadence is shared policy — see `scheduleRefusedReopen` in\n// `firestore-denial-reporter.ts` (AGL-1066, AGL-1440, AGL-2944, AGL-2945).\n\n/**\n * Raw `onSnapshot` listener with its own retry/backoff instead of\n * reactfire's `useFirestoreDocData` — that hook caches the query's RxJS\n * Observable, and once it errors the Observable is *terminated forever*:\n * remounting the component never reopens the Firestore subscription, so a\n * single transient `permission-denied` (e.g. right after sign-in, before\n * Firestore's own credential provider has attached the user's ID token)\n * permanently breaks every caller of `useHost`/`useLayout`/`useScreen`/\n * `useScreenVersion`/`useLayoutVersion` for the rest of the session\n * (AGL-216/217/223). A fresh `onSnapshot` call each retry opens a genuinely\n * new listener, so it recovers once the token has propagated.\n */\nexport function useDocData<T>(\n ref: DocumentReference<T>,\n options?: FirestoreDocOptions<T>,\n): ObservableStatus<T> {\n const idField = options?.idField\n const initialData = options?.initialData\n const [status, setStatus] = useState<'loading' | 'error' | 'success'>(\n initialData !== undefined ? 'success' : 'loading',\n )\n const [data, setData] = useState<T>(initialData as T)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [hasEmitted, setHasEmitted] = useState(initialData !== undefined)\n const [hasPendingWrites, setHasPendingWrites] = useState(false)\n // Un-confirmed until a snapshot says otherwise (AGL-1066).\n const [fromCache, setFromCache] = useState(true)\n const [serverDenied, setServerDenied] = useState(false)\n const resolveFirstValueRef = useRef<(() => void) | undefined>(undefined)\n const firstValuePromiseRef = useRef<Promise<void> | undefined>(undefined)\n if (!firstValuePromiseRef.current) {\n firstValuePromiseRef.current = new Promise((resolve) => {\n resolveFirstValueRef.current = resolve\n })\n }\n\n useEffect(() => {\n setStatus('loading')\n setError(undefined)\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 * Still distinct from `attempt` after the flip: `attempt` is a BUDGET\n * that the heal broadcast may refund, whereas this is EVIDENCE about the\n * server that only a server snapshot may clear. Counted here to publish\n * `serverDenied` and to decide the slow cadence — the `session-health`\n * reporting the other two listener hooks do is a separate decision, and\n * this hook's callers (the besigner document family) are exactly the ones\n * a false verdict would hurt most.\n */\n let deniedStreak = 0\n /** Epoch ms of the current streak's first refusal (AGL-1440). */\n let deniedStreakStartedAt = 0\n /**\n * The retry budget is spent and the server's last word was a refusal —\n * what keeps `status: 'error'` from being undone by the cached emission\n * that precedes every reopened refusal (AGL-1066). See the longer note\n * on the same variable in `use-firestore-collection`.\n */\n let terminal = false\n\n const subscribe = () => {\n unsubscribe = onSnapshot(\n ref,\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 // Only the SERVER answering refunds the retry budget, and only a\n // server snapshot ends a refusal (AGL-1066).\n if (!snapshot.metadata.fromCache) {\n attempt = 0\n terminal = false\n deniedStreak = 0\n setServerDenied(false)\n // Evidence only, deliberately not the reporting call: this hook\n // stays out of `session-health` (see `deniedStreak` above), but\n // its server answers are still proof the session can read, which\n // is what `refusedRetryDelayMs` splits the cadence on (AGL-1440).\n noteFirestoreServerRead()\n } else if (deniedStreak === 0) {\n // Cached, but nothing has been refused since the server last\n // answered — not the AGL-1066 fault, so the budget is refunded\n // exactly as it always was. This is what keeps offline inert:\n // `unavailable` never enters `deniedStreak`.\n attempt = 0\n }\n if (!terminal) {\n setStatus('success')\n setError(undefined)\n }\n // Always true after ANY emission, cached included: this is what\n // tells a consumer \"I have data, the server refused\" apart from \"I\n // never had data\" — the distinction the besigner editors and the\n // host setup Theme tab blank on (AGL-1066).\n setHasEmitted(true)\n // Kept, not dropped: a snapshot that still carries this client's\n // own queued write is not evidence of what the store holds\n // (AGL-1262). Callers that record a \"last saved\" state need to\n // know the difference.\n setHasPendingWrites(snapshot.metadata.hasPendingWrites)\n setFromCache(snapshot.metadata.fromCache)\n const value = snapshot.exists()\n ? ({\n ...(snapshot.data() as object),\n ...(idField ? { [idField]: snapshot.id } : {}),\n } as T)\n : (undefined as T)\n setData(value)\n resolveFirstValueRef.current?.()\n },\n (err) => {\n if (cancelled) return\n unsubscribe?.()\n // Only a refusal — a lost network produces no error callback at\n // all, and anything that does surface one carries `unavailable`,\n // which must never read as a dead session.\n if ((err as { code?: string })?.code === 'permission-denied') {\n deniedStreak += 1\n if (deniedStreak === 1) deniedStreakStartedAt = Date.now()\n if (deniedStreak >= DENIAL_STREAK_TO_REPORT) setServerDenied(true)\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 resolveFirstValueRef.current?.()\n // A refusal streak keeps a slow road back for a recovery nobody\n // announced — see the same branch in `use-firestore-collection`.\n // The cadence splits on session-vs-ref evidence (AGL-1440), grows\n // with the streak's age (AGL-2945) and waits while the tab is\n // hidden (AGL-2944).\n if (deniedStreak > MAX_RETRIES) {\n cancelRefusedReopen = scheduleRefusedReopen(\n subscribe,\n deniedStreakStartedAt,\n )\n }\n }\n },\n )\n }\n subscribe()\n\n /**\n * Reopen when the session heals — see the long note on the same\n * subscription in `use-firestore-collection` (AGL-1066).\n *\n * This hook is where it counts most: its deps are `[ref.firestore,\n * ref.path]`, neither of which moves when a `stale` re-auth signs the\n * same uid back in, and it is what every besigner editor reads through.\n * Nothing else was ever going to bring those pages back without a reload.\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 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 }, [ref.firestore, ref.path])\n\n return {\n status,\n hasEmitted,\n isComplete: false,\n data,\n error,\n firstValuePromise: firstValuePromiseRef.current,\n hasPendingWrites,\n fromCache,\n serverDenied,\n }\n}\nexport type UseDocData<T> = typeof useDocData<T>\n\nexport function useDoc<T>(\n ref: DocumentReference<T>,\n options?: FirestoreDocOptions<T>,\n): {doc: ReturnType<UseDocData<T>>, setDoc: ReturnType<UseModifyDocCallback<T>>} {\n return {doc: useDocData(ref, options), setDoc: useModifyDocCallback(ref)}\n}\n\n\nexport default useDoc\n"],"names":["onSnapshot","useEffect","useRef","useState","DENIAL_STREAK_TO_REPORT","noteFirestoreServerRead","scheduleRefusedReopen","subscribeFirestoreSessionHeal","CONFIRMABLE_LISTEN_OPTIONS","useModifyDocCallback","RETRY_DELAY_MS","MAX_RETRIES","useDocData","ref","options","idField","initialData","status","setStatus","undefined","data","setData","error","setError","hasEmitted","setHasEmitted","hasPendingWrites","setHasPendingWrites","fromCache","setFromCache","serverDenied","setServerDenied","resolveFirstValueRef","firstValuePromiseRef","current","Promise","resolve","cancelled","unsubscribe","timer","cancelRefusedReopen","attempt","deniedStreak","deniedStreakStartedAt","terminal","subscribe","snapshot","metadata","value","exists","id","err","code","Date","now","setTimeout","unsubscribeHeal","clearTimeout","firestore","path","isComplete","firstValuePromise","useDoc","doc","setDoc"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 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 DocumentReference, type SetOptions, type UpdateData } from 'firebase/firestore';
18
+ export type UpdateDocCallback<T> = (data: UpdateData<T>) => Promise<void>;
19
+ export type SetDocCallback<T> = (data: Partial<T>, options?: SetOptions) => Promise<void>;
20
+ export type ModifyDocOptions = SetOptions & {
21
+ shouldSet?: boolean;
22
+ };
23
+ export type ModifyDocCallback<T> = (data: UpdateData<T> | Partial<T>, options?: ModifyDocOptions) => Promise<void>;
24
+ export declare function useUpdateDocCallback<T>(ref: DocumentReference<T>): UpdateDocCallback<T>;
25
+ export declare function useSetDocCallback<T>(ref: DocumentReference<T>): SetDocCallback<T>;
26
+ export declare function useModifyDocCallback<T>(ref: DocumentReference<T>): ModifyDocCallback<T>;
27
+ export type UseModifyDocCallback<T> = typeof useModifyDocCallback<T>;
28
+ export default useModifyDocCallback;
@@ -0,0 +1,110 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2022 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { HOST_UNPERSISTED_FIELDS, ORG_UNPERSISTED_FIELDS } from "@aglyn/aglyn";
18
+ import { serverTimestamp, setDoc, updateDoc } from "firebase/firestore";
19
+ import { useCallback } from "react";
20
+ /**
21
+ * Every UI write stamps `updatedAt` (AGL-455) — the "last updated" columns
22
+ * read it, and callers historically forgot it. Spread order lets a caller's
23
+ * explicit `updatedAt` win (backfills, imports).
24
+ */ function stampUpdatedAt(data) {
25
+ return _extends({
26
+ updatedAt: serverTimestamp()
27
+ }, data);
28
+ }
29
+ /**
30
+ * Keys that are never document fields, taken from where that is written down
31
+ * (AGL-1429). `HOST_UNPERSISTED_FIELDS` and `ORG_UNPERSISTED_FIELDS` each
32
+ * declare the synthetic `$id` the readers attach; the INTERSECTION is used
33
+ * deliberately, so this generic boundary only ever strips a key that every
34
+ * declaration agrees is unpersisted. A key added to one map alone is not
35
+ * silently stripped from the other collection's writes — the spec's drift
36
+ * assertion fires instead, and someone decides.
37
+ */ /*
38
+ * Resolved on first use, not at module scope. Computing it eagerly made this
39
+ * module — and therefore every consumer of the write callbacks — fail to LOAD
40
+ * in any suite that mocks the `@aglyn/aglyn` barrel without re-exporting both
41
+ * maps, which two console specs do. A write-boundary guard that cannot be
42
+ * imported is worse than the leak it prevents.
43
+ */ let unpersistedFields;
44
+ function getUnpersistedFields() {
45
+ if (unpersistedFields) return unpersistedFields;
46
+ const host = HOST_UNPERSISTED_FIELDS != null ? HOST_UNPERSISTED_FIELDS : {};
47
+ const org = ORG_UNPERSISTED_FIELDS != null ? ORG_UNPERSISTED_FIELDS : {};
48
+ unpersistedFields = Object.freeze(Object.keys(host).filter((key)=>key in org));
49
+ return unpersistedFields;
50
+ }
51
+ /**
52
+ * Drop the reader-injected keys before the payload leaves for Firestore.
53
+ *
54
+ * This has to happen here rather than in the refs' converters, which is where
55
+ * it used to live alone (`503f197ca` added the `$id` strip to `useHostRef`'s
56
+ * `toFirestore`). `updateDoc` never applies a converter — `setDoc` calls
57
+ * `applyFirestoreDataConverter` and `updateDoc` does not — so a converter
58
+ * strip is a guarantee about ONE of the two SDK calls, and which one a write
59
+ * gets is decided by whether its caller happened to pass `merge`. That is how
60
+ * `hosts/-MtN17_cpfPPLwWjE6z4` came to hold a persisted `$id` (AGL-1423).
61
+ *
62
+ * Only exact top-level keys are removed. Dotted `updateDoc` paths address
63
+ * nested maps, and besigner node maps legitimately store a `$id` per node —
64
+ * matching those would be the same over-broad scan AGL-1423 warned about.
65
+ */ function dropUnpersistedFields(data) {
66
+ const keys = getUnpersistedFields();
67
+ if (!keys.some((key)=>key in data)) return data;
68
+ const rest = _extends({}, data);
69
+ for (const key of keys)delete rest[key];
70
+ return rest;
71
+ }
72
+ // `updateDoc`'s overload resolution keys off `DocumentReference`'s
73
+ // `DbModelType` param, which defaults to the generic `DocumentData` when a
74
+ // caller only supplies `AppModelType` (as every hook here does) — that
75
+ // makes TS widen `UpdateData<T>` for unconstrained `T` and reject the
76
+ // match. Pin the call to the single-overload signature actually used.
77
+ const typedUpdateDoc = updateDoc;
78
+ export function useUpdateDocCallback(ref) {
79
+ return useCallback((data)=>typedUpdateDoc(ref, stampUpdatedAt(dropUnpersistedFields(data))), [
80
+ ref
81
+ ]);
82
+ }
83
+ export function useSetDocCallback(ref) {
84
+ return useCallback((data, options)=>setDoc(ref, stampUpdatedAt(dropUnpersistedFields(data)), options != null ? options : {}), [
85
+ ref
86
+ ]);
87
+ }
88
+ export function useModifyDocCallback(ref) {
89
+ const updateDocCb = useUpdateDocCallback(ref);
90
+ const setDocCb = useSetDocCallback(ref);
91
+ return useCallback((data, options)=>{
92
+ // SetOptions semantics (merge/mergeFields) require setDoc: updateDoc
93
+ // ignores them and, critically, bypasses the ref's withConverter
94
+ // serialization (e.g. screen-version node compression) — and with it
95
+ // EVERY strip the converter performs, including `$id` (AGL-1429).
96
+ //
97
+ // Nothing downstream of this line may therefore be relied on to clean
98
+ // a payload. Anything that must hold for both branches belongs above
99
+ // it, which is where `dropUnpersistedFields` now runs.
100
+ const shouldSet = (options == null ? void 0 : options.shouldSet) || options && 'merge' in options || options && 'mergeFields' in options;
101
+ if (shouldSet) return setDocCb(data, options);
102
+ return updateDocCb(data);
103
+ }, [
104
+ updateDocCb,
105
+ setDocCb
106
+ ]);
107
+ }
108
+ export default useModifyDocCallback;
109
+
110
+ //# sourceMappingURL=use-modify-doc-callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/helpers/use-modify-doc-callback.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 { HOST_UNPERSISTED_FIELDS, ORG_UNPERSISTED_FIELDS } from '@aglyn/aglyn'\nimport {\n type DocumentReference,\n serverTimestamp,\n setDoc,\n type SetOptions,\n type UpdateData,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback } from 'react'\n\n/**\n * Every UI write stamps `updatedAt` (AGL-455) — the \"last updated\" columns\n * read it, and callers historically forgot it. Spread order lets a caller's\n * explicit `updatedAt` win (backfills, imports).\n */\nfunction stampUpdatedAt<D extends object>(data: D): D {\n return { updatedAt: serverTimestamp(), ...data }\n}\n\n/**\n * Keys that are never document fields, taken from where that is written down\n * (AGL-1429). `HOST_UNPERSISTED_FIELDS` and `ORG_UNPERSISTED_FIELDS` each\n * declare the synthetic `$id` the readers attach; the INTERSECTION is used\n * deliberately, so this generic boundary only ever strips a key that every\n * declaration agrees is unpersisted. A key added to one map alone is not\n * silently stripped from the other collection's writes — the spec's drift\n * assertion fires instead, and someone decides.\n */\n/*\n * Resolved on first use, not at module scope. Computing it eagerly made this\n * module — and therefore every consumer of the write callbacks — fail to LOAD\n * in any suite that mocks the `@aglyn/aglyn` barrel without re-exporting both\n * maps, which two console specs do. A write-boundary guard that cannot be\n * imported is worse than the leak it prevents.\n */\nlet unpersistedFields: readonly string[] | undefined\n\nfunction getUnpersistedFields(): readonly string[] {\n if (unpersistedFields) return unpersistedFields\n const host = HOST_UNPERSISTED_FIELDS ?? {}\n const org = ORG_UNPERSISTED_FIELDS ?? {}\n unpersistedFields = Object.freeze(\n Object.keys(host).filter((key) => key in org),\n )\n return unpersistedFields\n}\n\n/**\n * Drop the reader-injected keys before the payload leaves for Firestore.\n *\n * This has to happen here rather than in the refs' converters, which is where\n * it used to live alone (`503f197ca` added the `$id` strip to `useHostRef`'s\n * `toFirestore`). `updateDoc` never applies a converter — `setDoc` calls\n * `applyFirestoreDataConverter` and `updateDoc` does not — so a converter\n * strip is a guarantee about ONE of the two SDK calls, and which one a write\n * gets is decided by whether its caller happened to pass `merge`. That is how\n * `hosts/-MtN17_cpfPPLwWjE6z4` came to hold a persisted `$id` (AGL-1423).\n *\n * Only exact top-level keys are removed. Dotted `updateDoc` paths address\n * nested maps, and besigner node maps legitimately store a `$id` per node —\n * matching those would be the same over-broad scan AGL-1423 warned about.\n */\nfunction dropUnpersistedFields<D extends object>(data: D): D {\n const keys = getUnpersistedFields()\n if (!keys.some((key) => key in data)) return data\n const rest = { ...data } as Record<string, unknown>\n for (const key of keys) delete rest[key]\n return rest as D\n}\n\n\nexport type UpdateDocCallback<T> = (data: UpdateData<T>) => Promise<void>\nexport type SetDocCallback<T> = (\n data: Partial<T>,\n options?: SetOptions,\n) => Promise<void>\nexport type ModifyDocOptions = SetOptions & { shouldSet?: boolean }\nexport type ModifyDocCallback<T> = (\n data: UpdateData<T> | Partial<T>,\n options?: ModifyDocOptions,\n) => Promise<void>\n\n// `updateDoc`'s overload resolution keys off `DocumentReference`'s\n// `DbModelType` param, which defaults to the generic `DocumentData` when a\n// caller only supplies `AppModelType` (as every hook here does) — that\n// makes TS widen `UpdateData<T>` for unconstrained `T` and reject the\n// match. Pin the call to the single-overload signature actually used.\nconst typedUpdateDoc = updateDoc as <T>(\n ref: DocumentReference<T>,\n data: UpdateData<T>,\n) => Promise<void>\n\nexport function useUpdateDocCallback<T>(\n ref: DocumentReference<T>,\n): UpdateDocCallback<T> {\n return useCallback(\n (data: UpdateData<T>) =>\n typedUpdateDoc(\n ref,\n stampUpdatedAt(dropUnpersistedFields(data as object)) as UpdateData<T>,\n ),\n [ref],\n )\n}\n\nexport function useSetDocCallback<T>(\n ref: DocumentReference<T>,\n): SetDocCallback<T> {\n return useCallback(\n (data: Partial<T>, options?: SetOptions) =>\n setDoc(\n ref,\n stampUpdatedAt(dropUnpersistedFields(data as object)) as Partial<T>,\n options ?? {},\n ),\n [ref],\n )\n}\n\nexport function useModifyDocCallback<T>(\n ref: DocumentReference<T>,\n): ModifyDocCallback<T> {\n const updateDocCb = useUpdateDocCallback(ref)\n const setDocCb = useSetDocCallback(ref)\n return useCallback(\n (data: UpdateData<T> | Partial<T>, options?: ModifyDocOptions) => {\n // SetOptions semantics (merge/mergeFields) require setDoc: updateDoc\n // ignores them and, critically, bypasses the ref's withConverter\n // serialization (e.g. screen-version node compression) — and with it\n // EVERY strip the converter performs, including `$id` (AGL-1429).\n //\n // Nothing downstream of this line may therefore be relied on to clean\n // a payload. Anything that must hold for both branches belongs above\n // it, which is where `dropUnpersistedFields` now runs.\n const shouldSet =\n options?.shouldSet ||\n (options && 'merge' in options) ||\n (options && 'mergeFields' in options)\n if (shouldSet) return setDocCb(data as Partial<T>, options)\n return updateDocCb(data as UpdateData<T>)\n },\n [updateDocCb, setDocCb],\n )\n}\n\n\nexport type UseModifyDocCallback<T> = typeof useModifyDocCallback<T>\n\nexport default useModifyDocCallback\n"],"names":["HOST_UNPERSISTED_FIELDS","ORG_UNPERSISTED_FIELDS","serverTimestamp","setDoc","updateDoc","useCallback","stampUpdatedAt","data","updatedAt","unpersistedFields","getUnpersistedFields","host","org","Object","freeze","keys","filter","key","dropUnpersistedFields","some","rest","typedUpdateDoc","useUpdateDocCallback","ref","useSetDocCallback","options","useModifyDocCallback","updateDocCb","setDocCb","shouldSet"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,uBAAuB,EAAEC,sBAAsB,QAAQ,eAAc;AAC9E,SAEEC,eAAe,EACfC,MAAM,EAGNC,SAAS,QACJ,qBAAoB;AAC3B,SAASC,WAAW,QAAQ,QAAO;AAEnC;;;;CAIC,GACD,SAASC,eAAiCC,IAAO;IAC/C,OAAO;QAAEC,WAAWN;OAAsBK;AAC5C;AAEA;;;;;;;;CAQC,GACD;;;;;;CAMC,GACD,IAAIE;AAEJ,SAASC;IACP,IAAID,mBAAmB,OAAOA;IAC9B,MAAME,OAAOX,kCAAAA,0BAA2B,CAAC;IACzC,MAAMY,MAAMX,iCAAAA,yBAA0B,CAAC;IACvCQ,oBAAoBI,OAAOC,MAAM,CAC/BD,OAAOE,IAAI,CAACJ,MAAMK,MAAM,CAAC,CAACC,MAAQA,OAAOL;IAE3C,OAAOH;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASS,sBAAwCX,IAAO;IACtD,MAAMQ,OAAOL;IACb,IAAI,CAACK,KAAKI,IAAI,CAAC,CAACF,MAAQA,OAAOV,OAAO,OAAOA;IAC7C,MAAMa,OAAO,aAAKb;IAClB,KAAK,MAAMU,OAAOF,KAAM,OAAOK,IAAI,CAACH,IAAI;IACxC,OAAOG;AACT;AAcA,mEAAmE;AACnE,2EAA2E;AAC3E,uEAAuE;AACvE,sEAAsE;AACtE,sEAAsE;AACtE,MAAMC,iBAAiBjB;AAKvB,OAAO,SAASkB,qBACdC,GAAyB;IAEzB,OAAOlB,YACL,CAACE,OACCc,eACEE,KACAjB,eAAeY,sBAAsBX,SAEzC;QAACgB;KAAI;AAET;AAEA,OAAO,SAASC,kBACdD,GAAyB;IAEzB,OAAOlB,YACL,CAACE,MAAkBkB,UACjBtB,OACEoB,KACAjB,eAAeY,sBAAsBX,QACrCkB,kBAAAA,UAAW,CAAC,IAEhB;QAACF;KAAI;AAET;AAEA,OAAO,SAASG,qBACdH,GAAyB;IAEzB,MAAMI,cAAcL,qBAAqBC;IACzC,MAAMK,WAAWJ,kBAAkBD;IACnC,OAAOlB,YACL,CAACE,MAAkCkB;QACjC,qEAAqE;QACrE,iEAAiE;QACjE,qEAAqE;QACrE,kEAAkE;QAClE,EAAE;QACF,sEAAsE;QACtE,qEAAqE;QACrE,uDAAuD;QACvD,MAAMI,YACJJ,CAAAA,2BAAAA,QAASI,SAAS,KACjBJ,WAAW,WAAWA,WACtBA,WAAW,iBAAiBA;QAC/B,IAAII,WAAW,OAAOD,SAASrB,MAAoBkB;QACnD,OAAOE,YAAYpB;IACrB,GACA;QAACoB;QAAaC;KAAS;AAE3B;AAKA,eAAeF,qBAAoB"}
@@ -0,0 +1,125 @@
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 CollectionReference, type Query } from 'firebase/firestore';
18
+ /**
19
+ * ONE ordering decision for the console lists a plugin renders (AGL-2501).
20
+ *
21
+ * ## What an unordered `limit()` actually answers
22
+ *
23
+ * Firestore answers a capped query with no `orderBy` in DOCUMENT-ID order, and
24
+ * every collection these cards read is keyed by a generated `createResourceUid`
25
+ * or by `add()`. So `limit(100)` is not "the first hundred variables" — it is a
26
+ * pseudo-random hundred, and the rows past it are not merely unrendered, they
27
+ * are UNREACHABLE, because nothing shows them and nothing asks for more. A
28
+ * client `.sort()` over that sample is what hides it: the rows on screen run in
29
+ * a believable order, they are simply the wrong rows, and the ones missing
30
+ * leave no gap to notice.
31
+ *
32
+ * The console's site-artifact lists reached this answer first
33
+ * (`hostArtifactQuery`) and could not share it: an app cannot be imported from
34
+ * a library, so every plugin card faced the same question alone and eleven of
35
+ * them answered it by not ordering at all. This is that decision, where a
36
+ * plugin can ask for it.
37
+ *
38
+ * ## Why the walk orders on the document ID, and not on a field
39
+ *
40
+ * The obvious fix is `orderBy` on the field the list is sorted by — `name` for
41
+ * most of these. It cannot be used. `orderBy` matches only documents that HAVE
42
+ * the field, so ordering on one that any writer omits does not mis-order the
43
+ * list, it HIDES rows from it: a worse failure than the one being fixed, and a
44
+ * silent one.
45
+ *
46
+ * Every collection these cards read has such a writer:
47
+ *
48
+ * * `/api/hosts/resources` stores an ALLOW-LIST of fields and validates none
49
+ * of them for presence, so a create that sends no name produces a document
50
+ * with no `name` at all.
51
+ * * `/api/hosts/import` copies only the keys a bundle actually carries through
52
+ * `IMPORTABLE_FIELDS` — `variables`, `functions`, `workflows` and `actions`
53
+ * are all in it — so a restored document carries a name only if the exported
54
+ * one had it.
55
+ * * `createdAt`/`updatedAt` are no better: the resources route stamps them
56
+ * server-side and an import's `cleanDoc` stamps `updatedAt` only.
57
+ *
58
+ * A document's NAME is not a field and cannot be absent, so ordering on it
59
+ * drops nothing and the walk is TOTAL: every row is reachable by paging, which
60
+ * is the property the old queries lacked. It is not insertion order — the ids
61
+ * are random — so no list built on it may claim to be in one. What it is is
62
+ * stable, complete, and the same on every load.
63
+ *
64
+ * ## Callers must not re-sort the page
65
+ *
66
+ * Re-sorting a window of an id-ordered walk by name is the same lie the old
67
+ * code told: rows would run in one order within a page and another across
68
+ * pages, and the first page would still not be the alphabetical first page. A
69
+ * caller that holds a whole CEILING may sort — see `ceilingedWindow` — because
70
+ * it is sorting the entire collection rather than a slice of it.
71
+ *
72
+ * ## Why a `Query` is accepted and not only a `CollectionReference`
73
+ *
74
+ * A list is often a FILTERED collection — one kind of conversion, one channel
75
+ * — and a caller holding equality predicates could not reach this ordering
76
+ * without rebuilding it, which is how a second, unordered `limit()` comes to
77
+ * be written beside the one this module exists to replace. `query()` composes
78
+ * onto either, and a `CollectionReference` IS a `Query`, so widening the
79
+ * parameter adds no behavior: the walk is still ordered on the document name
80
+ * and is still total over the matching documents.
81
+ *
82
+ * Equality predicates only. A range filter or a second ordering would fight
83
+ * the `orderBy(documentId())` applied here — Firestore requires the range
84
+ * field to be ordered first — and such a query fails loudly at the listener
85
+ * rather than quietly returning the wrong rows.
86
+ */
87
+ export declare function collectionPage(ref: CollectionReference | Query, pageLimit: number): Query;
88
+ /**
89
+ * The same ordering, for a list that is BOUNDED rather than paged.
90
+ *
91
+ * Asks for one document more than the ceiling, which is what turns "there is
92
+ * more than this" into a fact. Pair it with `ceilingedWindow` to drop the probe
93
+ * row before rendering.
94
+ */
95
+ export declare function collectionCeiling(ref: CollectionReference | Query, ceiling: number): Query;
96
+ /** A ceilinged read, and whether the ceiling actually bit. */
97
+ export interface CeilingedWindow<T> {
98
+ /** At most `ceiling` rows — the probe is never among them. */
99
+ rows: T[];
100
+ /** The collection holds MORE than the ceiling. A fact, not an estimate. */
101
+ truncated: boolean;
102
+ }
103
+ /**
104
+ * A read that is bounded but not paged, and can say when it is short.
105
+ *
106
+ * Some lists cannot be windowed by document and must still be bounded: a
107
+ * hierarchy sliced by row separates a child from its parent, a bundle
108
+ * straddling a page renders twice and partial each time, a precedence list
109
+ * paged by ten cannot swap its tenth row with its eleventh, and a table
110
+ * assembled from two collections cannot dedupe across a page boundary.
111
+ *
112
+ * A bare ceiling is silent, which is the half that matters. Every consequence
113
+ * on those surfaces is computed from what was read, so a collection above the
114
+ * ceiling is drawn as a whole one with nothing to distinguish "there is no such
115
+ * row" from "this card did not read it".
116
+ *
117
+ * Asking for ONE document more than the ceiling turns that into a fact for the
118
+ * price of a single read. The probe is never handed on: a caller that rendered
119
+ * `ceiling + 1` rows would be describing a window it did not draw.
120
+ *
121
+ * `hasMore` in `usePagedCollection` is the same trick for a paged list, and for
122
+ * the same reason — a comparison against the cap is wrong exactly when the
123
+ * count is an even multiple of it.
124
+ */
125
+ export declare function ceilingedWindow<T>(read: readonly T[] | undefined, ceiling: number): CeilingedWindow<T>;