mindoodb 0.0.89 → 0.0.91

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 (114) hide show
  1. package/README.md +1 -0
  2. package/dist/browser/index.mjs +15 -0
  3. package/dist/core/BaseMindooDB.d.ts +18 -0
  4. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  5. package/dist/core/BaseMindooDB.js +57 -0
  6. package/dist/core/BaseMindooDB.js.map +1 -1
  7. package/dist/core/BaseMindooTenant.d.ts +3 -0
  8. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  9. package/dist/core/BaseMindooTenant.js +28 -0
  10. package/dist/core/BaseMindooTenant.js.map +1 -1
  11. package/dist/core/BaseMindooTenantDirectory.d.ts +2 -1
  12. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  13. package/dist/core/BaseMindooTenantDirectory.js +10 -2
  14. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  15. package/dist/core/expressions/builder.d.ts +30 -0
  16. package/dist/core/expressions/builder.d.ts.map +1 -1
  17. package/dist/core/expressions/builder.js +42 -0
  18. package/dist/core/expressions/builder.js.map +1 -1
  19. package/dist/core/expressions/evaluateExpression.d.ts +10 -0
  20. package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
  21. package/dist/core/expressions/evaluateExpression.js +66 -1
  22. package/dist/core/expressions/evaluateExpression.js.map +1 -1
  23. package/dist/core/expressions/formulaSource.d.ts.map +1 -1
  24. package/dist/core/expressions/formulaSource.js +5 -0
  25. package/dist/core/expressions/formulaSource.js.map +1 -1
  26. package/dist/core/expressions/index.d.ts +1 -1
  27. package/dist/core/expressions/index.d.ts.map +1 -1
  28. package/dist/core/expressions/index.js +2 -1
  29. package/dist/core/expressions/index.js.map +1 -1
  30. package/dist/core/expressions/metadata.d.ts +2 -2
  31. package/dist/core/expressions/metadata.d.ts.map +1 -1
  32. package/dist/core/expressions/metadata.js +14 -0
  33. package/dist/core/expressions/metadata.js.map +1 -1
  34. package/dist/core/expressions/types.d.ts +1 -1
  35. package/dist/core/expressions/types.d.ts.map +1 -1
  36. package/dist/core/filing/FilingIndex.d.ts +209 -0
  37. package/dist/core/filing/FilingIndex.d.ts.map +1 -0
  38. package/dist/core/filing/FilingIndex.js +689 -0
  39. package/dist/core/filing/FilingIndex.js.map +1 -0
  40. package/dist/core/filing/MindooFilingStore.d.ts +200 -0
  41. package/dist/core/filing/MindooFilingStore.d.ts.map +1 -0
  42. package/dist/core/filing/MindooFilingStore.js +515 -0
  43. package/dist/core/filing/MindooFilingStore.js.map +1 -0
  44. package/dist/core/filing/index.d.ts +4 -0
  45. package/dist/core/filing/index.d.ts.map +1 -0
  46. package/dist/core/filing/index.js +22 -0
  47. package/dist/core/filing/index.js.map +1 -0
  48. package/dist/core/filing/refs.d.ts +56 -0
  49. package/dist/core/filing/refs.d.ts.map +1 -0
  50. package/dist/core/filing/refs.js +109 -0
  51. package/dist/core/filing/refs.js.map +1 -0
  52. package/dist/core/index.d.ts +2 -1
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/index.js +5 -2
  55. package/dist/core/index.js.map +1 -1
  56. package/dist/core/indexing/createViewDataProvider.d.ts +6 -0
  57. package/dist/core/indexing/createViewDataProvider.d.ts.map +1 -1
  58. package/dist/core/indexing/createViewDataProvider.js +26 -1
  59. package/dist/core/indexing/createViewDataProvider.js.map +1 -1
  60. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +43 -1
  61. package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
  62. package/dist/core/indexing/summary/DocumentSummaryStore.js +302 -2
  63. package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
  64. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts +33 -2
  65. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts.map +1 -1
  66. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js +129 -26
  67. package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js.map +1 -1
  68. package/dist/core/indexing/summary/types.d.ts +40 -0
  69. package/dist/core/indexing/summary/types.d.ts.map +1 -1
  70. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +27 -2
  71. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
  72. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +106 -1
  73. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
  74. package/dist/core/indexing/virtualviews/VirtualView.d.ts +70 -5
  75. package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
  76. package/dist/core/indexing/virtualviews/VirtualView.js +329 -28
  77. package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
  78. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +4 -22
  79. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
  80. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +175 -129
  81. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
  82. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +28 -0
  83. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -1
  84. package/dist/core/indexing/virtualviews/VirtualViewFactory.js +62 -0
  85. package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -1
  86. package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.d.ts +75 -0
  87. package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.d.ts.map +1 -0
  88. package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.js +163 -0
  89. package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.js.map +1 -0
  90. package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.d.ts +112 -0
  91. package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.d.ts.map +1 -0
  92. package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.js +296 -0
  93. package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.js.map +1 -0
  94. package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts +2 -0
  95. package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts.map +1 -1
  96. package/dist/core/indexing/virtualviews/VirtualViewNavigator.js +10 -9
  97. package/dist/core/indexing/virtualviews/VirtualViewNavigator.js.map +1 -1
  98. package/dist/core/indexing/virtualviews/index.d.ts +2 -0
  99. package/dist/core/indexing/virtualviews/index.d.ts.map +1 -1
  100. package/dist/core/indexing/virtualviews/index.js +8 -1
  101. package/dist/core/indexing/virtualviews/index.js.map +1 -1
  102. package/dist/core/query/executeQuery.d.ts.map +1 -1
  103. package/dist/core/query/executeQuery.js +4 -0
  104. package/dist/core/query/executeQuery.js.map +1 -1
  105. package/dist/core/query/executeQueryInclude.d.ts.map +1 -1
  106. package/dist/core/query/executeQueryInclude.js +4 -0
  107. package/dist/core/query/executeQueryInclude.js.map +1 -1
  108. package/dist/core/query/queryView.d.ts.map +1 -1
  109. package/dist/core/query/queryView.js +4 -0
  110. package/dist/core/query/queryView.js.map +1 -1
  111. package/dist/core/types.d.ts +20 -0
  112. package/dist/core/types.d.ts.map +1 -1
  113. package/dist/core/types.js.map +1 -1
  114. package/package.json +1 -1
