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.
- package/README.md +1 -0
- package/dist/browser/index.mjs +15 -0
- package/dist/core/BaseMindooDB.d.ts +18 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +57 -0
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +3 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +28 -0
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +2 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +10 -2
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/expressions/builder.d.ts +30 -0
- package/dist/core/expressions/builder.d.ts.map +1 -1
- package/dist/core/expressions/builder.js +42 -0
- package/dist/core/expressions/builder.js.map +1 -1
- package/dist/core/expressions/evaluateExpression.d.ts +10 -0
- package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
- package/dist/core/expressions/evaluateExpression.js +66 -1
- package/dist/core/expressions/evaluateExpression.js.map +1 -1
- package/dist/core/expressions/formulaSource.d.ts.map +1 -1
- package/dist/core/expressions/formulaSource.js +5 -0
- package/dist/core/expressions/formulaSource.js.map +1 -1
- package/dist/core/expressions/index.d.ts +1 -1
- package/dist/core/expressions/index.d.ts.map +1 -1
- package/dist/core/expressions/index.js +2 -1
- package/dist/core/expressions/index.js.map +1 -1
- package/dist/core/expressions/metadata.d.ts +2 -2
- package/dist/core/expressions/metadata.d.ts.map +1 -1
- package/dist/core/expressions/metadata.js +14 -0
- package/dist/core/expressions/metadata.js.map +1 -1
- package/dist/core/expressions/types.d.ts +1 -1
- package/dist/core/expressions/types.d.ts.map +1 -1
- package/dist/core/filing/FilingIndex.d.ts +209 -0
- package/dist/core/filing/FilingIndex.d.ts.map +1 -0
- package/dist/core/filing/FilingIndex.js +689 -0
- package/dist/core/filing/FilingIndex.js.map +1 -0
- package/dist/core/filing/MindooFilingStore.d.ts +200 -0
- package/dist/core/filing/MindooFilingStore.d.ts.map +1 -0
- package/dist/core/filing/MindooFilingStore.js +515 -0
- package/dist/core/filing/MindooFilingStore.js.map +1 -0
- package/dist/core/filing/index.d.ts +4 -0
- package/dist/core/filing/index.d.ts.map +1 -0
- package/dist/core/filing/index.js +22 -0
- package/dist/core/filing/index.js.map +1 -0
- package/dist/core/filing/refs.d.ts +56 -0
- package/dist/core/filing/refs.d.ts.map +1 -0
- package/dist/core/filing/refs.js +109 -0
- package/dist/core/filing/refs.js.map +1 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexing/createViewDataProvider.d.ts +6 -0
- package/dist/core/indexing/createViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/createViewDataProvider.js +26 -1
- package/dist/core/indexing/createViewDataProvider.js.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +43 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.js +302 -2
- package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts +33 -2
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js +129 -26
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js.map +1 -1
- package/dist/core/indexing/summary/types.d.ts +40 -0
- package/dist/core/indexing/summary/types.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +27 -2
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +106 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualView.d.ts +70 -5
- package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualView.js +329 -28
- package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +4 -22
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +175 -129
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +28 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js +62 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.d.ts +75 -0
- package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.js +163 -0
- package/dist/core/indexing/virtualviews/VirtualViewFilingJoin.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.d.ts +112 -0
- package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.js +296 -0
- package/dist/core/indexing/virtualviews/VirtualViewMergedNavigator.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts +2 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.js +10 -9
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.js.map +1 -1
- package/dist/core/indexing/virtualviews/index.d.ts +2 -0
- package/dist/core/indexing/virtualviews/index.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/index.js +8 -1
- package/dist/core/indexing/virtualviews/index.js.map +1 -1
- package/dist/core/query/executeQuery.d.ts.map +1 -1
- package/dist/core/query/executeQuery.js +4 -0
- package/dist/core/query/executeQuery.js.map +1 -1
- package/dist/core/query/executeQueryInclude.d.ts.map +1 -1
- package/dist/core/query/executeQueryInclude.js +4 -0
- package/dist/core/query/executeQueryInclude.js.map +1 -1
- package/dist/core/query/queryView.d.ts.map +1 -1
- package/dist/core/query/queryView.js +4 -0
- package/dist/core/query/queryView.js.map +1 -1
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- 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
|