@aglyn/tenant-feature-instance 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +3 -0
- package/jest.config.d.ts +36 -0
- package/package.json +45 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
- package/src/lib/components/duplicate-resource-dialog.js +217 -0
- package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
- package/src/lib/constants/firebase-config.d.ts +68 -0
- package/src/lib/constants/firebase-config.js +155 -0
- package/src/lib/constants/firebase-config.js.map +1 -0
- package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
- package/src/lib/constants/firebase-emulator-hosts.js +81 -0
- package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
- package/src/lib/constants/mock-data.d.ts +24 -0
- package/src/lib/constants/mock-data.js +49 -0
- package/src/lib/constants/mock-data.js.map +1 -0
- package/src/lib/constants/site-paths.d.ts +47 -0
- package/src/lib/constants/site-paths.js +66 -0
- package/src/lib/constants/site-paths.js.map +1 -0
- package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
- package/src/lib/hooks/firebase/auth-persistence.js +117 -0
- package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
- package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
- package/src/lib/hooks/firebase/firebase-services.js +522 -0
- package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
- package/src/lib/hooks/firebase/firestore-cache.js +152 -0
- package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
- package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
- package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
- package/src/lib/hooks/firestore-denial-reporter.js +293 -0
- package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
- package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
- package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
- package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
- package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
- package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
- package/src/lib/hooks/helpers/listen-options.js +92 -0
- package/src/lib/hooks/helpers/listen-options.js.map +1 -0
- package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
- package/src/lib/hooks/helpers/use-doc.js +213 -0
- package/src/lib/hooks/helpers/use-doc.js.map +1 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
- package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
- package/src/lib/hooks/host-collection-queries.d.ts +125 -0
- package/src/lib/hooks/host-collection-queries.js +126 -0
- package/src/lib/hooks/host-collection-queries.js.map +1 -0
- package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
- package/src/lib/hooks/list-filter-constraints.js +288 -0
- package/src/lib/hooks/list-filter-constraints.js.map +1 -0
- package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
- package/src/lib/hooks/sorted-collection-window.js +150 -0
- package/src/lib/hooks/sorted-collection-window.js.map +1 -0
- package/src/lib/hooks/use-component-version.d.ts +43 -0
- package/src/lib/hooks/use-component-version.js +73 -0
- package/src/lib/hooks/use-component-version.js.map +1 -0
- package/src/lib/hooks/use-component.d.ts +39 -0
- package/src/lib/hooks/use-component.js +55 -0
- package/src/lib/hooks/use-component.js.map +1 -0
- package/src/lib/hooks/use-console-host-route.d.ts +24 -0
- package/src/lib/hooks/use-console-host-route.js +77 -0
- package/src/lib/hooks/use-console-host-route.js.map +1 -0
- package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
- package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
- package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
- package/src/lib/hooks/use-firestore-collection.js +307 -0
- package/src/lib/hooks/use-firestore-collection.js.map +1 -0
- package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
- package/src/lib/hooks/use-firestore-doc.js +192 -0
- package/src/lib/hooks/use-firestore-doc.js.map +1 -0
- package/src/lib/hooks/use-form-version.d.ts +44 -0
- package/src/lib/hooks/use-form-version.js +73 -0
- package/src/lib/hooks/use-form-version.js.map +1 -0
- package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
- package/src/lib/hooks/use-host-activity-logger.js +96 -0
- package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
- package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
- package/src/lib/hooks/use-host-campaigns.js +93 -0
- package/src/lib/hooks/use-host-campaigns.js.map +1 -0
- package/src/lib/hooks/use-host-org-id.d.ts +96 -0
- package/src/lib/hooks/use-host-org-id.js +154 -0
- package/src/lib/hooks/use-host-org-id.js.map +1 -0
- package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
- package/src/lib/hooks/use-host-resource-api.js +60 -0
- package/src/lib/hooks/use-host-resource-api.js.map +1 -0
- package/src/lib/hooks/use-host-template.d.ts +44 -0
- package/src/lib/hooks/use-host-template.js +82 -0
- package/src/lib/hooks/use-host-template.js.map +1 -0
- package/src/lib/hooks/use-host-version-api.d.ts +59 -0
- package/src/lib/hooks/use-host-version-api.js +69 -0
- package/src/lib/hooks/use-host-version-api.js.map +1 -0
- package/src/lib/hooks/use-host.d.ts +101 -0
- package/src/lib/hooks/use-host.js +45 -0
- package/src/lib/hooks/use-host.js.map +1 -0
- package/src/lib/hooks/use-layout-version.d.ts +33 -0
- package/src/lib/hooks/use-layout-version.js +64 -0
- package/src/lib/hooks/use-layout-version.js.map +1 -0
- package/src/lib/hooks/use-layout.d.ts +41 -0
- package/src/lib/hooks/use-layout.js +49 -0
- package/src/lib/hooks/use-layout.js.map +1 -0
- package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
- package/src/lib/hooks/use-live-artifact-count.js +102 -0
- package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +57 -0
- package/src/lib/hooks/use-org-member-options.js +112 -0
- package/src/lib/hooks/use-org-member-options.js.map +1 -0
- package/src/lib/hooks/use-org-plan.d.ts +46 -0
- package/src/lib/hooks/use-org-plan.js +86 -0
- package/src/lib/hooks/use-org-plan.js.map +1 -0
- package/src/lib/hooks/use-paged-collection.d.ts +95 -0
- package/src/lib/hooks/use-paged-collection.js +130 -0
- package/src/lib/hooks/use-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-plugin-config.d.ts +38 -0
- package/src/lib/hooks/use-plugin-config.js +120 -0
- package/src/lib/hooks/use-plugin-config.js.map +1 -0
- package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
- package/src/lib/hooks/use-scope-tokens.js +100 -0
- package/src/lib/hooks/use-scope-tokens.js.map +1 -0
- package/src/lib/hooks/use-screen-version.d.ts +33 -0
- package/src/lib/hooks/use-screen-version.js +65 -0
- package/src/lib/hooks/use-screen-version.js.map +1 -0
- package/src/lib/hooks/use-screen.d.ts +68 -0
- package/src/lib/hooks/use-screen.js +50 -0
- package/src/lib/hooks/use-screen.js.map +1 -0
- package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
- package/src/lib/hooks/use-sending-identity-api.js +95 -0
- package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
- package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
- package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
- package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
- package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
- package/src/lib/hooks/use-switcher-collection.js +275 -0
- package/src/lib/hooks/use-switcher-collection.js.map +1 -0
- package/src/lib/hooks/use-user-name.d.ts +51 -0
- package/src/lib/hooks/use-user-name.js +90 -0
- package/src/lib/hooks/use-user-name.js.map +1 -0
- package/src/lib/hooks/use-user-photo.d.ts +50 -0
- package/src/lib/hooks/use-user-photo.js +87 -0
- package/src/lib/hooks/use-user-photo.js.map +1 -0
- package/src/lib/tenant.d.ts +61 -0
- package/src/lib/tenant.js +62 -0
- package/src/lib/tenant.js.map +1 -0
|
@@ -0,0 +1,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>;
|