@@ -0,0 +1,689 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilingIndex = exports.FILING_FOLDER_FIELD_PREFIX = exports.FILING_FOLDER_TYPE = exports.FILING_SIDECAR_TYPE = void 0;
4
+ exports.readFolderFields = readFolderFields;
5
+ const idGeneration_1 = require("../utils/idGeneration");
6
+ const refs_1 = require("./refs");
7
+ /** Document type of a filing sidecar (one per filed document and store). */
8
+ exports.FILING_SIDECAR_TYPE = "$filing";
9
+ /** Document type of a folder definition. */
10
+ exports.FILING_FOLDER_TYPE = "$folder";
11
+ /**
12
+ * Field-name prefix of folder memberships in a sidecar: `"f:<folderId>": filedAt`.
13
+ *
14
+ * Memberships are top-level fields rather than a nested `folders` map on
15
+ * purpose: when two replicas file the same document for the first time
16
+ * concurrently, each would create its own nested map object, and Automerge
17
+ * keeps only one of two concurrently assigned objects — the other replica's
18
+ * folders would be lost. Top-level keys of the (seeded, shared) document
19
+ * root merge key by key.
20
+ */
21
+ exports.FILING_FOLDER_FIELD_PREFIX = "f:";
22
+ /** Keep at most this many changed refs in the change log (older consumers get "all"). */
23
+ const MAX_LOG_REFS = 20000;
24
+ /**
25
+ * Membership index of one filing store in one database: which documents
26
+ * are filed in which folders, plus the store's folder definitions.
27
+ *
28
+ * It is maintained incrementally from the filing database's changefeed —
29
+ * restricted by id prefix to the store's own documents, so a shared team
30
+ * database with lots of other documents costs nothing extra — and shared by
31
+ * every view that joins the store (one index per database + store, like the
32
+ * summary buffer). Views ask {@link changesSince} which filed documents
33
+ * changed membership since their last update and re-evaluate exactly those.
34
+ *
35
+ * A document may have more than one sidecar (e.g. after a key rotation
36
+ * changed the deterministic sidecar id); its folders are the union.
37
+ */
38
+ class FilingIndex {
39
+ constructor(db, store, idPrefix) {
40
+ this.generation = (0, idGeneration_1.generateRandomUuid)();
41
+ this.seq = 0;
42
+ this.cursor = null;
43
+ /**
44
+ * Deletions of filed documents in the filing database itself (physical
45
+ * key -> deleted at), read from the database's metadata changefeed. Feeds
46
+ * the automatic cleanup (MindooFilingStore). Not persisted: rebuilt from
47
+ * the (in-memory, cheap) metadata index once per session.
48
+ */
49
+ this.deletedTargets = new Map();
50
+ this.targetCursor = null;
51
+ this.sidecars = new Map();
52
+ this.sidecarsByRef = new Map();
53
+ this.membersByFolder = new Map();
54
+ this.folders = new Map();
55
+ this.folderIdByDocId = new Map();
56
+ /** First seq the change log still covers completely. */
57
+ this.logStartSeq = 0;
58
+ this.log = [];
59
+ this.logRefCount = 0;
60
+ /** Seq ranges a crashed previous run may have handed out (see DocumentSummaryStore). */
61
+ this.lostSeqRanges = [];
62
+ this.updatePromise = null;
63
+ this.updatePending = false;
64
+ this.listeners = new Set();
65
+ // cache support
66
+ this.cacheManager = null;
67
+ this.cachePrefix = null;
68
+ this.dirty = false;
69
+ // Journal (see flushToCache): changes since the last flush, as latest states.
70
+ this.pendingSidecars = new Map();
71
+ this.pendingFolders = new Set();
72
+ this.pendingFolderDocIds = new Map();
73
+ this.pendingLog = [];
74
+ /** deletedTargets changed since the last flush. */
75
+ this.pendingDeletedTargets = false;
76
+ /** Whether this instance wrote the current snapshot (else the next flush writes one). */
77
+ this.ownSnapshot = false;
78
+ this.journalEntriesSinceSnapshot = 0;
79
+ this.nextJournalSegment = 0;
80
+ this.journalWriterId = (0, idGeneration_1.generateRandomUuid)().replace(/-/g, "").slice(0, 12);
81
+ this.restored = false;
82
+ this.restorePromise = null;
83
+ this.db = db;
84
+ this.store = store;
85
+ this.idPrefix = idPrefix;
86
+ this.base = { tenantId: db.getTenant().getId(), dbId: db.getStore().getId() };
87
+ }
88
+ /** Name of the filing store this index covers. */
89
+ getStoreName() {
90
+ return this.store;
91
+ }
92
+ /** The filing database. */
93
+ getDatabase() {
94
+ return this.db;
95
+ }
96
+ /** Physical identity of the filing database. */
97
+ getBase() {
98
+ return { ...this.base };
99
+ }
100
+ /**
101
+ * Register a listener fired after an update changed the index (membership
102
+ * or folder definitions).
103
+ *
104
+ * @returns An unsubscribe function.
105
+ */
106
+ onDidChange(listener) {
107
+ this.listeners.add(listener);
108
+ return () => {
109
+ this.listeners.delete(listener);
110
+ };
111
+ }
112
+ /**
113
+ * Bring the index up to date with the filing database. Concurrent calls
114
+ * share the running catch-up; a call arriving mid-run triggers exactly one
115
+ * follow-up run, so the returned promise always covers changes made
116
+ * before the call.
117
+ */
118
+ async update() {
119
+ if (this.updatePromise) {
120
+ // The running loop re-checks the flag before finishing, so the
121
+ // running promise also covers this call's changes.
122
+ this.updatePending = true;
123
+ return this.updatePromise;
124
+ }
125
+ this.updatePromise = (async () => {
126
+ try {
127
+ await this.ensureRestored();
128
+ do {
129
+ this.updatePending = false;
130
+ await this.runUpdate();
131
+ } while (this.updatePending);
132
+ }
133
+ finally {
134
+ this.updatePromise = null;
135
+ }
136
+ })();
137
+ return this.updatePromise;
138
+ }
139
+ async runUpdate() {
140
+ const changedRefs = new Set();
141
+ let foldersChanged = false;
142
+ for await (const { doc, cursor } of this.db.iterateChangesSince(this.cursor, { idPrefix: this.idPrefix })) {
143
+ const docId = doc.getId();
144
+ if (doc.isDeleted()) {
145
+ if (this.sidecars.has(docId)) {
146
+ this.applySidecar(docId, null, changedRefs);
147
+ }
148
+ else {
149
+ const folderId = this.folderIdByDocId.get(docId);
150
+ if (folderId !== undefined) {
151
+ foldersChanged = this.applyFolder(folderId, null, changedRefs) || foldersChanged;
152
+ }
153
+ }
154
+ }
155
+ else {
156
+ const data = doc.getData();
157
+ if (data.store === this.store && data.type === exports.FILING_SIDECAR_TYPE && typeof data.ref === "string") {
158
+ let physRef = null;
159
+ try {
160
+ physRef = (0, refs_1.physicalFilingRefKey)((0, refs_1.resolveFilingRef)(data.ref, this.base));
161
+ }
162
+ catch {
163
+ physRef = null;
164
+ }
165
+ this.applySidecar(docId, physRef === null ? null : { ref: physRef, folders: readFolderFields(data) }, changedRefs);
166
+ }
167
+ else if (data.store === this.store &&
168
+ data.type === exports.FILING_FOLDER_TYPE &&
169
+ typeof data.folderId === "string" &&
170
+ data.folderId) {
171
+ this.folderIdByDocId.set(docId, data.folderId);
172
+ this.pendingFolderDocIds.set(docId, data.folderId);
173
+ foldersChanged = this.applyFolder(data.folderId, {
174
+ folderId: data.folderId,
175
+ name: typeof data.name === "string" ? data.name : data.folderId,
176
+ parentId: typeof data.parentId === "string" && data.parentId ? data.parentId : null,
177
+ role: typeof data.role === "string" && data.role ? data.role : null,
178
+ design: typeof data.design === "string" && data.design ? data.design : null,
179
+ deleted: data.deleted === true,
180
+ }, changedRefs) || foldersChanged;
181
+ }
182
+ }
183
+ this.cursor = cursor;
184
+ this.dirty = true;
185
+ }
186
+ // Deletions (and undeletions) of filed documents of this database:
187
+ // metadata only, nothing is loaded or decrypted.
188
+ const targetPrefix = `${this.base.tenantId}/${this.base.dbId}/`;
189
+ let deletionsChanged = false;
190
+ for await (const { docId, isDeleted, lastModified, cursor } of this.db.iterateChangeMetadataSince(this.targetCursor)) {
191
+ const physRef = `${targetPrefix}${docId}`;
192
+ if (isDeleted && this.sidecarsByRef.has(physRef)) {
193
+ if (this.deletedTargets.get(physRef) !== lastModified) {
194
+ this.deletedTargets.set(physRef, lastModified);
195
+ deletionsChanged = true;
196
+ }
197
+ }
198
+ else if (this.deletedTargets.delete(physRef)) {
199
+ deletionsChanged = true;
200
+ }
201
+ this.targetCursor = cursor;
202
+ }
203
+ if (deletionsChanged) {
204
+ foldersChanged = true;
205
+ this.pendingDeletedTargets = true;
206
+ this.dirty = true;
207
+ }
208
+ if (changedRefs.size > 0) {
209
+ this.appendLog(Array.from(changedRefs));
210
+ }
211
+ if (this.dirty) {
212
+ this.cacheManager?.markDirty();
213
+ }
214
+ if (changedRefs.size > 0 || foldersChanged) {
215
+ for (const listener of this.listeners) {
216
+ try {
217
+ listener();
218
+ }
219
+ catch {
220
+ // listeners must not break the index
221
+ }
222
+ }
223
+ }
224
+ }
225
+ applySidecar(sidecarId, next, changedRefs) {
226
+ const previous = this.sidecars.get(sidecarId);
227
+ const affectedRefs = new Set();
228
+ if (previous)
229
+ affectedRefs.add(previous.ref);
230
+ if (next)
231
+ affectedRefs.add(next.ref);
232
+ const before = new Map();
233
+ for (const ref of affectedRefs) {
234
+ before.set(ref, this.foldersOf(ref).join("\u0000"));
235
+ }
236
+ if (previous) {
237
+ this.sidecarsByRef.get(previous.ref)?.delete(sidecarId);
238
+ if (this.sidecarsByRef.get(previous.ref)?.size === 0) {
239
+ this.sidecarsByRef.delete(previous.ref);
240
+ }
241
+ this.sidecars.delete(sidecarId);
242
+ for (const folderId of Object.keys(previous.folders)) {
243
+ this.refreshMembership(folderId, previous.ref);
244
+ }
245
+ }
246
+ if (next) {
247
+ this.sidecars.set(sidecarId, next);
248
+ let ids = this.sidecarsByRef.get(next.ref);
249
+ if (!ids) {
250
+ ids = new Set();
251
+ this.sidecarsByRef.set(next.ref, ids);
252
+ }
253
+ ids.add(sidecarId);
254
+ for (const folderId of Object.keys(next.folders)) {
255
+ this.refreshMembership(folderId, next.ref);
256
+ }
257
+ }
258
+ for (const ref of affectedRefs) {
259
+ if (before.get(ref) !== this.foldersOf(ref).join("\u0000")) {
260
+ changedRefs.add(ref);
261
+ }
262
+ }
263
+ this.pendingSidecars.set(sidecarId, next);
264
+ }
265
+ /** Recompute whether `ref` is a raw member of `folderId` (any sidecar has the key). */
266
+ refreshMembership(folderId, ref) {
267
+ let isMember = false;
268
+ for (const sidecarId of this.sidecarsByRef.get(ref) ?? []) {
269
+ if (Object.prototype.hasOwnProperty.call(this.sidecars.get(sidecarId)?.folders ?? {}, folderId)) {
270
+ isMember = true;
271
+ break;
272
+ }
273
+ }
274
+ let members = this.membersByFolder.get(folderId);
275
+ if (isMember) {
276
+ if (!members) {
277
+ members = new Set();
278
+ this.membersByFolder.set(folderId, members);
279
+ }
280
+ members.add(ref);
281
+ }
282
+ else if (members) {
283
+ members.delete(ref);
284
+ if (members.size === 0) {
285
+ this.membersByFolder.delete(folderId);
286
+ }
287
+ }
288
+ }
289
+ /** @returns whether the folder definitions changed. */
290
+ applyFolder(folderId, next, changedRefs) {
291
+ const previous = this.folders.get(folderId);
292
+ const wasHidden = previous?.deleted === true;
293
+ const isHidden = next === null ? wasHidden || previous !== undefined : next.deleted;
294
+ if (next === null) {
295
+ // The definition document itself was (hard) deleted: keep the folder
296
+ // hidden, orphaned sidecar keys must not resurface.
297
+ if (previous) {
298
+ this.folders.set(folderId, { ...previous, deleted: true });
299
+ }
300
+ }
301
+ else {
302
+ this.folders.set(folderId, next);
303
+ }
304
+ if (wasHidden !== isHidden) {
305
+ for (const ref of this.membersByFolder.get(folderId) ?? []) {
306
+ changedRefs.add(ref);
307
+ }
308
+ }
309
+ if (this.folders.has(folderId)) {
310
+ this.pendingFolders.add(folderId);
311
+ }
312
+ return JSON.stringify(previous ?? null) !== JSON.stringify(this.folders.get(folderId) ?? null);
313
+ }
314
+ appendLog(refs) {
315
+ this.seq++;
316
+ this.log.push([this.seq, refs]);
317
+ this.pendingLog.push([this.seq, refs]);
318
+ this.logRefCount += refs.length;
319
+ while (this.logRefCount > MAX_LOG_REFS && this.log.length > 1) {
320
+ const [droppedSeq, droppedRefs] = this.log.shift();
321
+ this.logRefCount -= droppedRefs.length;
322
+ this.logStartSeq = droppedSeq;
323
+ }
324
+ }
325
+ /**
326
+ * Documents whose effective folders changed after position
327
+ * (`generation`, `seq`). Pass `null` for "never synced" (yields `"all"`).
328
+ */
329
+ changesSince(position) {
330
+ const current = { generation: this.generation, seq: this.seq };
331
+ if (!position ||
332
+ position.generation !== this.generation ||
333
+ position.seq < this.logStartSeq ||
334
+ position.seq > this.seq ||
335
+ this.lostSeqRanges.some(([start, end]) => position.seq > start && position.seq <= end)) {
336
+ return { ...current, refs: "all" };
337
+ }
338
+ const refs = new Set();
339
+ for (const [seq, entryRefs] of this.log) {
340
+ if (seq > position.seq) {
341
+ for (const ref of entryRefs) {
342
+ refs.add(ref);
343
+ }
344
+ }
345
+ }
346
+ return { ...current, refs };
347
+ }
348
+ /** Current position of the index (for {@link changesSince}). */
349
+ getPosition() {
350
+ return { generation: this.generation, seq: this.seq };
351
+ }
352
+ /**
353
+ * Effective folder ids of a filed document (physical key), sorted.
354
+ * Folders whose definition was deleted are left out.
355
+ */
356
+ foldersOf(physRef) {
357
+ const sidecarIds = this.sidecarsByRef.get(physRef);
358
+ if (!sidecarIds) {
359
+ return EMPTY;
360
+ }
361
+ const result = new Set();
362
+ for (const sidecarId of sidecarIds) {
363
+ for (const folderId of Object.keys(this.sidecars.get(sidecarId)?.folders ?? {})) {
364
+ if (this.folders.get(folderId)?.deleted !== true) {
365
+ result.add(folderId);
366
+ }
367
+ }
368
+ }
369
+ return result.size === 0 ? EMPTY : Array.from(result).sort();
370
+ }
371
+ /** When the document was filed into `folderId` (earliest of its sidecars), or `null`. */
372
+ filedAt(physRef, folderId) {
373
+ let result = null;
374
+ for (const sidecarId of this.sidecarsByRef.get(physRef) ?? []) {
375
+ const at = this.sidecars.get(sidecarId)?.folders[folderId];
376
+ if (typeof at === "number" && (result === null || at < result)) {
377
+ result = at;
378
+ }
379
+ }
380
+ return result;
381
+ }
382
+ /** Physical keys of the documents in a folder (empty for deleted folders). */
383
+ membersOf(folderId) {
384
+ if (this.folders.get(folderId)?.deleted === true) {
385
+ return EMPTY;
386
+ }
387
+ return Array.from(this.membersByFolder.get(folderId) ?? []);
388
+ }
389
+ /** Physical keys of every document whose sidecars list `folderId`, also when the folder is deleted. */
390
+ allSidecarRefsInFolder(folderId) {
391
+ return Array.from(this.membersByFolder.get(folderId) ?? []);
392
+ }
393
+ /**
394
+ * Filed documents of the filing database itself whose latest state is
395
+ * "deleted", with the deletion time (physical key -> epoch ms).
396
+ */
397
+ getDeletedTargets() {
398
+ return this.deletedTargets;
399
+ }
400
+ /** Physical keys of every document with at least one membership key (also of deleted folders). */
401
+ allSidecarRefs() {
402
+ const result = [];
403
+ for (const [ref, ids] of this.sidecarsByRef) {
404
+ for (const id of ids) {
405
+ if (Object.keys(this.sidecars.get(id)?.folders ?? {}).length > 0) {
406
+ result.push(ref);
407
+ break;
408
+ }
409
+ }
410
+ }
411
+ return result;
412
+ }
413
+ /** Physical keys of every document filed in any (visible) folder. */
414
+ allFiledRefs() {
415
+ const result = [];
416
+ for (const ref of this.sidecarsByRef.keys()) {
417
+ if (this.foldersOf(ref).length > 0) {
418
+ result.push(ref);
419
+ }
420
+ }
421
+ return result;
422
+ }
423
+ /** Sidecar document ids of a filed document. */
424
+ sidecarIdsOf(physRef) {
425
+ return Array.from(this.sidecarsByRef.get(physRef) ?? []);
426
+ }
427
+ /** Stored (relative) reference string of a physical key, as written into sidecars. */
428
+ formatRef(physRef) {
429
+ return (0, refs_1.formatFilingRef)((0, refs_1.resolveFilingRef)(`mdbref://${physRef}`, this.base), this.base);
430
+ }
431
+ /** A folder definition (also for deleted folders, see `deleted`), or `undefined`. */
432
+ getFolder(folderId) {
433
+ const folder = this.folders.get(folderId);
434
+ return folder ? { ...folder } : undefined;
435
+ }
436
+ /** All visible (not deleted) folder definitions. */
437
+ listFolders() {
438
+ const result = [];
439
+ for (const folder of this.folders.values()) {
440
+ if (!folder.deleted) {
441
+ const { deleted: _deleted, ...rest } = folder;
442
+ result.push(rest);
443
+ }
444
+ }
445
+ return result;
446
+ }
447
+ /** Number of documents with at least one visible folder. */
448
+ getFiledCount() {
449
+ return this.allFiledRefs().length;
450
+ }
451
+ // ---------------------------------------------------------------------------
452
+ // Cache persistence
453
+ // ---------------------------------------------------------------------------
454
+ /**
455
+ * Attach cache persistence (recommended prefix: `<dbCachePrefix>/filing/<store tag>`).
456
+ * The persisted state is restored lazily on the first {@link update}.
457
+ */
458
+ attachCache(cacheManager, cachePrefix) {
459
+ this.cacheManager = cacheManager;
460
+ this.cachePrefix = cachePrefix;
461
+ cacheManager.register(this);
462
+ }
463
+ getCachePrefix() {
464
+ return this.cachePrefix ?? `filing/${this.idPrefix}`;
465
+ }
466
+ hasDirtyState() {
467
+ return this.dirty;
468
+ }
469
+ clearDirty() {
470
+ this.dirty = false;
471
+ }
472
+ /** Drop dirty markers without flushing (the records are about to be deleted). */
473
+ discardDirtyState() {
474
+ this.dirty = false;
475
+ }
476
+ /**
477
+ * Persist the index as snapshot + journal: normally only what changed
478
+ * since the last flush is appended as a small segment (`filingj`); the
479
+ * full snapshot (`filing`) is written when this instance has none yet
480
+ * (also right after a restore), when the journal outgrows 10 % of the
481
+ * index (min. 200 changes), and on forced flushes (close). Each instance
482
+ * writes its own segment chain; the snapshot names the chain that
483
+ * continues it, so several instances on one key never interleave. A failed
484
+ * write keeps the pending changes for the next flush.
485
+ */
486
+ async flushToCache(store, options) {
487
+ const pendingCount = this.pendingSidecars.size + this.pendingFolders.size;
488
+ const wantSnapshot = options?.force === true ||
489
+ !this.ownSnapshot ||
490
+ this.journalEntriesSinceSnapshot + pendingCount > Math.max(200, this.sidecars.size * 0.1);
491
+ const scalars = {
492
+ generation: this.generation,
493
+ seq: this.seq,
494
+ cursor: this.cursor,
495
+ logStartSeq: this.logStartSeq,
496
+ lostSeqRanges: this.lostSeqRanges,
497
+ targetCursor: this.targetCursor,
498
+ };
499
+ if (wantSnapshot) {
500
+ const journalBase = this.nextJournalSegment;
501
+ const payload = {
502
+ v: 1,
503
+ ...scalars,
504
+ sidecars: Array.from(this.sidecars, ([id, state]) => [id, state.ref, state.folders]),
505
+ folders: Array.from(this.folders),
506
+ folderIdByDocId: Array.from(this.folderIdByDocId),
507
+ log: this.log,
508
+ deletedTargets: Array.from(this.deletedTargets),
509
+ journalWriter: this.journalWriterId,
510
+ journalBase,
511
+ };
512
+ await store.put("filing", this.getCachePrefix(), new TextEncoder().encode(JSON.stringify(payload)));
513
+ this.clearPending();
514
+ this.ownSnapshot = true;
515
+ this.journalEntriesSinceSnapshot = 0;
516
+ try {
517
+ const prefix = `${this.getCachePrefix()}/`;
518
+ for (const id of await store.list("filingj")) {
519
+ if (id.startsWith(prefix))
520
+ await store.delete("filingj", id);
521
+ }
522
+ }
523
+ catch {
524
+ // Stale segments are ignored on restore (other chain or before the base).
525
+ }
526
+ return 1;
527
+ }
528
+ const segment = {
529
+ ...scalars,
530
+ sidecars: Array.from(this.pendingSidecars, ([id, state]) => state ? [id, state.ref, state.folders] : [id, null]),
531
+ folders: Array.from(this.pendingFolders, (folderId) => [folderId, this.folders.get(folderId)]),
532
+ folderIdByDocId: Array.from(this.pendingFolderDocIds),
533
+ log: this.pendingLog,
534
+ ...(this.pendingDeletedTargets ? { deletedTargets: Array.from(this.deletedTargets) } : {}),
535
+ };
536
+ const segmentNo = this.nextJournalSegment;
537
+ await store.put("filingj", `${this.getCachePrefix()}/${this.journalWriterId}/${String(segmentNo).padStart(10, "0")}`, new TextEncoder().encode(JSON.stringify(segment)));
538
+ this.nextJournalSegment = segmentNo + 1;
539
+ this.journalEntriesSinceSnapshot += pendingCount;
540
+ this.clearPending();
541
+ return 1;
542
+ }
543
+ clearPending() {
544
+ this.pendingSidecars = new Map();
545
+ this.pendingFolders = new Set();
546
+ this.pendingFolderDocIds = new Map();
547
+ this.pendingLog = [];
548
+ this.pendingDeletedTargets = false;
549
+ }
550
+ async ensureRestored() {
551
+ if (this.restored || !this.cacheManager) {
552
+ this.restored = true;
553
+ return;
554
+ }
555
+ if (!this.restorePromise) {
556
+ this.restorePromise = this.restoreFromCache().finally(() => {
557
+ this.restored = true;
558
+ });
559
+ }
560
+ return this.restorePromise;
561
+ }
562
+ async restoreFromCache() {
563
+ const store = this.cacheManager?.getStore();
564
+ if (!store) {
565
+ return;
566
+ }
567
+ let payload;
568
+ try {
569
+ const bytes = await store.get("filing", this.getCachePrefix());
570
+ if (!bytes) {
571
+ return;
572
+ }
573
+ payload = JSON.parse(new TextDecoder().decode(bytes));
574
+ if (payload.v !== 1) {
575
+ return;
576
+ }
577
+ }
578
+ catch {
579
+ return;
580
+ }
581
+ this.generation = payload.generation;
582
+ this.seq = payload.seq;
583
+ this.cursor = payload.cursor ?? null;
584
+ this.folders = new Map(payload.folders);
585
+ this.folderIdByDocId = new Map(payload.folderIdByDocId);
586
+ this.logStartSeq = payload.logStartSeq;
587
+ this.log = payload.log ?? [];
588
+ this.lostSeqRanges = payload.lostSeqRanges ?? [];
589
+ // Older caches lack the deletion tracking: rescan it (metadata only).
590
+ if (payload.targetCursor !== undefined && Array.isArray(payload.deletedTargets)) {
591
+ this.targetCursor = payload.targetCursor;
592
+ this.deletedTargets = new Map(payload.deletedTargets);
593
+ }
594
+ const ignore = new Set();
595
+ for (const [id, ref, folders] of payload.sidecars) {
596
+ this.applySidecar(id, { ref, folders }, ignore);
597
+ }
598
+ // Replay the journal chain that continues this snapshot (in order; a
599
+ // missing or unreadable segment ends the replay — the changefeed catches
600
+ // up from the last good cursor).
601
+ if (typeof payload.journalWriter === "string") {
602
+ try {
603
+ const prefix = `${this.getCachePrefix()}/${payload.journalWriter}/`;
604
+ const numbers = (await store.list("filingj"))
605
+ .filter((id) => id.startsWith(prefix))
606
+ .map((id) => Number(id.slice(prefix.length)))
607
+ .filter((n) => Number.isInteger(n) && n >= (payload.journalBase ?? 0))
608
+ .sort((a, b) => a - b);
609
+ let expected = payload.journalBase ?? 0;
610
+ for (const n of numbers) {
611
+ if (n !== expected)
612
+ break;
613
+ const bytes = await store.get("filingj", `${prefix}${String(n).padStart(10, "0")}`);
614
+ if (!bytes)
615
+ break;
616
+ const segment = JSON.parse(new TextDecoder().decode(bytes));
617
+ for (const entry of segment.sidecars) {
618
+ this.applySidecar(entry[0], entry[1] === null ? null : { ref: entry[1], folders: entry[2] }, ignore);
619
+ }
620
+ for (const [folderId, folder] of segment.folders)
621
+ this.folders.set(folderId, folder);
622
+ for (const [docId, folderId] of segment.folderIdByDocId)
623
+ this.folderIdByDocId.set(docId, folderId);
624
+ this.log.push(...segment.log);
625
+ this.generation = segment.generation;
626
+ this.seq = segment.seq;
627
+ this.cursor = segment.cursor ?? null;
628
+ this.logStartSeq = segment.logStartSeq;
629
+ this.lostSeqRanges = segment.lostSeqRanges ?? this.lostSeqRanges;
630
+ if (segment.targetCursor !== undefined && payload.targetCursor !== undefined) {
631
+ this.targetCursor = segment.targetCursor;
632
+ }
633
+ if (Array.isArray(segment.deletedTargets) && payload.targetCursor !== undefined) {
634
+ this.deletedTargets = new Map(segment.deletedTargets);
635
+ }
636
+ expected = n + 1;
637
+ }
638
+ }
639
+ catch {
640
+ // Keep what was replayed.
641
+ }
642
+ }
643
+ this.log = this.log.filter(([seq]) => seq > this.logStartSeq);
644
+ this.logRefCount = this.log.reduce((sum, [, refs]) => sum + refs.length, 0);
645
+ while (this.logRefCount > MAX_LOG_REFS && this.log.length > 1) {
646
+ const [droppedSeq, droppedRefs] = this.log.shift();
647
+ this.logRefCount -= droppedRefs.length;
648
+ this.logStartSeq = droppedSeq;
649
+ }
650
+ this.clearPending();
651
+ // A previous run may have handed out seqs after this persisted state
652
+ // (and views may have consumed them): jump past them.
653
+ const jump = 2 ** 30;
654
+ this.lostSeqRanges = [...this.lostSeqRanges, [this.seq, this.seq + jump]];
655
+ if (this.lostSeqRanges.length > 20) {
656
+ const dropped = this.lostSeqRanges.splice(0, this.lostSeqRanges.length - 20);
657
+ this.logStartSeq = Math.max(this.logStartSeq, dropped[dropped.length - 1][1]);
658
+ }
659
+ this.seq += jump;
660
+ // This instance continues with its own chain: its first flush writes a
661
+ // snapshot.
662
+ this.ownSnapshot = false;
663
+ this.dirty = true;
664
+ }
665
+ }
666
+ exports.FilingIndex = FilingIndex;
667
+ const EMPTY = [];
668
+ Object.freeze(EMPTY);
669
+ /** Read the `f:<folderId> -> filedAt` membership fields of a sidecar. */
670
+ function readFolderFields(data) {
671
+ const result = {};
672
+ for (const [key, at] of Object.entries(data)) {
673
+ if (!key.startsWith(exports.FILING_FOLDER_FIELD_PREFIX) || key.length === exports.FILING_FOLDER_FIELD_PREFIX.length) {
674
+ continue;
675
+ }
676
+ const folderId = key.slice(exports.FILING_FOLDER_FIELD_PREFIX.length);
677
+ if (typeof at === "number" && Number.isFinite(at)) {
678
+ result[folderId] = at;
679
+ }
680
+ else if (at instanceof Date) {
681
+ result[folderId] = at.getTime();
682
+ }
683
+ else if (at !== null && at !== undefined) {
684
+ result[folderId] = 0;
685
+ }
686
+ }
687
+ return result;
688
+ }
689
+ //# sourceMappingURL=FilingIndex.js.map