@noy-db/hub 0.1.0-pre.3

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/aggregate/index.cjs +476 -0
  4. package/dist/aggregate/index.cjs.map +1 -0
  5. package/dist/aggregate/index.d.cts +38 -0
  6. package/dist/aggregate/index.d.ts +38 -0
  7. package/dist/aggregate/index.js +53 -0
  8. package/dist/aggregate/index.js.map +1 -0
  9. package/dist/blobs/index.cjs +1480 -0
  10. package/dist/blobs/index.cjs.map +1 -0
  11. package/dist/blobs/index.d.cts +45 -0
  12. package/dist/blobs/index.d.ts +45 -0
  13. package/dist/blobs/index.js +48 -0
  14. package/dist/blobs/index.js.map +1 -0
  15. package/dist/bundle/index.cjs +436 -0
  16. package/dist/bundle/index.cjs.map +1 -0
  17. package/dist/bundle/index.d.cts +7 -0
  18. package/dist/bundle/index.d.ts +7 -0
  19. package/dist/bundle/index.js +40 -0
  20. package/dist/bundle/index.js.map +1 -0
  21. package/dist/chunk-2QR2PQTT.js +217 -0
  22. package/dist/chunk-2QR2PQTT.js.map +1 -0
  23. package/dist/chunk-4OWFYIDQ.js +79 -0
  24. package/dist/chunk-4OWFYIDQ.js.map +1 -0
  25. package/dist/chunk-5AATM2M2.js +90 -0
  26. package/dist/chunk-5AATM2M2.js.map +1 -0
  27. package/dist/chunk-ACLDOTNQ.js +543 -0
  28. package/dist/chunk-ACLDOTNQ.js.map +1 -0
  29. package/dist/chunk-BTDCBVJW.js +160 -0
  30. package/dist/chunk-BTDCBVJW.js.map +1 -0
  31. package/dist/chunk-CIMZBAZB.js +72 -0
  32. package/dist/chunk-CIMZBAZB.js.map +1 -0
  33. package/dist/chunk-E445ICYI.js +365 -0
  34. package/dist/chunk-E445ICYI.js.map +1 -0
  35. package/dist/chunk-EXQRC2L4.js +722 -0
  36. package/dist/chunk-EXQRC2L4.js.map +1 -0
  37. package/dist/chunk-FZU343FL.js +32 -0
  38. package/dist/chunk-FZU343FL.js.map +1 -0
  39. package/dist/chunk-GJILMRPO.js +354 -0
  40. package/dist/chunk-GJILMRPO.js.map +1 -0
  41. package/dist/chunk-GOUT6DND.js +1285 -0
  42. package/dist/chunk-GOUT6DND.js.map +1 -0
  43. package/dist/chunk-J66GRPNH.js +111 -0
  44. package/dist/chunk-J66GRPNH.js.map +1 -0
  45. package/dist/chunk-M2F2JAWB.js +464 -0
  46. package/dist/chunk-M2F2JAWB.js.map +1 -0
  47. package/dist/chunk-M5INGEFC.js +84 -0
  48. package/dist/chunk-M5INGEFC.js.map +1 -0
  49. package/dist/chunk-M62XNWRA.js +72 -0
  50. package/dist/chunk-M62XNWRA.js.map +1 -0
  51. package/dist/chunk-MR4424N3.js +275 -0
  52. package/dist/chunk-MR4424N3.js.map +1 -0
  53. package/dist/chunk-NPC4LFV5.js +132 -0
  54. package/dist/chunk-NPC4LFV5.js.map +1 -0
  55. package/dist/chunk-NXFEYLVG.js +311 -0
  56. package/dist/chunk-NXFEYLVG.js.map +1 -0
  57. package/dist/chunk-R36SIKES.js +79 -0
  58. package/dist/chunk-R36SIKES.js.map +1 -0
  59. package/dist/chunk-TDR6T5CJ.js +381 -0
  60. package/dist/chunk-TDR6T5CJ.js.map +1 -0
  61. package/dist/chunk-UF3BUNQZ.js +1 -0
  62. package/dist/chunk-UF3BUNQZ.js.map +1 -0
  63. package/dist/chunk-UQFSPSWG.js +1109 -0
  64. package/dist/chunk-UQFSPSWG.js.map +1 -0
  65. package/dist/chunk-USKYUS74.js +793 -0
  66. package/dist/chunk-USKYUS74.js.map +1 -0
  67. package/dist/chunk-XCL3WP6J.js +121 -0
  68. package/dist/chunk-XCL3WP6J.js.map +1 -0
  69. package/dist/chunk-XHFOENR2.js +680 -0
  70. package/dist/chunk-XHFOENR2.js.map +1 -0
  71. package/dist/chunk-ZFKD4QMV.js +430 -0
  72. package/dist/chunk-ZFKD4QMV.js.map +1 -0
  73. package/dist/chunk-ZLMV3TUA.js +490 -0
  74. package/dist/chunk-ZLMV3TUA.js.map +1 -0
  75. package/dist/chunk-ZRG4V3F5.js +17 -0
  76. package/dist/chunk-ZRG4V3F5.js.map +1 -0
  77. package/dist/consent/index.cjs +204 -0
  78. package/dist/consent/index.cjs.map +1 -0
  79. package/dist/consent/index.d.cts +24 -0
  80. package/dist/consent/index.d.ts +24 -0
  81. package/dist/consent/index.js +23 -0
  82. package/dist/consent/index.js.map +1 -0
  83. package/dist/crdt/index.cjs +152 -0
  84. package/dist/crdt/index.cjs.map +1 -0
  85. package/dist/crdt/index.d.cts +30 -0
  86. package/dist/crdt/index.d.ts +30 -0
  87. package/dist/crdt/index.js +24 -0
  88. package/dist/crdt/index.js.map +1 -0
  89. package/dist/crypto-IVKU7YTT.js +44 -0
  90. package/dist/crypto-IVKU7YTT.js.map +1 -0
  91. package/dist/delegation-XDJCBTI2.js +16 -0
  92. package/dist/delegation-XDJCBTI2.js.map +1 -0
  93. package/dist/dev-unlock-CeXic1xC.d.cts +263 -0
  94. package/dist/dev-unlock-KrKkcqD3.d.ts +263 -0
  95. package/dist/hash-9KO1BGxh.d.cts +63 -0
  96. package/dist/hash-ChfJjRjQ.d.ts +63 -0
  97. package/dist/history/index.cjs +1215 -0
  98. package/dist/history/index.cjs.map +1 -0
  99. package/dist/history/index.d.cts +62 -0
  100. package/dist/history/index.d.ts +62 -0
  101. package/dist/history/index.js +79 -0
  102. package/dist/history/index.js.map +1 -0
  103. package/dist/i18n/index.cjs +746 -0
  104. package/dist/i18n/index.cjs.map +1 -0
  105. package/dist/i18n/index.d.cts +38 -0
  106. package/dist/i18n/index.d.ts +38 -0
  107. package/dist/i18n/index.js +55 -0
  108. package/dist/i18n/index.js.map +1 -0
  109. package/dist/index-BRHBCmLt.d.ts +1940 -0
  110. package/dist/index-C8kQtmOk.d.ts +380 -0
  111. package/dist/index-DN-J-5wT.d.cts +1940 -0
  112. package/dist/index-DhjMjz7L.d.cts +380 -0
  113. package/dist/index.cjs +14756 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +269 -0
  116. package/dist/index.d.ts +269 -0
  117. package/dist/index.js +6085 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/indexing/index.cjs +736 -0
  120. package/dist/indexing/index.cjs.map +1 -0
  121. package/dist/indexing/index.d.cts +36 -0
  122. package/dist/indexing/index.d.ts +36 -0
  123. package/dist/indexing/index.js +77 -0
  124. package/dist/indexing/index.js.map +1 -0
  125. package/dist/lazy-builder-BwEoBQZ9.d.ts +304 -0
  126. package/dist/lazy-builder-CZVLKh0Z.d.cts +304 -0
  127. package/dist/ledger-2NX4L7PN.js +33 -0
  128. package/dist/ledger-2NX4L7PN.js.map +1 -0
  129. package/dist/mime-magic-CBBSOkjm.d.cts +50 -0
  130. package/dist/mime-magic-CBBSOkjm.d.ts +50 -0
  131. package/dist/periods/index.cjs +1035 -0
  132. package/dist/periods/index.cjs.map +1 -0
  133. package/dist/periods/index.d.cts +21 -0
  134. package/dist/periods/index.d.ts +21 -0
  135. package/dist/periods/index.js +25 -0
  136. package/dist/periods/index.js.map +1 -0
  137. package/dist/predicate-SBHmi6D0.d.cts +161 -0
  138. package/dist/predicate-SBHmi6D0.d.ts +161 -0
  139. package/dist/query/index.cjs +1957 -0
  140. package/dist/query/index.cjs.map +1 -0
  141. package/dist/query/index.d.cts +3 -0
  142. package/dist/query/index.d.ts +3 -0
  143. package/dist/query/index.js +62 -0
  144. package/dist/query/index.js.map +1 -0
  145. package/dist/session/index.cjs +487 -0
  146. package/dist/session/index.cjs.map +1 -0
  147. package/dist/session/index.d.cts +45 -0
  148. package/dist/session/index.d.ts +45 -0
  149. package/dist/session/index.js +44 -0
  150. package/dist/session/index.js.map +1 -0
  151. package/dist/shadow/index.cjs +133 -0
  152. package/dist/shadow/index.cjs.map +1 -0
  153. package/dist/shadow/index.d.cts +16 -0
  154. package/dist/shadow/index.d.ts +16 -0
  155. package/dist/shadow/index.js +20 -0
  156. package/dist/shadow/index.js.map +1 -0
  157. package/dist/store/index.cjs +1069 -0
  158. package/dist/store/index.cjs.map +1 -0
  159. package/dist/store/index.d.cts +491 -0
  160. package/dist/store/index.d.ts +491 -0
  161. package/dist/store/index.js +34 -0
  162. package/dist/store/index.js.map +1 -0
  163. package/dist/strategy-BSxFXGzb.d.cts +110 -0
  164. package/dist/strategy-BSxFXGzb.d.ts +110 -0
  165. package/dist/strategy-D-SrOLCl.d.cts +548 -0
  166. package/dist/strategy-D-SrOLCl.d.ts +548 -0
  167. package/dist/sync/index.cjs +1062 -0
  168. package/dist/sync/index.cjs.map +1 -0
  169. package/dist/sync/index.d.cts +42 -0
  170. package/dist/sync/index.d.ts +42 -0
  171. package/dist/sync/index.js +28 -0
  172. package/dist/sync/index.js.map +1 -0
  173. package/dist/team/index.cjs +1233 -0
  174. package/dist/team/index.cjs.map +1 -0
  175. package/dist/team/index.d.cts +117 -0
  176. package/dist/team/index.d.ts +117 -0
  177. package/dist/team/index.js +39 -0
  178. package/dist/team/index.js.map +1 -0
  179. package/dist/tx/index.cjs +212 -0
  180. package/dist/tx/index.cjs.map +1 -0
  181. package/dist/tx/index.d.cts +20 -0
  182. package/dist/tx/index.d.ts +20 -0
  183. package/dist/tx/index.js +20 -0
  184. package/dist/tx/index.js.map +1 -0
  185. package/dist/types-BZpCZB8N.d.ts +7526 -0
  186. package/dist/types-Bfs0qr5F.d.cts +7526 -0
  187. package/dist/ulid-COREQ2RQ.js +9 -0
  188. package/dist/ulid-COREQ2RQ.js.map +1 -0
  189. package/dist/util/index.cjs +230 -0
  190. package/dist/util/index.cjs.map +1 -0
  191. package/dist/util/index.d.cts +77 -0
  192. package/dist/util/index.d.ts +77 -0
  193. package/dist/util/index.js +190 -0
  194. package/dist/util/index.js.map +1 -0
  195. package/package.json +244 -0
