@aglyn/tenant-feature-instance 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/jest.config.d.ts +36 -0
  4. package/package.json +45 -0
  5. package/src/index.d.ts +17 -0
  6. package/src/index.js +18 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
  9. package/src/lib/components/duplicate-resource-dialog.js +217 -0
  10. package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
  11. package/src/lib/constants/firebase-config.d.ts +68 -0
  12. package/src/lib/constants/firebase-config.js +155 -0
  13. package/src/lib/constants/firebase-config.js.map +1 -0
  14. package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
  15. package/src/lib/constants/firebase-emulator-hosts.js +81 -0
  16. package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
  17. package/src/lib/constants/mock-data.d.ts +24 -0
  18. package/src/lib/constants/mock-data.js +49 -0
  19. package/src/lib/constants/mock-data.js.map +1 -0
  20. package/src/lib/constants/site-paths.d.ts +47 -0
  21. package/src/lib/constants/site-paths.js +66 -0
  22. package/src/lib/constants/site-paths.js.map +1 -0
  23. package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
  24. package/src/lib/hooks/firebase/auth-persistence.js +117 -0
  25. package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
  26. package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
  27. package/src/lib/hooks/firebase/firebase-services.js +522 -0
  28. package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
  29. package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
  30. package/src/lib/hooks/firebase/firestore-cache.js +152 -0
  31. package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
  32. package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
  33. package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
  34. package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
  35. package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
  36. package/src/lib/hooks/firestore-denial-reporter.js +293 -0
  37. package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
  38. package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
  39. package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
  40. package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
  41. package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
  42. package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
  43. package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
  44. package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
  45. package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
  46. package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
  47. package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
  48. package/src/lib/hooks/helpers/listen-options.js +92 -0
  49. package/src/lib/hooks/helpers/listen-options.js.map +1 -0
  50. package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
  51. package/src/lib/hooks/helpers/use-doc.js +213 -0
  52. package/src/lib/hooks/helpers/use-doc.js.map +1 -0
  53. package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
  54. package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
  55. package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
  56. package/src/lib/hooks/host-collection-queries.d.ts +125 -0
  57. package/src/lib/hooks/host-collection-queries.js +126 -0
  58. package/src/lib/hooks/host-collection-queries.js.map +1 -0
  59. package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
  60. package/src/lib/hooks/list-filter-constraints.js +288 -0
  61. package/src/lib/hooks/list-filter-constraints.js.map +1 -0
  62. package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
  63. package/src/lib/hooks/sorted-collection-window.js +150 -0
  64. package/src/lib/hooks/sorted-collection-window.js.map +1 -0
  65. package/src/lib/hooks/use-component-version.d.ts +43 -0
  66. package/src/lib/hooks/use-component-version.js +73 -0
  67. package/src/lib/hooks/use-component-version.js.map +1 -0
  68. package/src/lib/hooks/use-component.d.ts +39 -0
  69. package/src/lib/hooks/use-component.js +55 -0
  70. package/src/lib/hooks/use-component.js.map +1 -0
  71. package/src/lib/hooks/use-console-host-route.d.ts +24 -0
  72. package/src/lib/hooks/use-console-host-route.js +77 -0
  73. package/src/lib/hooks/use-console-host-route.js.map +1 -0
  74. package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
  75. package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
  76. package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
  77. package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
  78. package/src/lib/hooks/use-firestore-collection.js +307 -0
  79. package/src/lib/hooks/use-firestore-collection.js.map +1 -0
  80. package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
  81. package/src/lib/hooks/use-firestore-doc.js +192 -0
  82. package/src/lib/hooks/use-firestore-doc.js.map +1 -0
  83. package/src/lib/hooks/use-form-version.d.ts +44 -0
  84. package/src/lib/hooks/use-form-version.js +73 -0
  85. package/src/lib/hooks/use-form-version.js.map +1 -0
  86. package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
  87. package/src/lib/hooks/use-host-activity-logger.js +96 -0
  88. package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
  89. package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
  90. package/src/lib/hooks/use-host-campaigns.js +93 -0
  91. package/src/lib/hooks/use-host-campaigns.js.map +1 -0
  92. package/src/lib/hooks/use-host-org-id.d.ts +96 -0
  93. package/src/lib/hooks/use-host-org-id.js +154 -0
  94. package/src/lib/hooks/use-host-org-id.js.map +1 -0
  95. package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
  96. package/src/lib/hooks/use-host-resource-api.js +60 -0
  97. package/src/lib/hooks/use-host-resource-api.js.map +1 -0
  98. package/src/lib/hooks/use-host-template.d.ts +44 -0
  99. package/src/lib/hooks/use-host-template.js +82 -0
  100. package/src/lib/hooks/use-host-template.js.map +1 -0
  101. package/src/lib/hooks/use-host-version-api.d.ts +59 -0
  102. package/src/lib/hooks/use-host-version-api.js +69 -0
  103. package/src/lib/hooks/use-host-version-api.js.map +1 -0
  104. package/src/lib/hooks/use-host.d.ts +101 -0
  105. package/src/lib/hooks/use-host.js +45 -0
  106. package/src/lib/hooks/use-host.js.map +1 -0
  107. package/src/lib/hooks/use-layout-version.d.ts +33 -0
  108. package/src/lib/hooks/use-layout-version.js +64 -0
  109. package/src/lib/hooks/use-layout-version.js.map +1 -0
  110. package/src/lib/hooks/use-layout.d.ts +41 -0
  111. package/src/lib/hooks/use-layout.js +49 -0
  112. package/src/lib/hooks/use-layout.js.map +1 -0
  113. package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
  114. package/src/lib/hooks/use-live-artifact-count.js +102 -0
  115. package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
  116. package/src/lib/hooks/use-org-member-options.d.ts +57 -0
  117. package/src/lib/hooks/use-org-member-options.js +112 -0
  118. package/src/lib/hooks/use-org-member-options.js.map +1 -0
  119. package/src/lib/hooks/use-org-plan.d.ts +46 -0
  120. package/src/lib/hooks/use-org-plan.js +86 -0
  121. package/src/lib/hooks/use-org-plan.js.map +1 -0
  122. package/src/lib/hooks/use-paged-collection.d.ts +95 -0
  123. package/src/lib/hooks/use-paged-collection.js +130 -0
  124. package/src/lib/hooks/use-paged-collection.js.map +1 -0
  125. package/src/lib/hooks/use-plugin-config.d.ts +38 -0
  126. package/src/lib/hooks/use-plugin-config.js +120 -0
  127. package/src/lib/hooks/use-plugin-config.js.map +1 -0
  128. package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
  129. package/src/lib/hooks/use-scope-tokens.js +100 -0
  130. package/src/lib/hooks/use-scope-tokens.js.map +1 -0
  131. package/src/lib/hooks/use-screen-version.d.ts +33 -0
  132. package/src/lib/hooks/use-screen-version.js +65 -0
  133. package/src/lib/hooks/use-screen-version.js.map +1 -0
  134. package/src/lib/hooks/use-screen.d.ts +68 -0
  135. package/src/lib/hooks/use-screen.js +50 -0
  136. package/src/lib/hooks/use-screen.js.map +1 -0
  137. package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
  138. package/src/lib/hooks/use-sending-identity-api.js +95 -0
  139. package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
  140. package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
  141. package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
  142. package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
  143. package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
  144. package/src/lib/hooks/use-switcher-collection.js +275 -0
  145. package/src/lib/hooks/use-switcher-collection.js.map +1 -0
  146. package/src/lib/hooks/use-user-name.d.ts +51 -0
  147. package/src/lib/hooks/use-user-name.js +90 -0
  148. package/src/lib/hooks/use-user-name.js.map +1 -0
  149. package/src/lib/hooks/use-user-photo.d.ts +50 -0
  150. package/src/lib/hooks/use-user-photo.js +87 -0
  151. package/src/lib/hooks/use-user-photo.js.map +1 -0
  152. package/src/lib/tenant.d.ts +61 -0
  153. package/src/lib/tenant.js +62 -0
  154. package/src/lib/tenant.js.map +1 -0
