@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,42 @@
|
|
|
1
|
+
import { type DocumentData, type DocumentReference, type FirestoreError } from 'firebase/firestore';
|
|
2
|
+
import { type DependencyList } from 'react';
|
|
3
|
+
export type FirestoreDocStatus = 'loading' | 'success' | 'error';
|
|
4
|
+
export interface UseFirestoreDocOptions {
|
|
5
|
+
idField?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UseFirestoreDocResult<T> {
|
|
8
|
+
data: T | undefined;
|
|
9
|
+
status: FirestoreDocStatus;
|
|
10
|
+
error: FirestoreError | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* `data` still carries writes this client made that the server has NOT
|
|
13
|
+
* acknowledged. Carried rather than dropped for the same reason as
|
|
14
|
+
* `ObservableStatus.hasPendingWrites` — see that comment (AGL-1262).
|
|
15
|
+
*/
|
|
16
|
+
hasPendingWrites: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* `data` came from the local cache and the server has not confirmed it
|
|
19
|
+
* (AGL-1066). See `ObservableStatus.fromCache` for why this matters and
|
|
20
|
+
* why it is the signal to reach for rather than `staleSession`.
|
|
21
|
+
*/
|
|
22
|
+
fromCache: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The server has REFUSED this listen for longer than the retry budget and
|
|
25
|
+
* no server snapshot has arrived since (AGL-1066) — the reason behind an
|
|
26
|
+
* `error`, not a duplicate of it. See
|
|
27
|
+
* `UseFirestoreCollectionResult.serverDenied`.
|
|
28
|
+
*/
|
|
29
|
+
serverDenied: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Single-document counterpart to `useFirestoreCollection` — see that hook's
|
|
33
|
+
* doc comment for why this exists instead of reactfire's
|
|
34
|
+
* `useFirestoreDocData` (AGL-216/217).
|
|
35
|
+
*
|
|
36
|
+
* `buildRef` is re-invoked from `deps` the same way a `useEffect`/`useMemo`
|
|
37
|
+
* callback would be — pass the same primitive values (ids, uid, firestore)
|
|
38
|
+
* you'd put in a `useMemo` dependency array, not the `DocumentReference`
|
|
39
|
+
* object itself.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useFirestoreDoc<T = DocumentData>(buildRef: () => DocumentReference<DocumentData> | null | undefined, deps: DependencyList, options?: UseFirestoreDocOptions): UseFirestoreDocResult<T>;
|
|
42
|
+
export default useFirestoreDoc;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { onSnapshot } from "firebase/firestore";
|
|
19
|
+
import { useEffect, useRef, useState } from "react";
|
|
20
|
+
import { CONFIRMABLE_LISTEN_OPTIONS } from "./helpers/listen-options.js";
|
|
21
|
+
import { DENIAL_STREAK_TO_REPORT, denialLabelForQuery, reportFirestoreDenial, reportFirestoreServerRead, scheduleRefusedReopen, subscribeFirestoreSessionHeal } from "./firestore-denial-reporter.js";
|
|
22
|
+
const RETRY_DELAY_MS = 400;
|
|
23
|
+
const MAX_RETRIES = 5;
|
|
24
|
+
/**
|
|
25
|
+
* Single-document counterpart to `useFirestoreCollection` — see that hook's
|
|
26
|
+
* doc comment for why this exists instead of reactfire's
|
|
27
|
+
* `useFirestoreDocData` (AGL-216/217).
|
|
28
|
+
*
|
|
29
|
+
* `buildRef` is re-invoked from `deps` the same way a `useEffect`/`useMemo`
|
|
30
|
+
* callback would be — pass the same primitive values (ids, uid, firestore)
|
|
31
|
+
* you'd put in a `useMemo` dependency array, not the `DocumentReference`
|
|
32
|
+
* object itself.
|
|
33
|
+
*/ export function useFirestoreDoc(buildRef, deps, options = {}) {
|
|
34
|
+
const [data, setData] = useState(undefined);
|
|
35
|
+
const [status, setStatus] = useState('loading');
|
|
36
|
+
const [error, setError] = useState(undefined);
|
|
37
|
+
const [hasPendingWrites, setHasPendingWrites] = useState(false);
|
|
38
|
+
// Un-confirmed until a snapshot says otherwise: a hook that has not heard
|
|
39
|
+
// from the server yet must not read as server-confirmed.
|
|
40
|
+
const [fromCache, setFromCache] = useState(true);
|
|
41
|
+
const [serverDenied, setServerDenied] = useState(false);
|
|
42
|
+
const buildRefRef = useRef(buildRef);
|
|
43
|
+
buildRefRef.current = buildRef;
|
|
44
|
+
const idField = options.idField;
|
|
45
|
+
useEffect(()=>{
|
|
46
|
+
setStatus('loading');
|
|
47
|
+
setError(undefined);
|
|
48
|
+
// Clear on every dep change (AGL-591): a scope move between two live
|
|
49
|
+
// refs must not keep showing the previous doc until the new snapshot
|
|
50
|
+
// arrives. Mirrors use-firestore-collection.
|
|
51
|
+
setData(undefined);
|
|
52
|
+
setHasPendingWrites(false);
|
|
53
|
+
setFromCache(true);
|
|
54
|
+
setServerDenied(false);
|
|
55
|
+
const ref = buildRefRef.current();
|
|
56
|
+
if (!ref) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
let unsubscribe = null;
|
|
61
|
+
let timer = null;
|
|
62
|
+
/** Cancels a pending reopen on the refused cadence (AGL-2944). */ let cancelRefusedReopen = null;
|
|
63
|
+
let attempt = 0;
|
|
64
|
+
// Refusals since the last SERVER snapshot (AGL-1066) — see the note on
|
|
65
|
+
// `DENIAL_STREAK_TO_REPORT` for why this is not `attempt`.
|
|
66
|
+
let deniedStreak = 0;
|
|
67
|
+
/** Epoch ms of the current streak's first refusal (AGL-1440). */ let deniedStreakStartedAt = 0;
|
|
68
|
+
let denialReported = false;
|
|
69
|
+
/**
|
|
70
|
+
* The retry budget is spent and the server's last word was a refusal —
|
|
71
|
+
* what keeps `status: 'error'` from being undone by the cached emission
|
|
72
|
+
* that precedes every reopened refusal (AGL-1066). See the longer note
|
|
73
|
+
* on the same variable in `use-firestore-collection`.
|
|
74
|
+
*/ let terminal = false;
|
|
75
|
+
const denialLabel = denialLabelForQuery(ref);
|
|
76
|
+
const subscribe = ()=>{
|
|
77
|
+
unsubscribe = onSnapshot(ref, // Without this the cache→server confirmation is never delivered and
|
|
78
|
+
// `fromCache` latches true for the life of the listener, refusing
|
|
79
|
+
// every guarded save on the page (AGL-2486). See `listen-options.ts`.
|
|
80
|
+
CONFIRMABLE_LISTEN_OPTIONS, (snapshot)=>{
|
|
81
|
+
if (cancelled) return;
|
|
82
|
+
if (!terminal) {
|
|
83
|
+
setStatus('success');
|
|
84
|
+
setError(undefined);
|
|
85
|
+
}
|
|
86
|
+
setHasPendingWrites(snapshot.metadata.hasPendingWrites);
|
|
87
|
+
setFromCache(snapshot.metadata.fromCache);
|
|
88
|
+
// Only the SERVER answering is evidence the session can read — and
|
|
89
|
+
// the only thing that refunds the retry budget (AGL-1066). See the
|
|
90
|
+
// long note on the same gate in `use-firestore-collection`.
|
|
91
|
+
if (!snapshot.metadata.fromCache) {
|
|
92
|
+
attempt = 0;
|
|
93
|
+
terminal = false;
|
|
94
|
+
setStatus('success');
|
|
95
|
+
setError(undefined);
|
|
96
|
+
deniedStreak = 0;
|
|
97
|
+
denialReported = false;
|
|
98
|
+
setServerDenied(false);
|
|
99
|
+
// Proof the session can read — stronger than any accumulated
|
|
100
|
+
// denial, and what stops a scoped collaborator's by-design
|
|
101
|
+
// denials from ever adding up to a re-auth prompt.
|
|
102
|
+
reportFirestoreServerRead();
|
|
103
|
+
} else if (deniedStreak === 0) {
|
|
104
|
+
// Cached, but nothing has been refused since the server last
|
|
105
|
+
// answered — not the AGL-1066 fault, so the budget is refunded
|
|
106
|
+
// exactly as it always was. This is what keeps offline inert:
|
|
107
|
+
// `unavailable` never enters `deniedStreak`.
|
|
108
|
+
attempt = 0;
|
|
109
|
+
}
|
|
110
|
+
if (!snapshot.exists()) {
|
|
111
|
+
setData(undefined);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const value = _extends({}, snapshot.data());
|
|
115
|
+
if (idField) value[idField] = snapshot.id;
|
|
116
|
+
setData(value);
|
|
117
|
+
}, (err)=>{
|
|
118
|
+
if (cancelled) return;
|
|
119
|
+
unsubscribe == null ? void 0 : unsubscribe();
|
|
120
|
+
// Say so once per outage, and only for a refusal — a lost network
|
|
121
|
+
// produces no error callback at all, so this cannot fire offline.
|
|
122
|
+
if ((err == null ? void 0 : err.code) === 'permission-denied') {
|
|
123
|
+
deniedStreak += 1;
|
|
124
|
+
if (deniedStreak === 1) deniedStreakStartedAt = Date.now();
|
|
125
|
+
if (deniedStreak >= DENIAL_STREAK_TO_REPORT) {
|
|
126
|
+
setServerDenied(true);
|
|
127
|
+
if (!denialReported) {
|
|
128
|
+
denialReported = true;
|
|
129
|
+
reportFirestoreDenial(denialLabel);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (attempt < MAX_RETRIES) {
|
|
134
|
+
attempt += 1;
|
|
135
|
+
if (deniedStreak > MAX_RETRIES) {
|
|
136
|
+
cancelRefusedReopen = scheduleRefusedReopen(subscribe, deniedStreakStartedAt);
|
|
137
|
+
} else {
|
|
138
|
+
timer = setTimeout(subscribe, RETRY_DELAY_MS);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
terminal = true;
|
|
142
|
+
setStatus('error');
|
|
143
|
+
setError(err);
|
|
144
|
+
// A refusal streak keeps a slow road back for a recovery nobody
|
|
145
|
+
// announced — see the same branch in `use-firestore-collection`.
|
|
146
|
+
// The cadence splits on session-vs-ref evidence (AGL-1440), grows
|
|
147
|
+
// with the streak's age (AGL-2945) and waits while the tab is
|
|
148
|
+
// hidden (AGL-2944).
|
|
149
|
+
if (deniedStreak > MAX_RETRIES) {
|
|
150
|
+
cancelRefusedReopen = scheduleRefusedReopen(subscribe, deniedStreakStartedAt);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
subscribe();
|
|
156
|
+
// Reopen when the session heals — see the long note on the same
|
|
157
|
+
// subscription in `use-firestore-collection` (AGL-1066). Gated on this
|
|
158
|
+
// listen actually being refused, so a healthy one ignores the broadcast.
|
|
159
|
+
const unsubscribeHeal = subscribeFirestoreSessionHeal(()=>{
|
|
160
|
+
if (cancelled || deniedStreak === 0) return;
|
|
161
|
+
if (timer) {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
timer = null;
|
|
164
|
+
}
|
|
165
|
+
cancelRefusedReopen == null ? void 0 : cancelRefusedReopen();
|
|
166
|
+
cancelRefusedReopen = null;
|
|
167
|
+
unsubscribe == null ? void 0 : unsubscribe();
|
|
168
|
+
attempt = 0;
|
|
169
|
+
denialReported = false;
|
|
170
|
+
subscribe();
|
|
171
|
+
});
|
|
172
|
+
return ()=>{
|
|
173
|
+
cancelled = true;
|
|
174
|
+
unsubscribeHeal();
|
|
175
|
+
if (timer) clearTimeout(timer);
|
|
176
|
+
cancelRefusedReopen == null ? void 0 : cancelRefusedReopen();
|
|
177
|
+
unsubscribe == null ? void 0 : unsubscribe();
|
|
178
|
+
};
|
|
179
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
180
|
+
}, deps);
|
|
181
|
+
return {
|
|
182
|
+
data,
|
|
183
|
+
status,
|
|
184
|
+
error,
|
|
185
|
+
hasPendingWrites,
|
|
186
|
+
fromCache,
|
|
187
|
+
serverDenied
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export default useFirestoreDoc;
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=use-firestore-doc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-firestore-doc.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n onSnapshot,\n type DocumentData,\n type DocumentReference,\n type FirestoreError,\n} from 'firebase/firestore'\nimport { useEffect, useRef, useState, type DependencyList } from 'react'\nimport { CONFIRMABLE_LISTEN_OPTIONS } from './helpers/listen-options'\nimport {\n DENIAL_STREAK_TO_REPORT,\n denialLabelForQuery,\n reportFirestoreDenial,\n reportFirestoreServerRead,\n scheduleRefusedReopen,\n subscribeFirestoreSessionHeal,\n} from './firestore-denial-reporter'\n\nconst RETRY_DELAY_MS = 400\nconst MAX_RETRIES = 5\n// The refused cadence is shared policy — see `scheduleRefusedReopen` in\n// `firestore-denial-reporter.ts` (AGL-1066, AGL-1440, AGL-2944, AGL-2945).\n\nexport type FirestoreDocStatus = 'loading' | 'success' | 'error'\n\nexport interface UseFirestoreDocOptions {\n idField?: string\n}\n\nexport interface UseFirestoreDocResult<T> {\n data: T | undefined\n status: FirestoreDocStatus\n error: FirestoreError | undefined\n /**\n * `data` still carries writes this client made that the server has NOT\n * acknowledged. Carried rather than dropped for the same reason as\n * `ObservableStatus.hasPendingWrites` — see that comment (AGL-1262).\n */\n hasPendingWrites: boolean\n /**\n * `data` came from the local cache and the server has not confirmed it\n * (AGL-1066). See `ObservableStatus.fromCache` for why this matters and\n * why it is the signal to reach for rather than `staleSession`.\n */\n fromCache: boolean\n /**\n * The server has REFUSED this listen for longer than the retry budget and\n * no server snapshot has arrived since (AGL-1066) — the reason behind an\n * `error`, not a duplicate of it. See\n * `UseFirestoreCollectionResult.serverDenied`.\n */\n serverDenied: boolean\n}\n\n/**\n * Single-document counterpart to `useFirestoreCollection` — see that hook's\n * doc comment for why this exists instead of reactfire's\n * `useFirestoreDocData` (AGL-216/217).\n *\n * `buildRef` is re-invoked from `deps` the same way a `useEffect`/`useMemo`\n * callback would be — pass the same primitive values (ids, uid, firestore)\n * you'd put in a `useMemo` dependency array, not the `DocumentReference`\n * object itself.\n */\nexport function useFirestoreDoc<T = DocumentData>(\n buildRef: () => DocumentReference<DocumentData> | null | undefined,\n deps: DependencyList,\n options: UseFirestoreDocOptions = {},\n): UseFirestoreDocResult<T> {\n const [data, setData] = useState<T | undefined>(undefined)\n const [status, setStatus] = useState<FirestoreDocStatus>('loading')\n const [error, setError] = useState<FirestoreError | undefined>(undefined)\n const [hasPendingWrites, setHasPendingWrites] = useState(false)\n // Un-confirmed until a snapshot says otherwise: a hook that has not heard\n // from the server yet must not read as server-confirmed.\n const [fromCache, setFromCache] = useState(true)\n const [serverDenied, setServerDenied] = useState(false)\n const buildRefRef = useRef(buildRef)\n buildRefRef.current = buildRef\n const idField = options.idField\n\n useEffect(() => {\n setStatus('loading')\n setError(undefined)\n // Clear on every dep change (AGL-591): a scope move between two live\n // refs must not keep showing the previous doc until the new snapshot\n // arrives. Mirrors use-firestore-collection.\n setData(undefined)\n setHasPendingWrites(false)\n setFromCache(true)\n setServerDenied(false)\n\n const ref = buildRefRef.current()\n if (!ref) {\n return\n }\n\n let cancelled = false\n let unsubscribe: (() => void) | null = null\n let timer: ReturnType<typeof setTimeout> | null = null\n /** Cancels a pending reopen on the refused cadence (AGL-2944). */\n let cancelRefusedReopen: (() => void) | null = null\n let attempt = 0\n // Refusals since the last SERVER snapshot (AGL-1066) — see the note on\n // `DENIAL_STREAK_TO_REPORT` for why this is not `attempt`.\n let deniedStreak = 0\n /** Epoch ms of the current streak's first refusal (AGL-1440). */\n let deniedStreakStartedAt = 0\n let denialReported = false\n /**\n * The retry budget is spent and the 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 const denialLabel = denialLabelForQuery(ref)\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 if (!terminal) {\n setStatus('success')\n setError(undefined)\n }\n setHasPendingWrites(snapshot.metadata.hasPendingWrites)\n setFromCache(snapshot.metadata.fromCache)\n // Only the SERVER answering is evidence the session can read — and\n // the only thing that refunds the retry budget (AGL-1066). See the\n // long note on the same gate in `use-firestore-collection`.\n if (!snapshot.metadata.fromCache) {\n attempt = 0\n terminal = false\n setStatus('success')\n setError(undefined)\n deniedStreak = 0\n denialReported = false\n setServerDenied(false)\n // Proof the session can read — stronger than any accumulated\n // denial, and what stops a scoped collaborator's by-design\n // denials from ever adding up to a re-auth prompt.\n reportFirestoreServerRead()\n } else if (deniedStreak === 0) {\n // Cached, but nothing has been refused since the server last\n // answered — 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 (!snapshot.exists()) {\n setData(undefined)\n return\n }\n const value = { ...snapshot.data() } as Record<string, unknown>\n if (idField) value[idField] = snapshot.id\n setData(value as T)\n },\n (err) => {\n if (cancelled) return\n unsubscribe?.()\n // Say so once per outage, and only for a refusal — a lost network\n // produces no error callback at all, so this cannot fire offline.\n if (err?.code === 'permission-denied') {\n deniedStreak += 1\n if (deniedStreak === 1) deniedStreakStartedAt = Date.now()\n if (deniedStreak >= DENIAL_STREAK_TO_REPORT) {\n setServerDenied(true)\n if (!denialReported) {\n denialReported = true\n reportFirestoreDenial(denialLabel)\n }\n }\n }\n if (attempt < MAX_RETRIES) {\n attempt += 1\n if (deniedStreak > MAX_RETRIES) {\n cancelRefusedReopen = scheduleRefusedReopen(\n subscribe,\n deniedStreakStartedAt,\n )\n } else {\n timer = setTimeout(subscribe, RETRY_DELAY_MS)\n }\n } else {\n terminal = true\n setStatus('error')\n setError(err)\n // 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 // Reopen when the session heals — see the long note on the same\n // subscription in `use-firestore-collection` (AGL-1066). Gated on this\n // listen actually being refused, so a healthy one ignores the broadcast.\n const unsubscribeHeal = subscribeFirestoreSessionHeal(() => {\n if (cancelled || deniedStreak === 0) return\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n cancelRefusedReopen?.()\n cancelRefusedReopen = null\n unsubscribe?.()\n attempt = 0\n denialReported = false\n subscribe()\n })\n\n return () => {\n cancelled = true\n unsubscribeHeal()\n if (timer) clearTimeout(timer)\n cancelRefusedReopen?.()\n unsubscribe?.()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps)\n\n return { data, status, error, hasPendingWrites, fromCache, serverDenied }\n}\n\nexport default useFirestoreDoc\n"],"names":["onSnapshot","useEffect","useRef","useState","CONFIRMABLE_LISTEN_OPTIONS","DENIAL_STREAK_TO_REPORT","denialLabelForQuery","reportFirestoreDenial","reportFirestoreServerRead","scheduleRefusedReopen","subscribeFirestoreSessionHeal","RETRY_DELAY_MS","MAX_RETRIES","useFirestoreDoc","buildRef","deps","options","data","setData","undefined","status","setStatus","error","setError","hasPendingWrites","setHasPendingWrites","fromCache","setFromCache","serverDenied","setServerDenied","buildRefRef","current","idField","ref","cancelled","unsubscribe","timer","cancelRefusedReopen","attempt","deniedStreak","deniedStreakStartedAt","denialReported","terminal","denialLabel","subscribe","snapshot","metadata","exists","value","id","err","code","Date","now","setTimeout","unsubscribeHeal","clearTimeout"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 * as Aglyn from '@aglyn/aglyn';
|
|
18
|
+
import type { DocumentReference } from 'firebase/firestore';
|
|
19
|
+
import { type FirestoreDocOptions } from './firebase/firebase-services';
|
|
20
|
+
/**
|
|
21
|
+
* A form's working version, at
|
|
22
|
+
* `hosts/{hostId}/forms/{formId}/versions/{versionId}`.
|
|
23
|
+
*
|
|
24
|
+
* Compressed at rest, and carrying the same asymmetry a component version
|
|
25
|
+
* does: the PUBLISHED tree lives on the parent `forms/{formId}` document,
|
|
26
|
+
* because a placed form has to resolve on the hot path of a page render and
|
|
27
|
+
* one collection query reaches every form on the site. These documents are
|
|
28
|
+
* editing history — the besigner's draft surface — and nothing renders from
|
|
29
|
+
* them.
|
|
30
|
+
*/
|
|
31
|
+
export declare const useFormVersionRef: ({ hostId, formId, versionId, }: {
|
|
32
|
+
hostId: string;
|
|
33
|
+
formId: string;
|
|
34
|
+
versionId: string;
|
|
35
|
+
}) => DocumentReference<Aglyn.FormVersion>;
|
|
36
|
+
export declare const useFormVersion: (data: {
|
|
37
|
+
hostId: string;
|
|
38
|
+
formId: string;
|
|
39
|
+
versionId: string;
|
|
40
|
+
}, options?: FirestoreDocOptions<Aglyn.FormVersion>) => {
|
|
41
|
+
doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.FormVersion<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
|
|
42
|
+
setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.FormVersion<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
|
|
43
|
+
};
|
|
44
|
+
export default useFormVersion;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/ // A TYPE-only namespace: every `Aglyn.` reference below is a type, and held
|
|
17
|
+
// as a VALUE the namespace pins every module the barrel reaches into the
|
|
18
|
+
// console shell that renders these hooks. `import type` erases instead.
|
|
19
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
20
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
21
|
+
import { compress, decompress } from "@aglyn/aglyn";
|
|
22
|
+
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
23
|
+
import { Bytes, doc } from "firebase/firestore";
|
|
24
|
+
import { useFirestore } from "./firebase/firebase-services.js";
|
|
25
|
+
import useDoc from "./helpers/use-doc.js";
|
|
26
|
+
/**
|
|
27
|
+
* A form's working version, at
|
|
28
|
+
* `hosts/{hostId}/forms/{formId}/versions/{versionId}`.
|
|
29
|
+
*
|
|
30
|
+
* Compressed at rest, and carrying the same asymmetry a component version
|
|
31
|
+
* does: the PUBLISHED tree lives on the parent `forms/{formId}` document,
|
|
32
|
+
* because a placed form has to resolve on the hot path of a page render and
|
|
33
|
+
* one collection query reaches every form on the site. These documents are
|
|
34
|
+
* editing history — the besigner's draft surface — and nothing renders from
|
|
35
|
+
* them.
|
|
36
|
+
*/ export const useFormVersionRef = ({ hostId, formId, versionId })=>{
|
|
37
|
+
const firestore = useFirestore();
|
|
38
|
+
const ref = doc(firestore, 'hosts', hostId, 'forms', formId, 'versions', versionId);
|
|
39
|
+
return ref.withConverter({
|
|
40
|
+
toFirestore (data) {
|
|
41
|
+
const { $id } = data, rest = _object_without_properties_loose(data, [
|
|
42
|
+
"$id"
|
|
43
|
+
]);
|
|
44
|
+
// Only emit `nodes` when the write carries them — see the note in
|
|
45
|
+
// use-screen-version (AGL-1250). A partial write that compressed
|
|
46
|
+
// `undefined` would replace the stored tree with an empty one.
|
|
47
|
+
if ((rest == null ? void 0 : rest.nodes) === undefined) return _extends({}, rest, {
|
|
48
|
+
updatedAt: Timestamp.now()
|
|
49
|
+
});
|
|
50
|
+
const nodes = rest.nodes instanceof Bytes ? rest.nodes : Bytes.fromUint8Array(compress(rest.nodes));
|
|
51
|
+
return _extends({}, rest, {
|
|
52
|
+
nodes,
|
|
53
|
+
updatedAt: Timestamp.now()
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
fromFirestore (snapshot, options) {
|
|
57
|
+
if (!snapshot.exists()) return undefined;
|
|
58
|
+
const data = snapshot.data(options);
|
|
59
|
+
if ((data == null ? void 0 : data.nodes) instanceof Bytes) {
|
|
60
|
+
return _extends({}, data, {
|
|
61
|
+
nodes: decompress(data.nodes)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
export const useFormVersion = (data, options)=>{
|
|
69
|
+
return useDoc(useFormVersionRef(data), options);
|
|
70
|
+
};
|
|
71
|
+
export default useFormVersion;
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=use-form-version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-form-version.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// A TYPE-only namespace: every `Aglyn.` reference below is a type, and held\n// as a VALUE the namespace pins every module the barrel reaches into the\n// console shell that renders these hooks. `import type` erases instead.\nimport type * as Aglyn from '@aglyn/aglyn'\nimport { compress, decompress } from '@aglyn/aglyn'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport type { DocumentReference } from 'firebase/firestore'\nimport { Bytes, doc } from 'firebase/firestore'\nimport {\n useFirestore,\n type FirestoreDocOptions,\n} from './firebase/firebase-services'\nimport useDoc from './helpers/use-doc'\n\n/**\n * A form's working version, at\n * `hosts/{hostId}/forms/{formId}/versions/{versionId}`.\n *\n * Compressed at rest, and carrying the same asymmetry a component version\n * does: the PUBLISHED tree lives on the parent `forms/{formId}` document,\n * because a placed form has to resolve on the hot path of a page render and\n * one collection query reaches every form on the site. These documents are\n * editing history — the besigner's draft surface — and nothing renders from\n * them.\n */\nexport const useFormVersionRef = ({\n hostId,\n formId,\n versionId,\n}: {\n hostId: string\n formId: string\n versionId: string\n}) => {\n const firestore = useFirestore()\n const ref = doc(\n firestore,\n 'hosts',\n hostId,\n 'forms',\n formId,\n 'versions',\n versionId,\n )\n return ref.withConverter({\n toFirestore(data) {\n const { $id, ...rest } = data\n // Only emit `nodes` when the write carries them — see the note in\n // use-screen-version (AGL-1250). A partial write that compressed\n // `undefined` would replace the stored tree with an empty one.\n if (rest?.nodes === undefined)\n return { ...rest, updatedAt: Timestamp.now() }\n const nodes =\n rest.nodes instanceof Bytes\n ? rest.nodes\n : Bytes.fromUint8Array(compress(rest.nodes))\n return { ...rest, nodes, updatedAt: Timestamp.now() }\n },\n fromFirestore(snapshot, options) {\n if (!snapshot.exists()) return undefined\n const data = snapshot.data(options)\n if (data?.nodes instanceof Bytes) {\n return {\n ...data,\n nodes: decompress(data.nodes),\n } as Aglyn.FormVersion\n }\n return data as Aglyn.FormVersion\n },\n }) as DocumentReference<Aglyn.FormVersion>\n}\n\nexport const useFormVersion = (\n data: {\n hostId: string\n formId: string\n versionId: string\n },\n options?: FirestoreDocOptions<Aglyn.FormVersion>,\n) => {\n return useDoc(useFormVersionRef(data), options)\n}\n\nexport default useFormVersion\n"],"names":["compress","decompress","Timestamp","Bytes","doc","useFirestore","useDoc","useFormVersionRef","hostId","formId","versionId","firestore","ref","withConverter","toFirestore","data","$id","rest","nodes","undefined","updatedAt","now","fromUint8Array","fromFirestore","snapshot","options","exists","useFormVersion"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4EAA4E;AAC5E,yEAAyE;AACzE,wEAAwE;;;AAExE,SAASA,QAAQ,EAAEC,UAAU,QAAQ,eAAc;AACnD,SAASC,SAAS,QAAQ,+BAA8B;AAExD,SAASC,KAAK,EAAEC,GAAG,QAAQ,qBAAoB;AAC/C,SACEC,YAAY,QAEP,kCAA8B;AACrC,OAAOC,YAAY,uBAAmB;AAEtC;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,oBAAoB,CAAC,EAChCC,MAAM,EACNC,MAAM,EACNC,SAAS,EAKV;IACC,MAAMC,YAAYN;IAClB,MAAMO,MAAMR,IACVO,WACA,SACAH,QACA,SACAC,QACA,YACAC;IAEF,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAAI;YACd,MAAM,EAAEC,GAAG,EAAW,GAAGD,MAATE,wCAASF;;;YACzB,kEAAkE;YAClE,iEAAiE;YACjE,+DAA+D;YAC/D,IAAIE,CAAAA,wBAAAA,KAAMC,KAAK,MAAKC,WAClB,OAAO,aAAKF;gBAAMG,WAAWlB,UAAUmB,GAAG;;YAC5C,MAAMH,QACJD,KAAKC,KAAK,YAAYf,QAClBc,KAAKC,KAAK,GACVf,MAAMmB,cAAc,CAACtB,SAASiB,KAAKC,KAAK;YAC9C,OAAO,aAAKD;gBAAMC;gBAAOE,WAAWlB,UAAUmB,GAAG;;QACnD;QACAE,eAAcC,QAAQ,EAAEC,OAAO;YAC7B,IAAI,CAACD,SAASE,MAAM,IAAI,OAAOP;YAC/B,MAAMJ,OAAOS,SAAST,IAAI,CAACU;YAC3B,IAAIV,CAAAA,wBAAAA,KAAMG,KAAK,aAAYf,OAAO;gBAChC,OAAO,aACFY;oBACHG,OAAOjB,WAAWc,KAAKG,KAAK;;YAEhC;YACA,OAAOH;QACT;IACF;AACF,EAAC;AAED,OAAO,MAAMY,iBAAiB,CAC5BZ,MAKAU;IAEA,OAAOnB,OAAOC,kBAAkBQ,OAAOU;AACzC,EAAC;AAED,eAAeE,eAAc"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** What an activity entry points at; `id` lets detail pages filter. */
|
|
2
|
+
export interface HostActivityTarget {
|
|
3
|
+
type: 'host' | 'screen' | 'layout' | 'theme' | 'media' | 'content' | 'variable' | 'function' | 'workflow' | 'member' | 'component' | 'template' | 'contact' | 'company' | 'deal' | 'lead';
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Version-addressed targets (screens) record this so the feed can deep-link
|
|
8
|
+
* straight to the exact version's detail view (AGL-812). Written only when
|
|
9
|
+
* truthy, like `id`/`name`.
|
|
10
|
+
*/
|
|
11
|
+
versionId?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* User activity log (AGL-118): fire-and-forget appends to
|
|
15
|
+
* `hosts/{hostId}/activity` from console mutation points. Never throws —
|
|
16
|
+
* an audit miss must not break the edit that triggered it. Covered by the
|
|
17
|
+
* host-admin wildcard rule, so no rules change is needed.
|
|
18
|
+
*
|
|
19
|
+
* `activity` is deliberately absent from the host catch-all's create
|
|
20
|
+
* exclusion list, so a member holding a content role writes here directly.
|
|
21
|
+
* `cloud/rules-tests/firestore-rules.test.mjs` pins that on both sides —
|
|
22
|
+
* the roles that may append and the roles that may not.
|
|
23
|
+
*
|
|
24
|
+
* An append needs an ACTOR, and the rule that admits it needs one too:
|
|
25
|
+
* `canWriteHostContent` resolves the writer's host role out of
|
|
26
|
+
* `memberRoles[request.auth.uid]`, so a request carrying no auth matches no
|
|
27
|
+
* role and is refused. `useUser()` reports `undefined` until
|
|
28
|
+
* `onIdTokenChanged` first fires and `null` once it reports a signed-out
|
|
29
|
+
* tab, and in both states the entry has nobody to attribute — so the append
|
|
30
|
+
* is skipped rather than sent to be denied. An unattributed audit line is
|
|
31
|
+
* not a smaller version of an audit line; it records that something happened
|
|
32
|
+
* and refuses to say who did it, which is the one field the log exists for.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useHostActivityLogger(hostId: string | undefined): (action: string, target: HostActivityTarget) => void;
|
|
35
|
+
export default useHostActivityLogger;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { reportHandledError } from "@aglyn/aglyn/app-utils/error-beacon";
|
|
19
|
+
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
20
|
+
import { addDoc, collection } from "firebase/firestore";
|
|
21
|
+
import { useCallback } from "react";
|
|
22
|
+
import { useFirestore, useUser } from "./firebase/firebase-services.js";
|
|
23
|
+
/** One console line per session, however many appends fail. */ let announced = false;
|
|
24
|
+
/**
|
|
25
|
+
* What a dropped audit append does instead of nothing.
|
|
26
|
+
*
|
|
27
|
+
* The append stays fire-and-forget: an audit miss must never break the edit
|
|
28
|
+
* that triggered it, and a snackbar about the LOG would be a worse answer to
|
|
29
|
+
* "your page saved" than silence. But a rejection with nowhere to go is a
|
|
30
|
+
* feature that can break for months without a single signal, so the failure
|
|
31
|
+
* leaves by two doors that cost the person nothing.
|
|
32
|
+
*
|
|
33
|
+
* The beacon is the durable one — it reaches Cloud Error Reporting, dedupes a
|
|
34
|
+
* failure that repeats on every edit into one report, and caps itself, so a
|
|
35
|
+
* permission denial shows up as a graph instead of a support ticket. The
|
|
36
|
+
* console line is for whoever has devtools open, and fires once: the second
|
|
37
|
+
* failure carries no information the first did not, and a message per edit
|
|
38
|
+
* buries the diagnostics that do.
|
|
39
|
+
*/ function reportDroppedEntry(error) {
|
|
40
|
+
reportHandledError(error, {
|
|
41
|
+
kind: 'host-activity-write'
|
|
42
|
+
});
|
|
43
|
+
if (announced) return;
|
|
44
|
+
announced = true;
|
|
45
|
+
console.error('Activity logging failed; entries from this session are being dropped.', error);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* User activity log (AGL-118): fire-and-forget appends to
|
|
49
|
+
* `hosts/{hostId}/activity` from console mutation points. Never throws —
|
|
50
|
+
* an audit miss must not break the edit that triggered it. Covered by the
|
|
51
|
+
* host-admin wildcard rule, so no rules change is needed.
|
|
52
|
+
*
|
|
53
|
+
* `activity` is deliberately absent from the host catch-all's create
|
|
54
|
+
* exclusion list, so a member holding a content role writes here directly.
|
|
55
|
+
* `cloud/rules-tests/firestore-rules.test.mjs` pins that on both sides —
|
|
56
|
+
* the roles that may append and the roles that may not.
|
|
57
|
+
*
|
|
58
|
+
* An append needs an ACTOR, and the rule that admits it needs one too:
|
|
59
|
+
* `canWriteHostContent` resolves the writer's host role out of
|
|
60
|
+
* `memberRoles[request.auth.uid]`, so a request carrying no auth matches no
|
|
61
|
+
* role and is refused. `useUser()` reports `undefined` until
|
|
62
|
+
* `onIdTokenChanged` first fires and `null` once it reports a signed-out
|
|
63
|
+
* tab, and in both states the entry has nobody to attribute — so the append
|
|
64
|
+
* is skipped rather than sent to be denied. An unattributed audit line is
|
|
65
|
+
* not a smaller version of an audit line; it records that something happened
|
|
66
|
+
* and refuses to say who did it, which is the one field the log exists for.
|
|
67
|
+
*/ export function useHostActivityLogger(hostId) {
|
|
68
|
+
const firestore = useFirestore();
|
|
69
|
+
const { data: user } = useUser();
|
|
70
|
+
return useCallback((action, target)=>{
|
|
71
|
+
var _user_email;
|
|
72
|
+
if (!hostId || !user) return;
|
|
73
|
+
void addDoc(collection(firestore, 'hosts', hostId, 'activity'), {
|
|
74
|
+
actorId: user.uid,
|
|
75
|
+
actorEmail: (_user_email = user.email) != null ? _user_email : null,
|
|
76
|
+
action,
|
|
77
|
+
target: _extends({
|
|
78
|
+
type: target.type
|
|
79
|
+
}, target.id ? {
|
|
80
|
+
id: target.id
|
|
81
|
+
} : {}, target.name ? {
|
|
82
|
+
name: target.name
|
|
83
|
+
} : {}, target.versionId ? {
|
|
84
|
+
versionId: target.versionId
|
|
85
|
+
} : {}),
|
|
86
|
+
createdAt: Timestamp.now()
|
|
87
|
+
}).catch(reportDroppedEntry);
|
|
88
|
+
}, [
|
|
89
|
+
firestore,
|
|
90
|
+
hostId,
|
|
91
|
+
user
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
export default useHostActivityLogger;
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=use-host-activity-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-host-activity-logger.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { reportHandledError } from '@aglyn/aglyn/app-utils/error-beacon'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport { addDoc, collection } from 'firebase/firestore'\nimport { useCallback } from 'react'\nimport { useFirestore, useUser } from './firebase/firebase-services'\n\n/** What an activity entry points at; `id` lets detail pages filter. */\nexport interface HostActivityTarget {\n type:\n | 'host'\n | 'screen'\n | 'layout'\n | 'theme'\n | 'media'\n | 'content'\n | 'variable'\n | 'function'\n | 'workflow'\n | 'member'\n // Standalone editors (AGL-680/681) — both are edited on their own now,\n // so their saves belong in the same log as screens and layouts.\n | 'component'\n | 'template'\n // A person on the CRM record page (AGL-2596): added by hand, edited, or\n // removed from the site. The record is org-shared, but the act happened\n // in THIS site's console and belongs in this site's feed.\n | 'contact'\n // The rest of the CRM (AGL-2622): a company or a deal created or deleted\n // from this site's console — one at a time, or one line per record from\n // the bulk bars (AGL-2621) — and a lead converted into the three. Org\n // data all of them, filed under the site whose console did the work for\n // the reason a contact is.\n | 'company'\n | 'deal'\n | 'lead'\n id?: string\n name?: string\n /**\n * Version-addressed targets (screens) record this so the feed can deep-link\n * straight to the exact version's detail view (AGL-812). Written only when\n * truthy, like `id`/`name`.\n */\n versionId?: string\n}\n\n/** One console line per session, however many appends fail. */\nlet announced = false\n\n/**\n * What a dropped audit append does instead of nothing.\n *\n * The append stays fire-and-forget: an audit miss must never break the edit\n * that triggered it, and a snackbar about the LOG would be a worse answer to\n * \"your page saved\" than silence. But a rejection with nowhere to go is a\n * feature that can break for months without a single signal, so the failure\n * leaves by two doors that cost the person nothing.\n *\n * The beacon is the durable one — it reaches Cloud Error Reporting, dedupes a\n * failure that repeats on every edit into one report, and caps itself, so a\n * permission denial shows up as a graph instead of a support ticket. The\n * console line is for whoever has devtools open, and fires once: the second\n * failure carries no information the first did not, and a message per edit\n * buries the diagnostics that do.\n */\nfunction reportDroppedEntry(error: unknown): void {\n reportHandledError(error, { kind: 'host-activity-write' })\n if (announced) return\n announced = true\n console.error(\n 'Activity logging failed; entries from this session are being dropped.',\n error,\n )\n}\n\n/**\n * User activity log (AGL-118): fire-and-forget appends to\n * `hosts/{hostId}/activity` from console mutation points. Never throws —\n * an audit miss must not break the edit that triggered it. Covered by the\n * host-admin wildcard rule, so no rules change is needed.\n *\n * `activity` is deliberately absent from the host catch-all's create\n * exclusion list, so a member holding a content role writes here directly.\n * `cloud/rules-tests/firestore-rules.test.mjs` pins that on both sides —\n * the roles that may append and the roles that may not.\n *\n * An append needs an ACTOR, and the rule that admits it needs one too:\n * `canWriteHostContent` resolves the writer's host role out of\n * `memberRoles[request.auth.uid]`, so a request carrying no auth matches no\n * role and is refused. `useUser()` reports `undefined` until\n * `onIdTokenChanged` first fires and `null` once it reports a signed-out\n * tab, and in both states the entry has nobody to attribute — so the append\n * is skipped rather than sent to be denied. An unattributed audit line is\n * not a smaller version of an audit line; it records that something happened\n * and refuses to say who did it, which is the one field the log exists for.\n */\nexport function useHostActivityLogger(hostId: string | undefined) {\n const firestore = useFirestore()\n const { data: user } = useUser()\n return useCallback(\n (action: string, target: HostActivityTarget) => {\n if (!hostId || !user) return\n void addDoc(collection(firestore, 'hosts', hostId, 'activity'), {\n actorId: user.uid,\n actorEmail: user.email ?? null,\n action,\n target: {\n type: target.type,\n ...(target.id ? { id: target.id } : {}),\n ...(target.name ? { name: target.name } : {}),\n ...(target.versionId ? { versionId: target.versionId } : {}),\n },\n createdAt: Timestamp.now(),\n }).catch(reportDroppedEntry)\n },\n [firestore, hostId, user],\n )\n}\n\nexport default useHostActivityLogger\n"],"names":["reportHandledError","Timestamp","addDoc","collection","useCallback","useFirestore","useUser","announced","reportDroppedEntry","error","kind","console","useHostActivityLogger","hostId","firestore","data","user","action","target","actorId","uid","actorEmail","email","type","id","name","versionId","createdAt","now","catch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,kBAAkB,QAAQ,sCAAqC;AACxE,SAASC,SAAS,QAAQ,+BAA8B;AACxD,SAASC,MAAM,EAAEC,UAAU,QAAQ,qBAAoB;AACvD,SAASC,WAAW,QAAQ,QAAO;AACnC,SAASC,YAAY,EAAEC,OAAO,QAAQ,kCAA8B;AAyCpE,6DAA6D,GAC7D,IAAIC,YAAY;AAEhB;;;;;;;;;;;;;;;CAeC,GACD,SAASC,mBAAmBC,KAAc;IACxCT,mBAAmBS,OAAO;QAAEC,MAAM;IAAsB;IACxD,IAAIH,WAAW;IACfA,YAAY;IACZI,QAAQF,KAAK,CACX,yEACAA;AAEJ;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASG,sBAAsBC,MAA0B;IAC9D,MAAMC,YAAYT;IAClB,MAAM,EAAEU,MAAMC,IAAI,EAAE,GAAGV;IACvB,OAAOF,YACL,CAACa,QAAgBC;YAIDF;QAHd,IAAI,CAACH,UAAU,CAACG,MAAM;QACtB,KAAKd,OAAOC,WAAWW,WAAW,SAASD,QAAQ,aAAa;YAC9DM,SAASH,KAAKI,GAAG;YACjBC,UAAU,GAAEL,cAAAA,KAAKM,KAAK,YAAVN,cAAc;YAC1BC;YACAC,QAAQ;gBACNK,MAAML,OAAOK,IAAI;eACbL,OAAOM,EAAE,GAAG;gBAAEA,IAAIN,OAAOM,EAAE;YAAC,IAAI,CAAC,GACjCN,OAAOO,IAAI,GAAG;gBAAEA,MAAMP,OAAOO,IAAI;YAAC,IAAI,CAAC,GACvCP,OAAOQ,SAAS,GAAG;gBAAEA,WAAWR,OAAOQ,SAAS;YAAC,IAAI,CAAC;YAE5DC,WAAW1B,UAAU2B,GAAG;QAC1B,GAAGC,KAAK,CAACrB;IACX,GACA;QAACM;QAAWD;QAAQG;KAAK;AAE7B;AAEA,eAAeJ,sBAAqB"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many campaigns a picker offers.
|
|
3
|
+
*
|
|
4
|
+
* The same fifty the campaigns table draws, so a campaign a merchant can see
|
|
5
|
+
* in the list is a campaign they can file a record under. A picker with a
|
|
6
|
+
* larger window would offer campaigns the list they came from does not show;
|
|
7
|
+
* a smaller one would hide campaigns that are plainly there.
|
|
8
|
+
*/
|
|
9
|
+
export declare const HOST_CAMPAIGN_CEILING = 50;
|
|
10
|
+
/** One campaign, as much of it as a picker needs. */
|
|
11
|
+
export interface HostCampaignOption {
|
|
12
|
+
value: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** The campaign window, for a caller that wants to say when it ran. */
|
|
15
|
+
startAtMs?: number | null;
|
|
16
|
+
endAtMs?: number | null;
|
|
17
|
+
}
|
|
18
|
+
export interface HostCampaigns {
|
|
19
|
+
options: HostCampaignOption[];
|
|
20
|
+
/** The site holds more campaigns than the ceiling offers. */
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
/** The read has answered — false while it is still settling or disabled. */
|
|
23
|
+
ready: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* THE SITE'S CAMPAIGNS, for a picker on some other record's page.
|
|
27
|
+
*
|
|
28
|
+
* Lives here rather than in the marketing plugin because the console app may
|
|
29
|
+
* not import a feature plugin — a screen's detail page is an app route — and
|
|
30
|
+
* the form and contact surfaces that need the same list are two other
|
|
31
|
+
* libraries again. One read, one shape, three callers.
|
|
32
|
+
*
|
|
33
|
+
* ## It is OFF unless a caller asks
|
|
34
|
+
*
|
|
35
|
+
* `enabled` defaults to false. The picker this feeds sits on a record's own
|
|
36
|
+
* page beside fields a reader came for; charging every open of a form, a
|
|
37
|
+
* screen or a contact for a hundred campaign documents they may not touch is
|
|
38
|
+
* the read-on-mount this console refuses. The callers turn it on when their
|
|
39
|
+
* editing surface opens.
|
|
40
|
+
*
|
|
41
|
+
* ## Ordered on the document name
|
|
42
|
+
*
|
|
43
|
+
* `collectionCeiling`, for the reason it exists: `startAtMs` and `endAtMs`
|
|
44
|
+
* are both optional on a campaign — an open-ended one carries neither — so
|
|
45
|
+
* ordering on a date would not mis-sort the picker, it would DROP the
|
|
46
|
+
* campaigns that have no dates. The labels are sorted by name here, over the
|
|
47
|
+
* whole ceiling rather than a slice of it, which is the one case sorting a
|
|
48
|
+
* window is honest.
|
|
49
|
+
*/
|
|
50
|
+
export declare function useHostCampaigns(hostId: string | undefined, options?: {
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
}): HostCampaigns;
|
|
53
|
+
export default useHostCampaigns;
|