@@ -0,0 +1,304 @@
1
+ import { I as IndexDef, C as CollectionIndexes, F as FieldClause, O as Operator } from './predicate-SBHmi6D0.js';
2
+
3
+ /**
4
+ * Persistent, encrypted secondary indexes for lazy-mode collections.
5
+ *
6
+ * Parallel to the in-memory `CollectionIndexes` used by eager mode (see
7
+ * `packages/hub/src/query/indexes.ts`): same logical surface, but entries
8
+ * are materialised as encrypted side-car records (`_idx/<field>/<recordId>`)
9
+ * and bulk-loaded into an in-memory mirror on first query.
10
+ *
11
+ * This module only owns the id-namespace convention, the in-memory mirror,
12
+ * and the typed errors. Write-path integration (PR 2 / ), query-planner
13
+ * dispatch (PR 3 / , PR 4 / ), and the rebuild/reconcile utilities
14
+ * (PR 5 / ) live in other files.
15
+ *
16
+ * See the design spec for the full architecture + threat model.
17
+ */
18
+ /**
19
+ * Reserved id prefix for encrypted index side-car records.
20
+ * Matches the existing `_keyring`, `_ledger_deltas/…`, `_meta/handle`
21
+ * conventions inside a collection's id namespace.
22
+ */
23
+ declare const IDX_PREFIX: "_idx/";
24
+ /**
25
+ * Encode the side-car record id for a (field, recordId) pair.
26
+ *
27
+ * Format: `_idx/<field>/<recordId>` — no escaping. Field names may contain
28
+ * dots (for dotted-path access consistent with eager-mode `readPath`);
29
+ * record ids may contain slashes. The first two slash-separated segments
30
+ * are `_idx` and the field; everything after the *second* slash is the
31
+ * record id verbatim.
32
+ */
33
+ declare function encodeIdxId(field: string, recordId: string): string;
34
+ /**
35
+ * Decode a side-car id back into `{ field, recordId }`, or `null` if the
36
+ * input is not a well-formed idx id. A well-formed id is:
37
+ * - prefixed with `_idx/`
38
+ * - contains a field segment (non-empty, no slashes)
39
+ * - contains a record-id segment (non-empty, may contain slashes)
40
+ */
41
+ declare function decodeIdxId(id: string): {
42
+ field: string;
43
+ recordId: string;
44
+ } | null;
45
+ /**
46
+ * Fast-path predicate for discriminating side-car ids from regular record
47
+ * ids and other reserved namespaces. Used by the hub to filter `list()`
48
+ * results during bulk-load of the in-memory mirror.
49
+ */
50
+ declare function isIdxId(id: string): boolean;
51
+ /**
52
+ * Sorted-value entry returned by `orderedBy()`. Mirrors the body shape
53
+ * used by the write path — but `orderedBy` emits them already sorted by
54
+ * `value` in the requested direction. Consumers (PR 4 / ) treat the
55
+ * array as immutable and paginate via a numeric offset.
56
+ *
57
+ * **Note on `value`:** as of, this is the ORIGINAL TYPED
58
+ * value (number, Date, boolean, etc.), not the stringified bucket key.
59
+ * That's what lets range predicates and `orderedBy` compare numerically
60
+ * instead of stumbling into `'10' < '2'` on `String(n)`.
61
+ */
62
+ interface OrderedEntry {
63
+ readonly recordId: string;
64
+ readonly value: unknown;
65
+ }
66
+ /**
67
+ * Bulk-load row shape accepted by `ingest()`. The `value` field is the
68
+ * decrypted index body's `value` field verbatim.
69
+ */
70
+ interface IngestRow {
71
+ readonly recordId: string;
72
+ readonly value: unknown;
73
+ }
74
+ /**
75
+ * Structured index definition. Single-field indexes carry just a field
76
+ * name; composite indexes carry the ordered list of fields and
77
+ * the synthetic `key` (= fields joined by `COMPOSITE_DELIMITER`) used
78
+ * as the bucket-map key and side-car envelope id segment.
79
+ */
80
+ type PersistedIndexDef = {
81
+ readonly kind: 'single';
82
+ readonly field: string;
83
+ readonly key: string;
84
+ } | {
85
+ readonly kind: 'composite';
86
+ readonly fields: readonly string[];
87
+ readonly key: string;
88
+ };
89
+ /**
90
+ * Delimiter used to synthesize a composite-index key from an ordered
91
+ * field list. Intentionally a character that is extremely unusual in
92
+ * JavaScript object keys (`|`) so collision with a literal field name
93
+ * is vanishingly rare in practice. Composite declarations whose field
94
+ * names contain `|` are rejected at declare-time with an explicit
95
+ * error.
96
+ */
97
+ declare const COMPOSITE_DELIMITER = "|";
98
+ declare function compositeKey(fields: readonly string[]): string;
99
+ declare class PersistedCollectionIndex {
100
+ private readonly indexes;
101
+ private readonly defs;
102
+ /**
103
+ * Declare a single-field index. Subsequent `upsert` / `ingest` calls
104
+ * populate the in-memory mirror; calls before `declare` are no-ops
105
+ * (tolerant bulk-load ordering). Idempotent.
106
+ */
107
+ declare(field: string): void;
108
+ /**
109
+ * Declare a composite (multi-field) index. The synthetic
110
+ * key is `fields.join('|')`; it doubles as the in-memory map key and
111
+ * the `_idx/<key>/<recordId>` side-car field segment. Callers upsert
112
+ * and lookup via the same `key` as single-field indexes, just with a
113
+ * tuple value (JSON-stringified for bucketing).
114
+ */
115
+ declareComposite(fields: readonly string[]): void;
116
+ /**
117
+ * Every declared index's structured definition. Collection walks this
118
+ * when materialising side-cars on put/delete so it can extract a
119
+ * single-field value or a composite tuple appropriately.
120
+ */
121
+ definitions(): PersistedIndexDef[];
122
+ /** True if `field` has been declared as indexable on this mirror. */
123
+ has(field: string): boolean;
124
+ /** All declared field names, in declaration order. */
125
+ fields(): string[];
126
+ /**
127
+ * Bulk-load the mirror from decrypted index bodies. Intended to be
128
+ * called once per field after reading the collection's `_idx/<field>/*`
129
+ * side-cars. Safe to call twice with the same rows — bucket Sets
130
+ * deduplicate recordIds. If `field` is not declared, this is a no-op
131
+ * (tolerates the case where bulk-load runs before `declare()` lands).
132
+ */
133
+ ingest(field: string, rows: readonly IngestRow[]): void;
134
+ /**
135
+ * Incrementally update a record's index entry for one field. Called by
136
+ * `Collection.put()` after the main write succeeds. If
137
+ * `previousValue` is non-null, the record is removed from the old
138
+ * bucket first — this is the update path. Pass `null` for fresh adds.
139
+ * No-op if the field is not declared.
140
+ */
141
+ upsert(recordId: string, field: string, newValue: unknown, previousValue: unknown): void;
142
+ /**
143
+ * Remove a record from the index for one field. Called by
144
+ * `Collection.delete()`. No-op if the field is not declared or
145
+ * the record isn't in the bucket. Empty buckets are dropped to keep
146
+ * the Map clean.
147
+ */
148
+ remove(recordId: string, field: string, value: unknown): void;
149
+ /**
150
+ * Drop all bucket data while preserving field declarations. Called on
151
+ * invalidation (incoming sync changes, keyring rotation) — the next
152
+ * query re-populates via `ingest`.
153
+ */
154
+ clear(): void;
155
+ /**
156
+ * Equality lookup — return the set of record ids whose `field` matches
157
+ * `value`. Returns `null` if the field is not declared (caller falls
158
+ * back to scan or throws `IndexRequiredError`). Returns a shared empty
159
+ * set if the field is declared but no record matches — that set MUST
160
+ * NOT be mutated by the caller.
161
+ */
162
+ lookupEqual(field: string, value: unknown): ReadonlySet<string> | null;
163
+ /**
164
+ * Set lookup — return the union of record ids whose `field` matches any
165
+ * of `values`. Returns `null` if the field is not declared. Returns a
166
+ * fresh (non-shared) Set — safe for the caller to mutate.
167
+ */
168
+ lookupIn(field: string, values: readonly unknown[]): ReadonlySet<string> | null;
169
+ /**
170
+ * Range lookup. Return record ids whose indexed value
171
+ * satisfies the predicate. Comparison happens on the ORIGINAL TYPED
172
+ * value carried in `state.values` — so numeric `<` sorts numerically,
173
+ * not lexicographically on `String(n)`. Returns `null` if the field
174
+ * is not declared.
175
+ *
176
+ * Supported ops: `'<'`, `'<='`, `'>'`, `'>='`, `'between'`. For
177
+ * `'between'`, `value` is `[lo, hi]` and both bounds are inclusive
178
+ * (matches the eager-mode operator contract in `predicate.ts`).
179
+ */
180
+ lookupRange(field: string, op: '<' | '<=' | '>' | '>=' | 'between', value: unknown): ReadonlySet<string> | null;
181
+ /**
182
+ * Sorted iteration — return every entry on `field` as an
183
+ * `OrderedEntry[]`, sorted by the ORIGINAL TYPED value (#275: no more
184
+ * `'10' < '2'` surprises on numeric fields). Consumers paginate with
185
+ * a numeric offset. `OrderedEntry.value` is the typed value.
186
+ */
187
+ orderedBy(field: string, dir: 'asc' | 'desc'): readonly OrderedEntry[] | null;
188
+ }
189
+
190
+ /**
191
+ * Strategy seam between core Collection and the optional indexing
192
+ * subsystem. Core imports `IndexStrategy` and `IndexState` as
193
+ * TYPE-ONLY symbols and `NO_INDEXING` as a tiny runtime stub.
194
+ *
195
+ * The heavy classes — `CollectionIndexes`, `PersistedCollectionIndex`,
196
+ * `LazyQuery` — are only instantiated inside the `withIndexing()`
197
+ * factory under `./active.ts`, which in turn is only reachable through
198
+ * the `@noy-db/hub/indexing` subpath export. A consumer that never
199
+ * imports the subpath ships none of those classes in their bundle
200
+ * (ESM tree-shaking + hub's `"sideEffects": false`).
201
+ *
202
+ * @internal
203
+ */
204
+
205
+ /**
206
+ * Per-collection container for whatever mirrors the active strategy
207
+ * decided to materialize. Both accessors may return `null` — they do
208
+ * for `NO_INDEXING`, and `getEagerIndexes` returns null in a
209
+ * lazy-mode collection even when indexing is active (lazy uses the
210
+ * persisted mirror instead).
211
+ *
212
+ * `isEnabled` is a cheap guard so collection code can short-circuit
213
+ * the full indexing path without inspecting either mirror.
214
+ *
215
+ * @internal
216
+ */
217
+ interface IndexState {
218
+ readonly isEnabled: boolean;
219
+ getEagerIndexes(): CollectionIndexes | null;
220
+ getPersistedIndexes(): PersistedCollectionIndex | null;
221
+ }
222
+ /**
223
+ * Factory that builds one `IndexState` per Collection. Called exactly
224
+ * once inside each Collection constructor with the declared
225
+ * `IndexDef[]` and the lazy-mode flag (so lazy collections get the
226
+ * persisted mirror and eager collections get the in-memory one).
227
+ *
228
+ * @internal
229
+ */
230
+ interface IndexStrategy {
231
+ createState(args: {
232
+ readonly defs: readonly IndexDef[];
233
+ readonly lazy: boolean;
234
+ }): IndexState;
235
+ }
236
+
237
+ /**
238
+ * Lazy-mode query builder.
239
+ *
240
+ * Companion to `Query<T>` in `builder.ts`, but built for collections in lazy
241
+ * mode where `snapshot()` is unavailable — records live in the adapter and
242
+ * are pulled on demand. Dispatches through `PersistedCollectionIndex` to
243
+ * resolve a candidate record-id set, then decrypts only those records.
244
+ *
245
+ * Scope:
246
+ * - `.where(field, '==' | 'in', value)` — dispatched through the index
247
+ * - `.where(field, other-op, value)` — evaluated against the decrypted
248
+ * candidate set (non-indexed ops still require the field to be indexed
249
+ * — we need SOMETHING to scope the candidate set)
250
+ * - `.orderBy(field, dir?)` — dispatched through `orderedBy` when no
251
+ * `==`/`in` clause is present; otherwise applied as an in-memory sort
252
+ * over the candidate set
253
+ * - `.limit(n)` / `.offset(n)` — page slice after filtering
254
+ * - `.toArray()` / `.first()` / `.count()` — terminals
255
+ *
256
+ * Every field referenced by a where or orderBy clause MUST be indexed;
257
+ * otherwise `toArray()` throws `IndexRequiredError`. This is deliberate:
258
+ * silent scan-fallback would hide the very performance cliff that lazy-mode
259
+ * indexes exist to prevent (see `docs/architecture.md` §indexes).
260
+ */
261
+
262
+ interface LazyOrderBy {
263
+ readonly field: string;
264
+ readonly direction: 'asc' | 'desc';
265
+ }
266
+ /**
267
+ * Source abstraction the LazyQuery runs against. Collection implements it.
268
+ * Kept minimal so the builder stays test-friendly.
269
+ */
270
+ interface LazyQuerySource<T> {
271
+ readonly collectionName: string;
272
+ readonly persistedIndexes: PersistedCollectionIndex;
273
+ /** Ensure `_idx/<field>/*` side-cars have been bulk-loaded into the mirror. */
274
+ ensurePersistedIndexesLoaded(): Promise<void>;
275
+ /** Decrypt one record by id, or return null if it's gone. */
276
+ getRecord(id: string): Promise<T | null>;
277
+ }
278
+ interface LazyPlan {
279
+ readonly clauses: readonly FieldClause[];
280
+ readonly orderBy: readonly LazyOrderBy[];
281
+ readonly limit: number | undefined;
282
+ readonly offset: number;
283
+ }
284
+ declare class LazyQuery<T> {
285
+ private readonly source;
286
+ private readonly plan;
287
+ constructor(source: LazyQuerySource<T>, plan?: LazyPlan);
288
+ where<V>(field: string, op: Operator, value: V): LazyQuery<T>;
289
+ orderBy(field: string, direction?: 'asc' | 'desc'): LazyQuery<T>;
290
+ limit(n: number): LazyQuery<T>;
291
+ offset(n: number): LazyQuery<T>;
292
+ toArray(): Promise<T[]>;
293
+ first(): Promise<T | null>;
294
+ count(): Promise<number>;
295
+ /**
296
+ * Resolve the candidate record-id set to decrypt. Returns null when the
297
+ * query has no usable driver — no `==`/`in` clause and no `orderBy`
298
+ * clause that can scope the scan. Callers interpret null as
299
+ * IndexRequiredError (see `toArray`).
300
+ */
301
+ private resolveCandidateIds;
302
+ }
303
+
304
+ export { COMPOSITE_DELIMITER as C, type IndexStrategy as I, type LazyOrderBy as L, type OrderedEntry as O, PersistedCollectionIndex as P, IDX_PREFIX as a, type IndexState as b, type IngestRow as c, LazyQuery as d, type LazyQuerySource as e, type PersistedIndexDef as f, compositeKey as g, decodeIdxId as h, encodeIdxId as i, isIdxId as j };
@@ -0,0 +1,304 @@
1
+ import { I as IndexDef, C as CollectionIndexes, F as FieldClause, O as Operator } from './predicate-SBHmi6D0.cjs';
2
+
3
+ /**
4
+ * Persistent, encrypted secondary indexes for lazy-mode collections.
5
+ *
6
+ * Parallel to the in-memory `CollectionIndexes` used by eager mode (see
7
+ * `packages/hub/src/query/indexes.ts`): same logical surface, but entries
8
+ * are materialised as encrypted side-car records (`_idx/<field>/<recordId>`)
9
+ * and bulk-loaded into an in-memory mirror on first query.
10
+ *
11
+ * This module only owns the id-namespace convention, the in-memory mirror,
12
+ * and the typed errors. Write-path integration (PR 2 / ), query-planner
13
+ * dispatch (PR 3 / , PR 4 / ), and the rebuild/reconcile utilities
14
+ * (PR 5 / ) live in other files.
15
+ *
16
+ * See the design spec for the full architecture + threat model.
17
+ */
18
+ /**
19
+ * Reserved id prefix for encrypted index side-car records.
20
+ * Matches the existing `_keyring`, `_ledger_deltas/…`, `_meta/handle`
21
+ * conventions inside a collection's id namespace.
22
+ */
23
+ declare const IDX_PREFIX: "_idx/";
24
+ /**
25
+ * Encode the side-car record id for a (field, recordId) pair.
26
+ *
27
+ * Format: `_idx/<field>/<recordId>` — no escaping. Field names may contain
28
+ * dots (for dotted-path access consistent with eager-mode `readPath`);
29
+ * record ids may contain slashes. The first two slash-separated segments
30
+ * are `_idx` and the field; everything after the *second* slash is the
31
+ * record id verbatim.
32
+ */
33
+ declare function encodeIdxId(field: string, recordId: string): string;
34
+ /**
35
+ * Decode a side-car id back into `{ field, recordId }`, or `null` if the
36
+ * input is not a well-formed idx id. A well-formed id is:
37
+ * - prefixed with `_idx/`
38
+ * - contains a field segment (non-empty, no slashes)
39
+ * - contains a record-id segment (non-empty, may contain slashes)
40
+ */
41
+ declare function decodeIdxId(id: string): {
42
+ field: string;
43
+ recordId: string;
44
+ } | null;
45
+ /**
46
+ * Fast-path predicate for discriminating side-car ids from regular record
47
+ * ids and other reserved namespaces. Used by the hub to filter `list()`
48
+ * results during bulk-load of the in-memory mirror.
49
+ */
50
+ declare function isIdxId(id: string): boolean;
51
+ /**
52
+ * Sorted-value entry returned by `orderedBy()`. Mirrors the body shape
53
+ * used by the write path — but `orderedBy` emits them already sorted by
54
+ * `value` in the requested direction. Consumers (PR 4 / ) treat the
55
+ * array as immutable and paginate via a numeric offset.
56
+ *
57
+ * **Note on `value`:** as of, this is the ORIGINAL TYPED
58
+ * value (number, Date, boolean, etc.), not the stringified bucket key.
59
+ * That's what lets range predicates and `orderedBy` compare numerically
60
+ * instead of stumbling into `'10' < '2'` on `String(n)`.
61
+ */
62
+ interface OrderedEntry {
63
+ readonly recordId: string;
64
+ readonly value: unknown;
65
+ }
66
+ /**
67
+ * Bulk-load row shape accepted by `ingest()`. The `value` field is the
68
+ * decrypted index body's `value` field verbatim.
69
+ */
70
+ interface IngestRow {
71
+ readonly recordId: string;
72
+ readonly value: unknown;
73
+ }
74
+ /**
75
+ * Structured index definition. Single-field indexes carry just a field
76
+ * name; composite indexes carry the ordered list of fields and
77
+ * the synthetic `key` (= fields joined by `COMPOSITE_DELIMITER`) used
78
+ * as the bucket-map key and side-car envelope id segment.
79
+ */
80
+ type PersistedIndexDef = {
81
+ readonly kind: 'single';
82
+ readonly field: string;
83
+ readonly key: string;
84
+ } | {
85
+ readonly kind: 'composite';
86
+ readonly fields: readonly string[];
87
+ readonly key: string;
88
+ };
89
+ /**
90
+ * Delimiter used to synthesize a composite-index key from an ordered
91
+ * field list. Intentionally a character that is extremely unusual in
92
+ * JavaScript object keys (`|`) so collision with a literal field name
93
+ * is vanishingly rare in practice. Composite declarations whose field
94
+ * names contain `|` are rejected at declare-time with an explicit
95
+ * error.
96
+ */
97
+ declare const COMPOSITE_DELIMITER = "|";
98
+ declare function compositeKey(fields: readonly string[]): string;
99
+ declare class PersistedCollectionIndex {
100
+ private readonly indexes;
101
+ private readonly defs;
102
+ /**
103
+ * Declare a single-field index. Subsequent `upsert` / `ingest` calls
104
+ * populate the in-memory mirror; calls before `declare` are no-ops
105
+ * (tolerant bulk-load ordering). Idempotent.
106
+ */
107
+ declare(field: string): void;
108
+ /**
109
+ * Declare a composite (multi-field) index. The synthetic
110
+ * key is `fields.join('|')`; it doubles as the in-memory map key and
111
+ * the `_idx/<key>/<recordId>` side-car field segment. Callers upsert
112
+ * and lookup via the same `key` as single-field indexes, just with a
113
+ * tuple value (JSON-stringified for bucketing).
114
+ */
115
+ declareComposite(fields: readonly string[]): void;
116
+ /**
117
+ * Every declared index's structured definition. Collection walks this
118
+ * when materialising side-cars on put/delete so it can extract a
119
+ * single-field value or a composite tuple appropriately.
120
+ */
121
+ definitions(): PersistedIndexDef[];
122
+ /** True if `field` has been declared as indexable on this mirror. */
123
+ has(field: string): boolean;
124
+ /** All declared field names, in declaration order. */
125
+ fields(): string[];
126
+ /**
127
+ * Bulk-load the mirror from decrypted index bodies. Intended to be
128
+ * called once per field after reading the collection's `_idx/<field>/*`
129
+ * side-cars. Safe to call twice with the same rows — bucket Sets
130
+ * deduplicate recordIds. If `field` is not declared, this is a no-op
131
+ * (tolerates the case where bulk-load runs before `declare()` lands).
132
+ */
133
+ ingest(field: string, rows: readonly IngestRow[]): void;
134
+ /**
135
+ * Incrementally update a record's index entry for one field. Called by
136
+ * `Collection.put()` after the main write succeeds. If
137
+ * `previousValue` is non-null, the record is removed from the old
138
+ * bucket first — this is the update path. Pass `null` for fresh adds.
139
+ * No-op if the field is not declared.
140
+ */
141
+ upsert(recordId: string, field: string, newValue: unknown, previousValue: unknown): void;
142
+ /**
143
+ * Remove a record from the index for one field. Called by
144
+ * `Collection.delete()`. No-op if the field is not declared or
145
+ * the record isn't in the bucket. Empty buckets are dropped to keep
146
+ * the Map clean.
147
+ */
148
+ remove(recordId: string, field: string, value: unknown): void;
149
+ /**
150
+ * Drop all bucket data while preserving field declarations. Called on
151
+ * invalidation (incoming sync changes, keyring rotation) — the next
152
+ * query re-populates via `ingest`.
153
+ */
154
+ clear(): void;
155
+ /**
156
+ * Equality lookup — return the set of record ids whose `field` matches
157
+ * `value`. Returns `null` if the field is not declared (caller falls
158
+ * back to scan or throws `IndexRequiredError`). Returns a shared empty
159
+ * set if the field is declared but no record matches — that set MUST
160
+ * NOT be mutated by the caller.
161
+ */
162
+ lookupEqual(field: string, value: unknown): ReadonlySet<string> | null;
163
+ /**
164
+ * Set lookup — return the union of record ids whose `field` matches any
165
+ * of `values`. Returns `null` if the field is not declared. Returns a
166
+ * fresh (non-shared) Set — safe for the caller to mutate.
167
+ */
168
+ lookupIn(field: string, values: readonly unknown[]): ReadonlySet<string> | null;
169
+ /**
170
+ * Range lookup. Return record ids whose indexed value
171
+ * satisfies the predicate. Comparison happens on the ORIGINAL TYPED
172
+ * value carried in `state.values` — so numeric `<` sorts numerically,
173
+ * not lexicographically on `String(n)`. Returns `null` if the field
174
+ * is not declared.
175
+ *
176
+ * Supported ops: `'<'`, `'<='`, `'>'`, `'>='`, `'between'`. For
177
+ * `'between'`, `value` is `[lo, hi]` and both bounds are inclusive
178
+ * (matches the eager-mode operator contract in `predicate.ts`).
179
+ */
180
+ lookupRange(field: string, op: '<' | '<=' | '>' | '>=' | 'between', value: unknown): ReadonlySet<string> | null;
181
+ /**
182
+ * Sorted iteration — return every entry on `field` as an
183
+ * `OrderedEntry[]`, sorted by the ORIGINAL TYPED value (#275: no more
184
+ * `'10' < '2'` surprises on numeric fields). Consumers paginate with
185
+ * a numeric offset. `OrderedEntry.value` is the typed value.
186
+ */
187
+ orderedBy(field: string, dir: 'asc' | 'desc'): readonly OrderedEntry[] | null;
188
+ }
189
+
190
+ /**
191
+ * Strategy seam between core Collection and the optional indexing
192
+ * subsystem. Core imports `IndexStrategy` and `IndexState` as
193
+ * TYPE-ONLY symbols and `NO_INDEXING` as a tiny runtime stub.
194
+ *
195
+ * The heavy classes — `CollectionIndexes`, `PersistedCollectionIndex`,
196
+ * `LazyQuery` — are only instantiated inside the `withIndexing()`
197
+ * factory under `./active.ts`, which in turn is only reachable through
198
+ * the `@noy-db/hub/indexing` subpath export. A consumer that never
199
+ * imports the subpath ships none of those classes in their bundle
200
+ * (ESM tree-shaking + hub's `"sideEffects": false`).
201
+ *
202
+ * @internal
203
+ */
204
+
205
+ /**
206
+ * Per-collection container for whatever mirrors the active strategy
207
+ * decided to materialize. Both accessors may return `null` — they do
208
+ * for `NO_INDEXING`, and `getEagerIndexes` returns null in a
209
+ * lazy-mode collection even when indexing is active (lazy uses the
210
+ * persisted mirror instead).
211
+ *
212
+ * `isEnabled` is a cheap guard so collection code can short-circuit
213
+ * the full indexing path without inspecting either mirror.
214
+ *
215
+ * @internal
216
+ */
217
+ interface IndexState {
218
+ readonly isEnabled: boolean;
219
+ getEagerIndexes(): CollectionIndexes | null;
220
+ getPersistedIndexes(): PersistedCollectionIndex | null;
221
+ }
222
+ /**
223
+ * Factory that builds one `IndexState` per Collection. Called exactly
224
+ * once inside each Collection constructor with the declared
225
+ * `IndexDef[]` and the lazy-mode flag (so lazy collections get the
226
+ * persisted mirror and eager collections get the in-memory one).
227
+ *
228
+ * @internal
229
+ */
230
+ interface IndexStrategy {
231
+ createState(args: {
232
+ readonly defs: readonly IndexDef[];
233
+ readonly lazy: boolean;
234
+ }): IndexState;
235
+ }
236
+
237
+ /**
238
+ * Lazy-mode query builder.
239
+ *
240
+ * Companion to `Query<T>` in `builder.ts`, but built for collections in lazy
241
+ * mode where `snapshot()` is unavailable — records live in the adapter and
242
+ * are pulled on demand. Dispatches through `PersistedCollectionIndex` to
243
+ * resolve a candidate record-id set, then decrypts only those records.
244
+ *
245
+ * Scope:
246
+ * - `.where(field, '==' | 'in', value)` — dispatched through the index
247
+ * - `.where(field, other-op, value)` — evaluated against the decrypted
248
+ * candidate set (non-indexed ops still require the field to be indexed
249
+ * — we need SOMETHING to scope the candidate set)
250
+ * - `.orderBy(field, dir?)` — dispatched through `orderedBy` when no
251
+ * `==`/`in` clause is present; otherwise applied as an in-memory sort
252
+ * over the candidate set
253
+ * - `.limit(n)` / `.offset(n)` — page slice after filtering
254
+ * - `.toArray()` / `.first()` / `.count()` — terminals
255
+ *
256
+ * Every field referenced by a where or orderBy clause MUST be indexed;
257
+ * otherwise `toArray()` throws `IndexRequiredError`. This is deliberate:
258
+ * silent scan-fallback would hide the very performance cliff that lazy-mode
259
+ * indexes exist to prevent (see `docs/architecture.md` §indexes).
260
+ */
261
+
262
+ interface LazyOrderBy {
263
+ readonly field: string;
264
+ readonly direction: 'asc' | 'desc';
265
+ }
266
+ /**
267
+ * Source abstraction the LazyQuery runs against. Collection implements it.
268
+ * Kept minimal so the builder stays test-friendly.
269
+ */
270
+ interface LazyQuerySource<T> {
271
+ readonly collectionName: string;
272
+ readonly persistedIndexes: PersistedCollectionIndex;
273
+ /** Ensure `_idx/<field>/*` side-cars have been bulk-loaded into the mirror. */
274
+ ensurePersistedIndexesLoaded(): Promise<void>;
275
+ /** Decrypt one record by id, or return null if it's gone. */
276
+ getRecord(id: string): Promise<T | null>;
277
+ }
278
+ interface LazyPlan {
279
+ readonly clauses: readonly FieldClause[];
280
+ readonly orderBy: readonly LazyOrderBy[];
281
+ readonly limit: number | undefined;
282
+ readonly offset: number;
283
+ }
284
+ declare class LazyQuery<T> {
285
+ private readonly source;
286
+ private readonly plan;
287
+ constructor(source: LazyQuerySource<T>, plan?: LazyPlan);
288
+ where<V>(field: string, op: Operator, value: V): LazyQuery<T>;
289
+ orderBy(field: string, direction?: 'asc' | 'desc'): LazyQuery<T>;
290
+ limit(n: number): LazyQuery<T>;
291
+ offset(n: number): LazyQuery<T>;
292
+ toArray(): Promise<T[]>;
293
+ first(): Promise<T | null>;
294
+ count(): Promise<number>;
295
+ /**
296
+ * Resolve the candidate record-id set to decrypt. Returns null when the
297
+ * query has no usable driver — no `==`/`in` clause and no `orderBy`
298
+ * clause that can scope the scan. Callers interpret null as
299
+ * IndexRequiredError (see `toArray`).
300
+ */
301
+ private resolveCandidateIds;
302
+ }
303
+
304
+ export { COMPOSITE_DELIMITER as C, type IndexStrategy as I, type LazyOrderBy as L, type OrderedEntry as O, PersistedCollectionIndex as P, IDX_PREFIX as a, type IndexState as b, type IngestRow as c, LazyQuery as d, type LazyQuerySource as e, type PersistedIndexDef as f, compositeKey as g, decodeIdxId as h, encodeIdxId as i, isIdxId as j };
@@ -0,0 +1,33 @@
1
+ import "./chunk-UF3BUNQZ.js";
2
+ import {
3
+ LEDGER_COLLECTION,
4
+ LEDGER_DELTAS_COLLECTION,
5
+ LedgerStore,
6
+ applyPatch,
7
+ computePatch
8
+ } from "./chunk-XHFOENR2.js";
9
+ import {
10
+ canonicalJson,
11
+ envelopePayloadHash,
12
+ hashEntry,
13
+ paddedIndex,
14
+ parseIndex,
15
+ sha256Hex
16
+ } from "./chunk-CIMZBAZB.js";
17
+ import "./chunk-ZRG4V3F5.js";
18
+ import "./chunk-MR4424N3.js";
19
+ import "./chunk-ACLDOTNQ.js";
20
+ export {
21
+ LEDGER_COLLECTION,
22
+ LEDGER_DELTAS_COLLECTION,
23
+ LedgerStore,
24
+ applyPatch,
25
+ canonicalJson,
26
+ computePatch,
27
+ envelopePayloadHash,
28
+ hashEntry,
29
+ paddedIndex,
30
+ parseIndex,
31
+ sha256Hex
32
+ };
33
+ //# sourceMappingURL=ledger-2NX4L7PN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}