@@ -0,0 +1,207 @@
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 Query } from 'firebase/firestore';
18
+ /**
19
+ * A paged list SORTED BY A FIELD that still drops nothing (AGL-2853).
20
+ *
21
+ * ## The trap a field sort walks into
22
+ *
23
+ * `orderBy(field)` matches only documents that HAVE the field. A document
24
+ * without it is not sorted last, it is not in the result at all, so a list
25
+ * ordered that way does not mis-order the documents a writer left the field
26
+ * off — it hides them, and the rows it does show look complete. That is why
27
+ * `collectionPage` orders on the document name, which no document can lack.
28
+ *
29
+ * Content entries are the case that made the name order untenable: a
30
+ * changelog read in id order puts its newest release on whichever page the
31
+ * random id lands. And they carry exactly the gaps the trap feeds on —
32
+ * unpublishing deletes `publishedAt`, and a restored archive has no
33
+ * `createdAt`. Ordering on either hides every draft or every import.
34
+ *
35
+ * ## Two segments, one sequence
36
+ *
37
+ * The list is split by whether a document carries the sort field, and each
38
+ * half is walked by a query that can see all of it:
39
+ *
40
+ * KEYED `orderBy(field, dir), orderBy(documentId(), dir)` — every document
41
+ * that has the field, including an explicit `null`, in the
42
+ * requested order. The name is the tiebreak, so the order is total
43
+ * and a page boundary cannot fall between two equal values
44
+ * differently on two reads.
45
+ * UNKEYED `orderBy(documentId())` over the same base, kept to the documents
46
+ * that LACK the field. Firestore cannot query for an absent field,
47
+ * so this segment is a scan: it reads the base in name order and
48
+ * keeps what the keyed walk could not see.
49
+ *
50
+ * The two are disjoint and together are the whole base, so the list is the
51
+ * keyed segment followed by the unkeyed one — documents missing the sorted
52
+ * value come after every document that has one, in both directions. Every
53
+ * page is a slice of that one sequence, which is what keeps a document from
54
+ * being skipped or shown twice across a page boundary.
55
+ *
56
+ * ## What it costs
57
+ *
58
+ * The keyed walk is a window like `usePagedCollection`'s: one page plus a
59
+ * probe row, widened a page at a time. The scan opens only once the keyed
60
+ * walk has come back SHORT of its window — a server-confirmed fact that the
61
+ * keyed segment ends on this page — so the first pages of a long list cost
62
+ * exactly what an unsorted list costs. The scan starts at the number of rows
63
+ * the window still needs and doubles while it holds its whole limit without
64
+ * finding them; it stops the moment it has enough or reads past the end of the
65
+ * base. Its worst case is the base itself, reached only on the last pages.
66
+ *
67
+ * A base narrowed by an equality predicate narrows the scan with it, which is
68
+ * what keeps a filtered view cheap: a status filter scans only that status.
69
+ *
70
+ * ## Why the planner is pure
71
+ *
72
+ * `planKeyedSegment` and `planSortedWindow` are the whole decision — which
73
+ * limits to ask for, when the scan widens, which rows the page shows and
74
+ * whether another page exists. The hook adapts them to live listeners; the
75
+ * emulator spec drives the same two functions against a real Firestore, so
76
+ * the proof that nothing is dropped runs the code the console runs rather
77
+ * than a copy of it.
78
+ */
79
+ export type CollectionSortDirection = 'asc' | 'desc';
80
+ /** A field to sort a collection by, and which way. */
81
+ export interface CollectionSort {
82
+ /** A Firestore field path. Dotted paths address nested fields. */
83
+ field: string;
84
+ direction: CollectionSortDirection;
85
+ }
86
+ /**
87
+ * The KEYED segment: every document of `base` that carries `sort.field`, in
88
+ * that order, one window at a time.
89
+ *
90
+ * A field `base` already pins by EQUALITY holds one value across every
91
+ * matching document, so ordering on it orders nothing and neither direction
92
+ * means anything — the walk orders by the document name, ascending, which is
93
+ * the shape the field's automatic index serves. Every document of such a base
94
+ * carries the field, so no unkeyed segment exists; see `sortFieldIsTotal`.
95
+ */
96
+ export declare function sortedKeyedQuery(base: Query, sort: CollectionSort, pageLimit: number, equalityFields?: readonly string[]): Query;
97
+ /**
98
+ * The scan behind the UNKEYED segment: `base` in document-name order.
99
+ *
100
+ * Always ascending. The names are generated ids, so neither direction means
101
+ * anything to a reader, and an equality predicate followed by an ascending
102
+ * name order is the one shape every automatic index serves.
103
+ */
104
+ export declare function sortedUnkeyedQuery(base: Query, scanLimit: number): Query;
105
+ /**
106
+ * Whether every document `base` matches carries the sort field, because
107
+ * `base` pins that field by equality. Such a base has no unkeyed segment.
108
+ */
109
+ export declare function sortFieldIsTotal(sort: CollectionSort, equalityFields?: readonly string[]): boolean;
110
+ /**
111
+ * Whether a row read off a snapshot LACKS `field` — the membership test of
112
+ * the unkeyed segment, and the exact complement of what `orderBy(field)`
113
+ * returns.
114
+ *
115
+ * `undefined` and nothing else. Firestore cannot store `undefined`, so a
116
+ * missing key is the only way to read one; an explicit `null` is a stored
117
+ * value, indexed and returned by the keyed walk, and must not be read twice.
118
+ */
119
+ export declare function lacksSortField(row: unknown, field: string): boolean;
120
+ /** What the keyed read has established about the window. */
121
+ export interface KeyedSegmentPlan {
122
+ /** Rows before the page being read. */
123
+ offset: number;
124
+ /** Every page up to and including the one being read. */
125
+ windowSize: number;
126
+ /** The keyed walk's limit: the window plus a single probe row. */
127
+ keyedLimit: number;
128
+ /**
129
+ * Rows the unkeyed segment must supply to fill the window and its probe —
130
+ * `0` until the server has confirmed the keyed walk ends inside the window.
131
+ */
132
+ tailNeeded: number;
133
+ }
134
+ /** The keyed read, as the planner needs to see it. */
135
+ export interface KeyedSegmentInput<T> {
136
+ page: number;
137
+ pageSize: number;
138
+ /** The keyed rows read at this window's limit, or `undefined` if unread. */
139
+ keyed: readonly T[] | undefined;
140
+ /** The server has answered the keyed read — not only the local cache. */
141
+ keyedSettled: boolean;
142
+ /** See `sortFieldIsTotal`. */
143
+ keyedIsTotal: boolean;
144
+ }
145
+ /**
146
+ * The keyed half of the plan: the limit to ask for, and how many rows the
147
+ * scan owes the window.
148
+ *
149
+ * The scan is owed rows only when the keyed walk came back holding LESS than
150
+ * its limit, and only once the server said so. A cached answer can be short
151
+ * because the cache is, and opening a scan on it would pay to read documents
152
+ * the server's answer is about to render unnecessary.
153
+ */
154
+ export declare function planKeyedSegment<T>(input: KeyedSegmentInput<T>): KeyedSegmentPlan;
155
+ /**
156
+ * The limit the scan should be open at: `0` when no rows are owed, otherwise
157
+ * never less than it has already been widened to, so a keyed row arriving
158
+ * mid-scan does not re-open the scan one document narrower.
159
+ */
160
+ export declare function sortedScanLimit(plan: KeyedSegmentPlan, widenedTo: number): number;
161
+ /** Both reads, as the planner needs to see them. */
162
+ export interface SortedWindowInput<T> extends KeyedSegmentInput<T> {
163
+ sort: CollectionSort;
164
+ /** The scan rows read at `scanLimit`, or `undefined` if unread. */
165
+ scan: readonly T[] | undefined;
166
+ /** The server has answered the scan — not only the local cache. */
167
+ scanSettled: boolean;
168
+ /** The limit `scan` was read at; `0` when no scan is open. */
169
+ scanLimit: number;
170
+ /** A row's document id, which dedupes the two segments. */
171
+ idOf: (row: T) => string;
172
+ }
173
+ /** The window a list renders. */
174
+ export interface SortedWindow<T> extends KeyedSegmentPlan {
175
+ /**
176
+ * The limit the scan should be open at. Greater than the input's when the
177
+ * scan held its whole limit without finding the rows it owes, which is the
178
+ * signal to widen it.
179
+ */
180
+ scanLimit: number;
181
+ /** The page being read. Never the probe row. */
182
+ rows: T[];
183
+ /** A further page exists. A fact from the probe row, not a comparison. */
184
+ hasMore: boolean;
185
+ /**
186
+ * `rows` and `hasMore` are the server's answer for this window: the keyed
187
+ * walk is confirmed, and the scan is confirmed at its final limit whenever
188
+ * the window needed one.
189
+ */
190
+ settled: boolean;
191
+ /** The keyed rows then the unkeyed rows, deduped — probe row included. */
192
+ ordered: T[];
193
+ }
194
+ /**
195
+ * The page, from the keyed rows and the scan rows.
196
+ *
197
+ * The scan is widened only on a CONFIRMED short read: it held its whole limit
198
+ * (so the base continues past it) and kept fewer rows than the window owes.
199
+ * A scan that read past the end of the base has found every document the
200
+ * keyed walk cannot see, however few, and is done.
201
+ *
202
+ * Rows are deduped by id across the two segments. A document gaining or
203
+ * losing the sort field reaches the two listeners separately, and for the
204
+ * moment between their snapshots one of them still describes it the old way;
205
+ * the keyed copy wins, because the keyed walk is the one that orders it.
206
+ */
207
+ export declare function planSortedWindow<T>(input: SortedWindowInput<T>): SortedWindow<T>;
@@ -0,0 +1,150 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 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 { documentId, limit, orderBy, query } from "firebase/firestore";
18
+ /**
19
+ * The KEYED segment: every document of `base` that carries `sort.field`, in
20
+ * that order, one window at a time.
21
+ *
22
+ * A field `base` already pins by EQUALITY holds one value across every
23
+ * matching document, so ordering on it orders nothing and neither direction
24
+ * means anything — the walk orders by the document name, ascending, which is
25
+ * the shape the field's automatic index serves. Every document of such a base
26
+ * carries the field, so no unkeyed segment exists; see `sortFieldIsTotal`.
27
+ */ export function sortedKeyedQuery(base, sort, pageLimit, equalityFields = []) {
28
+ if (equalityFields.includes(sort.field)) {
29
+ return query(base, orderBy(documentId()), limit(pageLimit));
30
+ }
31
+ return query(base, orderBy(sort.field, sort.direction), orderBy(documentId(), sort.direction), limit(pageLimit));
32
+ }
33
+ /**
34
+ * The scan behind the UNKEYED segment: `base` in document-name order.
35
+ *
36
+ * Always ascending. The names are generated ids, so neither direction means
37
+ * anything to a reader, and an equality predicate followed by an ascending
38
+ * name order is the one shape every automatic index serves.
39
+ */ export function sortedUnkeyedQuery(base, scanLimit) {
40
+ return query(base, orderBy(documentId()), limit(scanLimit));
41
+ }
42
+ /**
43
+ * Whether every document `base` matches carries the sort field, because
44
+ * `base` pins that field by equality. Such a base has no unkeyed segment.
45
+ */ export function sortFieldIsTotal(sort, equalityFields = []) {
46
+ return equalityFields.includes(sort.field);
47
+ }
48
+ /**
49
+ * Whether a row read off a snapshot LACKS `field` — the membership test of
50
+ * the unkeyed segment, and the exact complement of what `orderBy(field)`
51
+ * returns.
52
+ *
53
+ * `undefined` and nothing else. Firestore cannot store `undefined`, so a
54
+ * missing key is the only way to read one; an explicit `null` is a stored
55
+ * value, indexed and returned by the keyed walk, and must not be read twice.
56
+ */ export function lacksSortField(row, field) {
57
+ let value = row;
58
+ for (const key of field.split('.')){
59
+ if (value === null || typeof value !== 'object') return true;
60
+ value = value[key];
61
+ }
62
+ return value === undefined;
63
+ }
64
+ /**
65
+ * The keyed half of the plan: the limit to ask for, and how many rows the
66
+ * scan owes the window.
67
+ *
68
+ * The scan is owed rows only when the keyed walk came back holding LESS than
69
+ * its limit, and only once the server said so. A cached answer can be short
70
+ * because the cache is, and opening a scan on it would pay to read documents
71
+ * the server's answer is about to render unnecessary.
72
+ */ export function planKeyedSegment(input) {
73
+ var _ref;
74
+ var _input_keyed;
75
+ const offset = input.page * input.pageSize;
76
+ const windowSize = offset + input.pageSize;
77
+ const keyedLimit = windowSize + 1;
78
+ const held = (_ref = (_input_keyed = input.keyed) == null ? void 0 : _input_keyed.length) != null ? _ref : 0;
79
+ const exhausted = input.keyed !== undefined && input.keyedSettled && held < keyedLimit;
80
+ return {
81
+ offset,
82
+ windowSize,
83
+ keyedLimit,
84
+ tailNeeded: exhausted && !input.keyedIsTotal ? keyedLimit - held : 0
85
+ };
86
+ }
87
+ /**
88
+ * The limit the scan should be open at: `0` when no rows are owed, otherwise
89
+ * never less than it has already been widened to, so a keyed row arriving
90
+ * mid-scan does not re-open the scan one document narrower.
91
+ */ export function sortedScanLimit(plan, widenedTo) {
92
+ return plan.tailNeeded > 0 ? Math.max(widenedTo, plan.tailNeeded) : 0;
93
+ }
94
+ /**
95
+ * The page, from the keyed rows and the scan rows.
96
+ *
97
+ * The scan is widened only on a CONFIRMED short read: it held its whole limit
98
+ * (so the base continues past it) and kept fewer rows than the window owes.
99
+ * A scan that read past the end of the base has found every document the
100
+ * keyed walk cannot see, however few, and is done.
101
+ *
102
+ * Rows are deduped by id across the two segments. A document gaining or
103
+ * losing the sort field reaches the two listeners separately, and for the
104
+ * moment between their snapshots one of them still describes it the old way;
105
+ * the keyed copy wins, because the keyed walk is the one that orders it.
106
+ */ export function planSortedWindow(input) {
107
+ var _input_keyed;
108
+ const plan = planKeyedSegment(input);
109
+ const keyed = (_input_keyed = input.keyed) != null ? _input_keyed : [];
110
+ const keyedSettled = input.keyed !== undefined && input.keyedSettled;
111
+ let scanLimit = 0;
112
+ let tail = [];
113
+ let tailSettled = true;
114
+ if (plan.tailNeeded > 0) {
115
+ var _ref;
116
+ const scanned = input.scanLimit > 0 ? input.scan : undefined;
117
+ const scanSettled = scanned !== undefined && input.scanSettled;
118
+ const scanExhausted = scanSettled && ((_ref = scanned == null ? void 0 : scanned.length) != null ? _ref : 0) < input.scanLimit;
119
+ tail = (scanned != null ? scanned : []).filter((row)=>lacksSortField(row, input.sort.field));
120
+ if (scanExhausted || scanSettled && tail.length >= plan.tailNeeded) {
121
+ scanLimit = input.scanLimit;
122
+ } else if (scanSettled) {
123
+ scanLimit = Math.max(input.scanLimit * 2, plan.tailNeeded);
124
+ tailSettled = false;
125
+ } else {
126
+ scanLimit = Math.max(input.scanLimit, plan.tailNeeded);
127
+ tailSettled = false;
128
+ }
129
+ }
130
+ const seen = new Set();
131
+ const ordered = [];
132
+ for (const row of [
133
+ ...keyed,
134
+ ...tail
135
+ ]){
136
+ const id = input.idOf(row);
137
+ if (seen.has(id)) continue;
138
+ seen.add(id);
139
+ ordered.push(row);
140
+ }
141
+ return _extends({}, plan, {
142
+ scanLimit,
143
+ rows: ordered.slice(plan.offset, plan.windowSize),
144
+ hasMore: ordered.length > plan.windowSize,
145
+ settled: keyedSettled && tailSettled,
146
+ ordered
147
+ });
148
+ }
149
+
150
+ //# sourceMappingURL=sorted-collection-window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/sorted-collection-window.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n documentId,\n limit,\n orderBy,\n query,\n type Query,\n} from 'firebase/firestore'\n\n/**\n * A paged list SORTED BY A FIELD that still drops nothing (AGL-2853).\n *\n * ## The trap a field sort walks into\n *\n * `orderBy(field)` matches only documents that HAVE the field. A document\n * without it is not sorted last, it is not in the result at all, so a list\n * ordered that way does not mis-order the documents a writer left the field\n * off — it hides them, and the rows it does show look complete. That is why\n * `collectionPage` orders on the document name, which no document can lack.\n *\n * Content entries are the case that made the name order untenable: a\n * changelog read in id order puts its newest release on whichever page the\n * random id lands. And they carry exactly the gaps the trap feeds on —\n * unpublishing deletes `publishedAt`, and a restored archive has no\n * `createdAt`. Ordering on either hides every draft or every import.\n *\n * ## Two segments, one sequence\n *\n * The list is split by whether a document carries the sort field, and each\n * half is walked by a query that can see all of it:\n *\n * KEYED `orderBy(field, dir), orderBy(documentId(), dir)` — every document\n * that has the field, including an explicit `null`, in the\n * requested order. The name is the tiebreak, so the order is total\n * and a page boundary cannot fall between two equal values\n * differently on two reads.\n * UNKEYED `orderBy(documentId())` over the same base, kept to the documents\n * that LACK the field. Firestore cannot query for an absent field,\n * so this segment is a scan: it reads the base in name order and\n * keeps what the keyed walk could not see.\n *\n * The two are disjoint and together are the whole base, so the list is the\n * keyed segment followed by the unkeyed one — documents missing the sorted\n * value come after every document that has one, in both directions. Every\n * page is a slice of that one sequence, which is what keeps a document from\n * being skipped or shown twice across a page boundary.\n *\n * ## What it costs\n *\n * The keyed walk is a window like `usePagedCollection`'s: one page plus a\n * probe row, widened a page at a time. The scan opens only once the keyed\n * walk has come back SHORT of its window — a server-confirmed fact that the\n * keyed segment ends on this page — so the first pages of a long list cost\n * exactly what an unsorted list costs. The scan starts at the number of rows\n * the window still needs and doubles while it holds its whole limit without\n * finding them; it stops the moment it has enough or reads past the end of the\n * base. Its worst case is the base itself, reached only on the last pages.\n *\n * A base narrowed by an equality predicate narrows the scan with it, which is\n * what keeps a filtered view cheap: a status filter scans only that status.\n *\n * ## Why the planner is pure\n *\n * `planKeyedSegment` and `planSortedWindow` are the whole decision — which\n * limits to ask for, when the scan widens, which rows the page shows and\n * whether another page exists. The hook adapts them to live listeners; the\n * emulator spec drives the same two functions against a real Firestore, so\n * the proof that nothing is dropped runs the code the console runs rather\n * than a copy of it.\n */\n\nexport type CollectionSortDirection = 'asc' | 'desc'\n\n/** A field to sort a collection by, and which way. */\nexport interface CollectionSort {\n /** A Firestore field path. Dotted paths address nested fields. */\n field: string\n direction: CollectionSortDirection\n}\n\n/**\n * The KEYED segment: every document of `base` that carries `sort.field`, in\n * that order, one window at a time.\n *\n * A field `base` already pins by EQUALITY holds one value across every\n * matching document, so ordering on it orders nothing and neither direction\n * means anything — the walk orders by the document name, ascending, which is\n * the shape the field's automatic index serves. Every document of such a base\n * carries the field, so no unkeyed segment exists; see `sortFieldIsTotal`.\n */\nexport function sortedKeyedQuery(\n base: Query,\n sort: CollectionSort,\n pageLimit: number,\n equalityFields: readonly string[] = [],\n): Query {\n if (equalityFields.includes(sort.field)) {\n return query(base, orderBy(documentId()), limit(pageLimit))\n }\n return query(\n base,\n orderBy(sort.field, sort.direction),\n orderBy(documentId(), sort.direction),\n limit(pageLimit),\n )\n}\n\n/**\n * The scan behind the UNKEYED segment: `base` in document-name order.\n *\n * Always ascending. The names are generated ids, so neither direction means\n * anything to a reader, and an equality predicate followed by an ascending\n * name order is the one shape every automatic index serves.\n */\nexport function sortedUnkeyedQuery(base: Query, scanLimit: number): Query {\n return query(base, orderBy(documentId()), limit(scanLimit))\n}\n\n/**\n * Whether every document `base` matches carries the sort field, because\n * `base` pins that field by equality. Such a base has no unkeyed segment.\n */\nexport function sortFieldIsTotal(\n sort: CollectionSort,\n equalityFields: readonly string[] = [],\n): boolean {\n return equalityFields.includes(sort.field)\n}\n\n/**\n * Whether a row read off a snapshot LACKS `field` — the membership test of\n * the unkeyed segment, and the exact complement of what `orderBy(field)`\n * returns.\n *\n * `undefined` and nothing else. Firestore cannot store `undefined`, so a\n * missing key is the only way to read one; an explicit `null` is a stored\n * value, indexed and returned by the keyed walk, and must not be read twice.\n */\nexport function lacksSortField(row: unknown, field: string): boolean {\n let value: unknown = row\n for (const key of field.split('.')) {\n if (value === null || typeof value !== 'object') return true\n value = (value as Record<string, unknown>)[key]\n }\n return value === undefined\n}\n\n/** What the keyed read has established about the window. */\nexport interface KeyedSegmentPlan {\n /** Rows before the page being read. */\n offset: number\n /** Every page up to and including the one being read. */\n windowSize: number\n /** The keyed walk's limit: the window plus a single probe row. */\n keyedLimit: number\n /**\n * Rows the unkeyed segment must supply to fill the window and its probe —\n * `0` until the server has confirmed the keyed walk ends inside the window.\n */\n tailNeeded: number\n}\n\n/** The keyed read, as the planner needs to see it. */\nexport interface KeyedSegmentInput<T> {\n page: number\n pageSize: number\n /** The keyed rows read at this window's limit, or `undefined` if unread. */\n keyed: readonly T[] | undefined\n /** The server has answered the keyed read — not only the local cache. */\n keyedSettled: boolean\n /** See `sortFieldIsTotal`. */\n keyedIsTotal: boolean\n}\n\n/**\n * The keyed half of the plan: the limit to ask for, and how many rows the\n * scan owes the window.\n *\n * The scan is owed rows only when the keyed walk came back holding LESS than\n * its limit, and only once the server said so. A cached answer can be short\n * because the cache is, and opening a scan on it would pay to read documents\n * the server's answer is about to render unnecessary.\n */\nexport function planKeyedSegment<T>(\n input: KeyedSegmentInput<T>,\n): KeyedSegmentPlan {\n const offset = input.page * input.pageSize\n const windowSize = offset + input.pageSize\n const keyedLimit = windowSize + 1\n const held = input.keyed?.length ?? 0\n const exhausted =\n input.keyed !== undefined && input.keyedSettled && held < keyedLimit\n return {\n offset,\n windowSize,\n keyedLimit,\n tailNeeded: exhausted && !input.keyedIsTotal ? keyedLimit - held : 0,\n }\n}\n\n/**\n * The limit the scan should be open at: `0` when no rows are owed, otherwise\n * never less than it has already been widened to, so a keyed row arriving\n * mid-scan does not re-open the scan one document narrower.\n */\nexport function sortedScanLimit(\n plan: KeyedSegmentPlan,\n widenedTo: number,\n): number {\n return plan.tailNeeded > 0 ? Math.max(widenedTo, plan.tailNeeded) : 0\n}\n\n/** Both reads, as the planner needs to see them. */\nexport interface SortedWindowInput<T> extends KeyedSegmentInput<T> {\n sort: CollectionSort\n /** The scan rows read at `scanLimit`, or `undefined` if unread. */\n scan: readonly T[] | undefined\n /** The server has answered the scan — not only the local cache. */\n scanSettled: boolean\n /** The limit `scan` was read at; `0` when no scan is open. */\n scanLimit: number\n /** A row's document id, which dedupes the two segments. */\n idOf: (row: T) => string\n}\n\n/** The window a list renders. */\nexport interface SortedWindow<T> extends KeyedSegmentPlan {\n /**\n * The limit the scan should be open at. Greater than the input's when the\n * scan held its whole limit without finding the rows it owes, which is the\n * signal to widen it.\n */\n scanLimit: number\n /** The page being read. Never the probe row. */\n rows: T[]\n /** A further page exists. A fact from the probe row, not a comparison. */\n hasMore: boolean\n /**\n * `rows` and `hasMore` are the server's answer for this window: the keyed\n * walk is confirmed, and the scan is confirmed at its final limit whenever\n * the window needed one.\n */\n settled: boolean\n /** The keyed rows then the unkeyed rows, deduped — probe row included. */\n ordered: T[]\n}\n\n/**\n * The page, from the keyed rows and the scan rows.\n *\n * The scan is widened only on a CONFIRMED short read: it held its whole limit\n * (so the base continues past it) and kept fewer rows than the window owes.\n * A scan that read past the end of the base has found every document the\n * keyed walk cannot see, however few, and is done.\n *\n * Rows are deduped by id across the two segments. A document gaining or\n * losing the sort field reaches the two listeners separately, and for the\n * moment between their snapshots one of them still describes it the old way;\n * the keyed copy wins, because the keyed walk is the one that orders it.\n */\nexport function planSortedWindow<T>(\n input: SortedWindowInput<T>,\n): SortedWindow<T> {\n const plan = planKeyedSegment(input)\n const keyed = input.keyed ?? []\n const keyedSettled = input.keyed !== undefined && input.keyedSettled\n\n let scanLimit = 0\n let tail: readonly T[] = []\n let tailSettled = true\n if (plan.tailNeeded > 0) {\n const scanned = input.scanLimit > 0 ? input.scan : undefined\n const scanSettled = scanned !== undefined && input.scanSettled\n const scanExhausted =\n scanSettled && (scanned?.length ?? 0) < input.scanLimit\n tail = (scanned ?? []).filter((row) =>\n lacksSortField(row, input.sort.field),\n )\n if (scanExhausted || (scanSettled && tail.length >= plan.tailNeeded)) {\n scanLimit = input.scanLimit\n } else if (scanSettled) {\n scanLimit = Math.max(input.scanLimit * 2, plan.tailNeeded)\n tailSettled = false\n } else {\n scanLimit = Math.max(input.scanLimit, plan.tailNeeded)\n tailSettled = false\n }\n }\n\n const seen = new Set<string>()\n const ordered: T[] = []\n for (const row of [...keyed, ...tail]) {\n const id = input.idOf(row)\n if (seen.has(id)) continue\n seen.add(id)\n ordered.push(row)\n }\n\n return {\n ...plan,\n scanLimit,\n rows: ordered.slice(plan.offset, plan.windowSize),\n hasMore: ordered.length > plan.windowSize,\n settled: keyedSettled && tailSettled,\n ordered,\n }\n}\n"],"names":["documentId","limit","orderBy","query","sortedKeyedQuery","base","sort","pageLimit","equalityFields","includes","field","direction","sortedUnkeyedQuery","scanLimit","sortFieldIsTotal","lacksSortField","row","value","key","split","undefined","planKeyedSegment","input","offset","page","pageSize","windowSize","keyedLimit","held","keyed","length","exhausted","keyedSettled","tailNeeded","keyedIsTotal","sortedScanLimit","plan","widenedTo","Math","max","planSortedWindow","tail","tailSettled","scanned","scan","scanSettled","scanExhausted","filter","seen","Set","ordered","id","idOf","has","add","push","rows","slice","hasMore","settled"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type * as Aglyn from '@aglyn/aglyn';
18
+ import type { DocumentReference } from 'firebase/firestore';
19
+ import { type FirestoreDocOptions } from './firebase/firebase-services';
20
+ /**
21
+ * A reusable component's working version (AGL-679), at
22
+ * `hosts/{hostId}/components/{componentId}/versions/{versionId}`.
23
+ *
24
+ * Compressed at rest like screen and layout versions. Note the asymmetry
25
+ * with those two: the component's PUBLISHED nodes live on the parent doc,
26
+ * because the tenant runtime reads every component in one collection query
27
+ * — pulling published content into version docs would make that N+1 per
28
+ * page render. These docs are editing history only.
29
+ */
30
+ export declare const useComponentVersionRef: ({ hostId, componentId, versionId, }: {
31
+ hostId: string;
32
+ componentId: string;
33
+ versionId: string;
34
+ }) => DocumentReference<Aglyn.AglynHostComponentVersion>;
35
+ export declare const useComponentVersion: (data: {
36
+ hostId: string;
37
+ componentId: string;
38
+ versionId: string;
39
+ }, options?: FirestoreDocOptions<Aglyn.AglynHostComponentVersion>) => {
40
+ doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.AglynHostComponentVersion<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
41
+ setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.AglynHostComponentVersion<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
42
+ };
43
+ export default useComponentVersion;
@@ -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 reusable component's working version (AGL-679), at
28
+ * `hosts/{hostId}/components/{componentId}/versions/{versionId}`.
29
+ *
30
+ * Compressed at rest like screen and layout versions. Note the asymmetry
31
+ * with those two: the component's PUBLISHED nodes live on the parent doc,
32
+ * because the tenant runtime reads every component in one collection query
33
+ * — pulling published content into version docs would make that N+1 per
34
+ * page render. These docs are editing history only.
35
+ */ export const useComponentVersionRef = ({ hostId, componentId, versionId })=>{
36
+ const firestore = useFirestore();
37
+ const ref = doc(firestore, 'hosts', hostId, 'components', componentId, 'versions', versionId);
38
+ return ref.withConverter({
39
+ toFirestore (data) {
40
+ const { $id } = data, rest = _object_without_properties_loose(data, [
41
+ "$id"
42
+ ]);
43
+ // Only emit `nodes` when the write carries them — see the note in
44
+ // use-screen-version (AGL-1250). This is the converter the component
45
+ // Properties dialog writes through, and unconditional compression
46
+ // destroyed a real component's tree on the first partial write.
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 useComponentVersion = (data, options)=>{
69
+ return useDoc(useComponentVersionRef(data), options);
70
+ };
71
+ export default useComponentVersion;
72
+
73
+ //# sourceMappingURL=use-component-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-component-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 reusable component's working version (AGL-679), at\n * `hosts/{hostId}/components/{componentId}/versions/{versionId}`.\n *\n * Compressed at rest like screen and layout versions. Note the asymmetry\n * with those two: the component's PUBLISHED nodes live on the parent doc,\n * because the tenant runtime reads every component in one collection query\n * — pulling published content into version docs would make that N+1 per\n * page render. These docs are editing history only.\n */\nexport const useComponentVersionRef = ({\n hostId,\n componentId,\n versionId,\n}: {\n hostId: string\n componentId: string\n versionId: string\n}) => {\n const firestore = useFirestore()\n const ref = doc(\n firestore,\n 'hosts',\n hostId,\n 'components',\n componentId,\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). This is the converter the component\n // Properties dialog writes through, and unconditional compression\n // destroyed a real component's tree on the first partial write.\n if (rest?.nodes === undefined) return { ...rest, updatedAt: Timestamp.now() }\n const nodes =\n rest.nodes instanceof Bytes ? rest.nodes : 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.AglynHostComponentVersion\n }\n return data as Aglyn.AglynHostComponentVersion\n },\n }) as DocumentReference<Aglyn.AglynHostComponentVersion>\n}\n\nexport const useComponentVersion = (\n data: {\n hostId: string\n componentId: string\n versionId: string\n },\n options?: FirestoreDocOptions<Aglyn.AglynHostComponentVersion>,\n) => {\n return useDoc(useComponentVersionRef(data), options)\n}\n\nexport default useComponentVersion\n"],"names":["compress","decompress","Timestamp","Bytes","doc","useFirestore","useDoc","useComponentVersionRef","hostId","componentId","versionId","firestore","ref","withConverter","toFirestore","data","$id","rest","nodes","undefined","updatedAt","now","fromUint8Array","fromFirestore","snapshot","options","exists","useComponentVersion"],"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;;;;;;;;;CASC,GACD,OAAO,MAAMC,yBAAyB,CAAC,EACrCC,MAAM,EACNC,WAAW,EACXC,SAAS,EAKV;IACC,MAAMC,YAAYN;IAClB,MAAMO,MAAMR,IACVO,WACA,SACAH,QACA,cACAC,aACA,YACAC;IAEF,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAAI;YACd,MAAM,EAAEC,GAAG,EAAW,GAAGD,MAATE,wCAASF;;;YACzB,kEAAkE;YAClE,qEAAqE;YACrE,kEAAkE;YAClE,gEAAgE;YAChE,IAAIE,CAAAA,wBAAAA,KAAMC,KAAK,MAAKC,WAAW,OAAO,aAAKF;gBAAMG,WAAWlB,UAAUmB,GAAG;;YACzE,MAAMH,QACJD,KAAKC,KAAK,YAAYf,QAAQc,KAAKC,KAAK,GAAGf,MAAMmB,cAAc,CAACtB,SAASiB,KAAKC,KAAK;YACrF,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,sBAAsB,CACjCZ,MAKAU;IAEA,OAAOnB,OAAOC,uBAAuBQ,OAAOU;AAC9C,EAAC;AAED,eAAeE,oBAAmB"}
@@ -0,0 +1,39 @@
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 reusable component definition (AGL-680).
22
+ *
23
+ * `nodes` and `rootId` on this doc are the PUBLISHED copy — what the tenant
24
+ * runtime renders, read for every component in one collection query. They
25
+ * are stored plainly rather than compressed, unlike screen and layout
26
+ * versions, so the runtime can read them without decoding.
27
+ */
28
+ export declare const useComponentRef: ({ hostId, componentId, }: {
29
+ hostId: Aglyn.HostUid;
30
+ componentId: string;
31
+ }) => DocumentReference<Aglyn.AglynHostComponent>;
32
+ export declare const useComponent: (data: {
33
+ hostId: Aglyn.HostUid;
34
+ componentId: string;
35
+ }, options?: FirestoreDocOptions<Aglyn.AglynHostComponent>) => {
36
+ doc: import("./firebase/firebase-services").ObservableStatus<Aglyn.AglynHostComponent<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
37
+ setDoc: import("./helpers/use-modify-doc-callback").ModifyDocCallback<Aglyn.AglynHostComponent<Aglyn.AglynNodeSchema<Partial<Record<JSX.Key.Any, unknown>>>>>;
38
+ };
39
+ export default useComponent;
@@ -0,0 +1,55 @@
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 { Timestamp } from "@aglyn/shared-util-timestamp";
22
+ import { doc } from "firebase/firestore";
23
+ import { useFirestore } from "./firebase/firebase-services.js";
24
+ import useDoc from "./helpers/use-doc.js";
25
+ /**
26
+ * A reusable component definition (AGL-680).
27
+ *
28
+ * `nodes` and `rootId` on this doc are the PUBLISHED copy — what the tenant
29
+ * runtime renders, read for every component in one collection query. They
30
+ * are stored plainly rather than compressed, unlike screen and layout
31
+ * versions, so the runtime can read them without decoding.
32
+ */ export const useComponentRef = ({ hostId, componentId })=>{
33
+ const firestore = useFirestore();
34
+ const ref = doc(firestore, 'hosts', hostId, 'components', componentId);
35
+ return ref.withConverter({
36
+ toFirestore (data) {
37
+ const { $id } = data, rest = _object_without_properties_loose(data, [
38
+ "$id"
39
+ ]);
40
+ return _extends({}, rest, {
41
+ updatedAt: Timestamp.now()
42
+ });
43
+ },
44
+ fromFirestore (snapshot, options) {
45
+ if (!snapshot.exists()) return undefined;
46
+ return snapshot.data(options);
47
+ }
48
+ });
49
+ };
50
+ export const useComponent = (data, options)=>{
51
+ return useDoc(useComponentRef(data), options);
52
+ };
53
+ export default useComponent;
54
+
55
+ //# sourceMappingURL=use-component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-component.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 { Timestamp } from '@aglyn/shared-util-timestamp'\nimport type { DocumentReference } from 'firebase/firestore'\nimport { 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 reusable component definition (AGL-680).\n *\n * `nodes` and `rootId` on this doc are the PUBLISHED copy — what the tenant\n * runtime renders, read for every component in one collection query. They\n * are stored plainly rather than compressed, unlike screen and layout\n * versions, so the runtime can read them without decoding.\n */\nexport const useComponentRef = ({\n hostId,\n componentId,\n}: {\n hostId: Aglyn.HostUid\n componentId: string\n}) => {\n const firestore = useFirestore()\n const ref = doc(firestore, 'hosts', hostId, 'components', componentId)\n return ref.withConverter({\n toFirestore(data: Aglyn.AglynHostComponent) {\n const { $id, ...rest } = data\n return { ...rest, updatedAt: Timestamp.now() }\n },\n fromFirestore(snapshot, options) {\n if (!snapshot.exists()) return undefined\n return snapshot.data(options) as Aglyn.AglynHostComponent\n },\n }) as DocumentReference<Aglyn.AglynHostComponent>\n}\n\nexport const useComponent = (\n data: { hostId: Aglyn.HostUid; componentId: string },\n options?: FirestoreDocOptions<Aglyn.AglynHostComponent>,\n) => {\n return useDoc(useComponentRef(data), options)\n}\n\nexport default useComponent\n"],"names":["Timestamp","doc","useFirestore","useDoc","useComponentRef","hostId","componentId","firestore","ref","withConverter","toFirestore","data","$id","rest","updatedAt","now","fromFirestore","snapshot","options","exists","undefined","useComponent"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4EAA4E;AAC5E,yEAAyE;AACzE,wEAAwE;;;AAExE,SAASA,SAAS,QAAQ,+BAA8B;AAExD,SAASC,GAAG,QAAQ,qBAAoB;AACxC,SACEC,YAAY,QAEP,kCAA8B;AACrC,OAAOC,YAAY,uBAAmB;AAEtC;;;;;;;CAOC,GACD,OAAO,MAAMC,kBAAkB,CAAC,EAC9BC,MAAM,EACNC,WAAW,EAIZ;IACC,MAAMC,YAAYL;IAClB,MAAMM,MAAMP,IAAIM,WAAW,SAASF,QAAQ,cAAcC;IAC1D,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAA8B;YACxC,MAAM,EAAEC,GAAG,EAAW,GAAGD,MAATE,wCAASF;;;YACzB,OAAO,aAAKE;gBAAMC,WAAWd,UAAUe,GAAG;;QAC5C;QACAC,eAAcC,QAAQ,EAAEC,OAAO;YAC7B,IAAI,CAACD,SAASE,MAAM,IAAI,OAAOC;YAC/B,OAAOH,SAASN,IAAI,CAACO;QACvB;IACF;AACF,EAAC;AAED,OAAO,MAAMG,eAAe,CAC1BV,MACAO;IAEA,OAAOf,OAAOC,gBAAgBO,OAAOO;AACvC,EAAC;AAED,eAAeG,aAAY"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Console route base for a host: `/{orgSlug}/hosts/{subdomain}` (AGL-673).
3
+ *
4
+ * Plugin components are handed a host DOC ID and used to build links as
5
+ * `/{hostDocId}/…`. Console routes have been `/[orgSlug]/hosts/[subdomain]/…`
6
+ * since AGL-621/622, so every one of those links silently began 404ing —
7
+ * found three separate times in different plugins, each looking like a new
8
+ * bug. This is the one resolution, so the next plugin does not become the
9
+ * fourth.
10
+ *
11
+ * `null` until resolved, and `null` forever if the org has no slug. Callers
12
+ * must render plain text rather than a link in that case: a link to nowhere
13
+ * is worse than no link.
14
+ *
15
+ * Reads `hostIndex` (signed-in readable, mirrors `subdomain` and `orgId`)
16
+ * plus the org doc for its slug — both cheap and already cached by the SDK
17
+ * on most screens.
18
+ */
19
+ export declare function useConsoleHostRoute(hostId: string | undefined | null): {
20
+ base: string | null;
21
+ orgSlug: string | null;
22
+ subdomain: string | null;
23
+ };
24
+ export default useConsoleHostRoute;