mindoodb 0.0.67 → 0.0.70
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/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +2 -2
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +2 -0
- package/dist/core/BaseMindooDB.d.ts +45 -6
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +152 -35
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +3 -3
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +2 -2
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +3 -3
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
- package/dist/core/copy/attachments.d.ts +1 -1
- package/dist/core/copy/attachments.js +1 -1
- package/dist/core/copy/entryRewrite.d.ts +1 -1
- package/dist/core/copy/entryRewrite.js +1 -1
- package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
- package/dist/core/expressions/evaluateExpression.js +7 -1
- package/dist/core/expressions/evaluateExpression.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexing/summary/extractSummaryFields.d.ts +10 -1
- package/dist/core/indexing/summary/extractSummaryFields.d.ts.map +1 -1
- package/dist/core/indexing/summary/extractSummaryFields.js +34 -7
- package/dist/core/indexing/summary/extractSummaryFields.js.map +1 -1
- package/dist/core/indexing/summary/types.d.ts +6 -0
- package/dist/core/indexing/summary/types.d.ts.map +1 -1
- package/dist/core/indexing/summary/types.js +8 -1
- package/dist/core/indexing/summary/types.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/utils/idGeneration.d.ts +29 -7
- package/dist/core/utils/idGeneration.d.ts.map +1 -1
- package/dist/core/utils/idGeneration.js +73 -13
- package/dist/core/utils/idGeneration.js.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +3 -1
- package/dist/core/utils/index.js.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +3 -3
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/DeviceDiscoveryService.js +2 -2
- package/dist/node/server/DeviceDiscoveryService.js.map +1 -1
- package/dist/node/server/SystemAdminAuth.js +2 -2
- package/dist/node/server/SystemAdminAuth.js.map +1 -1
- package/dist/node/server/peer/PeerAuthService.js +2 -2
- package/dist/node/server/peer/PeerAuthService.js.map +1 -1
- package/package.json +8 -9
|
@@ -7,7 +7,6 @@ exports.BaseMindooDB = exports.CUSTOM_DOC_ID_INITIAL_CHANGE_BYTES = void 0;
|
|
|
7
7
|
const automerge_adapter_1 = require("./automerge-adapter");
|
|
8
8
|
const values_1 = require("./values");
|
|
9
9
|
const storeEntryTime_1 = require("./storeEntryTime");
|
|
10
|
-
const uuid_1 = require("uuid");
|
|
11
10
|
const types_1 = require("./types");
|
|
12
11
|
const types_2 = require("./appendonlystores/types");
|
|
13
12
|
const syncStores_1 = require("./appendonlystores/syncStores");
|
|
@@ -483,6 +482,19 @@ class BaseMindooDB {
|
|
|
483
482
|
* flushed to L2 under the new changeSeq) as if it were current.
|
|
484
483
|
*/
|
|
485
484
|
this.docEntryGeneration = new Map();
|
|
485
|
+
/**
|
|
486
|
+
* Ids of entries this instance wrote itself (local writes) that the next
|
|
487
|
+
* {@link syncStoreChanges} run has not consumed yet. Local writes update
|
|
488
|
+
* the index directly but do not advance the processed-entry cursor, so
|
|
489
|
+
* that run (at the latest on the next open) re-reads them as "new". For a
|
|
490
|
+
* cached doc that is a no-op; for an uncached one the metadata-first path
|
|
491
|
+
* used to force a changeSeq bump, re-emitting unchanged documents to every
|
|
492
|
+
* changefeed consumer (summary, views, full-text) and invalidating their
|
|
493
|
+
* L2 records. Entries listed here are known to be indexed already, so the
|
|
494
|
+
* bump is only taken when the recomputed index state actually differs.
|
|
495
|
+
* Persisted in the metadata checkpoint (the restart case).
|
|
496
|
+
*/
|
|
497
|
+
this.locallyIndexedEntryIds = new Set();
|
|
486
498
|
this.cacheMetaDirty = false;
|
|
487
499
|
/**
|
|
488
500
|
* Per-document fingerprint (`changeSeq:automergeHeads`) of the state last
|
|
@@ -492,6 +504,15 @@ class BaseMindooDB {
|
|
|
492
504
|
* that were merely loaded and cached, not changed).
|
|
493
505
|
*/
|
|
494
506
|
this.lastFlushedDocState = new Map();
|
|
507
|
+
/**
|
|
508
|
+
* Per-document `changeSeq` of the record known to be in the L2 cache.
|
|
509
|
+
* Unlike {@link lastFlushedDocState} it is persisted in the metadata
|
|
510
|
+
* checkpoint, so after a restart the background warmer can tell which
|
|
511
|
+
* documents need work without reading every L2 record. A missing entry
|
|
512
|
+
* means "unknown" (legacy checkpoint): the record header is read once.
|
|
513
|
+
* Maintained only via {@link rememberL2Record} / {@link forgetL2Record}.
|
|
514
|
+
*/
|
|
515
|
+
this.l2RecordChangeSeqs = new Map();
|
|
495
516
|
/**
|
|
496
517
|
* Tenant KeyBag fingerprint observed at the time the in-memory index
|
|
497
518
|
* was last reconciled with the underlying store.
|
|
@@ -1583,7 +1604,29 @@ class BaseMindooDB {
|
|
|
1583
1604
|
value.set(headerBytes, 4);
|
|
1584
1605
|
value.set(amBinary, 4 + headerBytes.length);
|
|
1585
1606
|
await store.put("doc", `${prefix}/${docId}`, value);
|
|
1607
|
+
this.rememberL2Record(docId, changeSeq, flushKey);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Record that the L2 cache holds `docId` at `changeSeq` (with the given
|
|
1611
|
+
* `changeSeq:automergeHeads` fingerprint). Marks the metadata checkpoint
|
|
1612
|
+
* dirty when the persisted per-doc changeSeq moved, so the next flush
|
|
1613
|
+
* carries it (flush-before-evict writes records outside a flush).
|
|
1614
|
+
*/
|
|
1615
|
+
rememberL2Record(docId, changeSeq, flushKey) {
|
|
1586
1616
|
this.lastFlushedDocState.set(docId, flushKey);
|
|
1617
|
+
if (this.l2RecordChangeSeqs.get(docId) !== changeSeq) {
|
|
1618
|
+
this.l2RecordChangeSeqs.set(docId, changeSeq);
|
|
1619
|
+
this.cacheMetaDirty = true;
|
|
1620
|
+
this.cacheManager?.markDirty();
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
/** Forget everything known about the L2 record of `docId`. */
|
|
1624
|
+
forgetL2Record(docId) {
|
|
1625
|
+
this.lastFlushedDocState.delete(docId);
|
|
1626
|
+
if (this.l2RecordChangeSeqs.delete(docId)) {
|
|
1627
|
+
this.cacheMetaDirty = true;
|
|
1628
|
+
this.cacheManager?.markDirty();
|
|
1629
|
+
}
|
|
1587
1630
|
}
|
|
1588
1631
|
exportMetadataCheckpoint() {
|
|
1589
1632
|
const checkpoint = {
|
|
@@ -1607,6 +1650,16 @@ class BaseMindooDB {
|
|
|
1607
1650
|
if (!this.supportsCursorScan(this.store)) {
|
|
1608
1651
|
checkpoint.processedEntryIds = this.processedEntryIds;
|
|
1609
1652
|
}
|
|
1653
|
+
// Additive field: older checkpoints restore an empty set (the next
|
|
1654
|
+
// syncStoreChanges then re-emits those local writes once, as before).
|
|
1655
|
+
if (this.locallyIndexedEntryIds.size > 0) {
|
|
1656
|
+
checkpoint.locallyIndexedEntryIds = Array.from(this.locallyIndexedEntryIds);
|
|
1657
|
+
}
|
|
1658
|
+
// Additive field: older checkpoints restore an empty map, which makes
|
|
1659
|
+
// the warmer read each record header once (the previous behavior).
|
|
1660
|
+
if (this.l2RecordChangeSeqs.size > 0) {
|
|
1661
|
+
checkpoint.l2RecordChangeSeqs = Object.fromEntries(this.l2RecordChangeSeqs);
|
|
1662
|
+
}
|
|
1610
1663
|
// Persist the KeyBag fingerprint observed at the last reconciliation
|
|
1611
1664
|
// so the next restore can decide whether to skip the visibility
|
|
1612
1665
|
// metadata scan. `null` is acceptable - older checkpoints simply
|
|
@@ -1665,6 +1718,18 @@ class BaseMindooDB {
|
|
|
1665
1718
|
if (checkpoint.processedEntryIds) {
|
|
1666
1719
|
this.processedEntryIds = checkpoint.processedEntryIds;
|
|
1667
1720
|
}
|
|
1721
|
+
this.locallyIndexedEntryIds = new Set(Array.isArray(checkpoint.locallyIndexedEntryIds)
|
|
1722
|
+
? checkpoint.locallyIndexedEntryIds.filter((id) => typeof id === "string")
|
|
1723
|
+
: []);
|
|
1724
|
+
this.l2RecordChangeSeqs.clear();
|
|
1725
|
+
if (checkpoint.l2RecordChangeSeqs &&
|
|
1726
|
+
typeof checkpoint.l2RecordChangeSeqs === "object") {
|
|
1727
|
+
for (const [docId, changeSeq] of Object.entries(checkpoint.l2RecordChangeSeqs)) {
|
|
1728
|
+
if (typeof changeSeq === "number") {
|
|
1729
|
+
this.l2RecordChangeSeqs.set(docId, changeSeq);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1668
1733
|
this.lastReconciledKeyBagFingerprint =
|
|
1669
1734
|
typeof checkpoint.keyBagFingerprint === "string"
|
|
1670
1735
|
? checkpoint.keyBagFingerprint
|
|
@@ -1786,6 +1851,17 @@ class BaseMindooDB {
|
|
|
1786
1851
|
const docIds = await store.list("doc");
|
|
1787
1852
|
const docPrefix = `${prefix}/`;
|
|
1788
1853
|
const allOwnedKeys = docIds.filter((id) => id.startsWith(docPrefix));
|
|
1854
|
+
// The persisted per-doc L2 state must not outlive its record (a
|
|
1855
|
+
// record lost outside this instance would otherwise be skipped by the
|
|
1856
|
+
// warmer forever).
|
|
1857
|
+
if (this.l2RecordChangeSeqs.size > 0) {
|
|
1858
|
+
const ownedDocIds = new Set(allOwnedKeys.map((key) => key.slice(docPrefix.length)));
|
|
1859
|
+
for (const docId of Array.from(this.l2RecordChangeSeqs.keys())) {
|
|
1860
|
+
if (!ownedDocIds.has(docId)) {
|
|
1861
|
+
this.forgetL2Record(docId);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1789
1865
|
if (this.restoreToL2) {
|
|
1790
1866
|
// L2-only restore: leave L1 empty; let lazy reads via
|
|
1791
1867
|
// tryLoadFromL2 promote individual docs as they are needed. We
|
|
@@ -1836,9 +1912,14 @@ class BaseMindooDB {
|
|
|
1836
1912
|
const docId = batch[i].slice(docPrefix.length);
|
|
1837
1913
|
await store.delete("doc", batch[i]);
|
|
1838
1914
|
this.docCache.delete(docId);
|
|
1839
|
-
this.
|
|
1915
|
+
this.forgetL2Record(docId);
|
|
1840
1916
|
continue;
|
|
1841
1917
|
}
|
|
1918
|
+
// Legacy checkpoints lack the per-doc L2 state; the header
|
|
1919
|
+
// just read provides it for free.
|
|
1920
|
+
if (!this.l2RecordChangeSeqs.has(deserialized.internal.id)) {
|
|
1921
|
+
this.rememberL2Record(deserialized.internal.id, deserialized.persistedChangeSeq, `${deserialized.persistedChangeSeq}:${deserialized.persistedHeads.join(",")}`);
|
|
1922
|
+
}
|
|
1842
1923
|
await this.storeCachedDocument(deserialized.internal);
|
|
1843
1924
|
restoredDocs++;
|
|
1844
1925
|
}
|
|
@@ -2223,6 +2304,17 @@ class BaseMindooDB {
|
|
|
2223
2304
|
}
|
|
2224
2305
|
this.indexLookupStaleFrom = null;
|
|
2225
2306
|
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Write entries created by this instance to the docs store. The callers
|
|
2309
|
+
* index the affected documents right away (see
|
|
2310
|
+
* {@link locallyIndexedEntryIds}).
|
|
2311
|
+
*/
|
|
2312
|
+
async putLocalEntries(entries) {
|
|
2313
|
+
await this.store.putEntries(entries);
|
|
2314
|
+
for (const entry of entries) {
|
|
2315
|
+
this.locallyIndexedEntryIds.add(entry.id);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2226
2318
|
/**
|
|
2227
2319
|
* Register an automerge hash -> entry ID mapping for a document.
|
|
2228
2320
|
* This is used to resolve Automerge dependency hashes to entry IDs.
|
|
@@ -2637,7 +2729,7 @@ class BaseMindooDB {
|
|
|
2637
2729
|
async purgeMaterializedDocument(docId) {
|
|
2638
2730
|
this.docCache.delete(docId);
|
|
2639
2731
|
this.dirtyDocIds.delete(docId);
|
|
2640
|
-
this.
|
|
2732
|
+
this.forgetL2Record(docId);
|
|
2641
2733
|
this.automergeHashToEntryId.delete(docId);
|
|
2642
2734
|
this.forgetScannedEntryMetadata(docId);
|
|
2643
2735
|
// Plaintext summary values must not outlive the purge either.
|
|
@@ -2964,10 +3056,12 @@ class BaseMindooDB {
|
|
|
2964
3056
|
this.indexLookupStaleFrom = null;
|
|
2965
3057
|
this.docCache.clear();
|
|
2966
3058
|
this.lastFlushedDocState.clear();
|
|
3059
|
+
this.l2RecordChangeSeqs.clear();
|
|
2967
3060
|
this.automergeHashToEntryId.clear();
|
|
2968
3061
|
this.clearScannedEntryMetadata();
|
|
2969
3062
|
this.processedEntryIds = [];
|
|
2970
3063
|
this.processedEntryCursor = null;
|
|
3064
|
+
this.locallyIndexedEntryIds.clear();
|
|
2971
3065
|
this.nextChangeSeq = 1;
|
|
2972
3066
|
this.cacheMetaDirty = true;
|
|
2973
3067
|
this.cacheManager?.markDirty();
|
|
@@ -3153,7 +3247,11 @@ class BaseMindooDB {
|
|
|
3153
3247
|
// Force the re-emit only when the batch contains actual replay
|
|
3154
3248
|
// entries (create/change/delete/undelete): a snapshot-only batch
|
|
3155
3249
|
// changes nothing observable and may keep the idempotent skip.
|
|
3156
|
-
|
|
3250
|
+
// Replay entries this instance wrote itself are already indexed
|
|
3251
|
+
// (the local write bumped changeSeq), so they only re-emit when
|
|
3252
|
+
// the recomputed state differs.
|
|
3253
|
+
const batchHasReplayEntries = docLifecycleEntries.some((e) => this.isDocumentReplayEntry(e) &&
|
|
3254
|
+
!this.locallyIndexedEntryIds.has(e.id));
|
|
3157
3255
|
this.updateIndex(docId, lastModified, isDeleted, representativeEntry.decryptionKeyId, "visible", awaitingWitness, witnessed, batchHasReplayEntries);
|
|
3158
3256
|
// `allDocMetadata` is this document's complete history in both
|
|
3159
3257
|
// branches above. Keeping it lets the materialization that
|
|
@@ -3261,6 +3359,9 @@ class BaseMindooDB {
|
|
|
3261
3359
|
if (!this.supportsCursorScan(this.store)) {
|
|
3262
3360
|
this.processedEntryIds.push(...newEntryMetadata.map((em) => em.id));
|
|
3263
3361
|
}
|
|
3362
|
+
for (const entryMeta of newEntryMetadata) {
|
|
3363
|
+
this.locallyIndexedEntryIds.delete(entryMeta.id);
|
|
3364
|
+
}
|
|
3264
3365
|
this.processedEntryCursor = nextCursor;
|
|
3265
3366
|
this.cacheMetaDirty = true;
|
|
3266
3367
|
this.cacheManager?.markDirty();
|
|
@@ -3888,7 +3989,7 @@ class BaseMindooDB {
|
|
|
3888
3989
|
allEntries.push(...preparedDoc.entries);
|
|
3889
3990
|
}
|
|
3890
3991
|
if (allEntries.length > 0) {
|
|
3891
|
-
await this.
|
|
3992
|
+
await this.putLocalEntries(allEntries);
|
|
3892
3993
|
}
|
|
3893
3994
|
for (const { index, internalDoc } of prepared) {
|
|
3894
3995
|
await this.storeCachedDocument(internalDoc);
|
|
@@ -4204,7 +4305,7 @@ class BaseMindooDB {
|
|
|
4204
4305
|
encryptedData: encryptedPayload,
|
|
4205
4306
|
};
|
|
4206
4307
|
// Store entry
|
|
4207
|
-
await this.
|
|
4308
|
+
await this.putLocalEntries([fullEntry]);
|
|
4208
4309
|
// Register automerge hash -> entry ID mapping
|
|
4209
4310
|
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
4210
4311
|
// A freshly created entry is versioned and un-witnessed, so the document is
|
|
@@ -6796,7 +6897,7 @@ class BaseMindooDB {
|
|
|
6796
6897
|
if (prepared.length === 0) {
|
|
6797
6898
|
return;
|
|
6798
6899
|
}
|
|
6799
|
-
await this.
|
|
6900
|
+
await this.putLocalEntries(prepared.map((p) => p.fullEntry));
|
|
6800
6901
|
for (const p of prepared) {
|
|
6801
6902
|
await this.applyLifecycleEntryLocally(p, { markDirty: false });
|
|
6802
6903
|
}
|
|
@@ -6829,7 +6930,7 @@ class BaseMindooDB {
|
|
|
6829
6930
|
*/
|
|
6830
6931
|
async writeLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck) {
|
|
6831
6932
|
const prepared = await this.prepareLifecycleEntry(internalDoc, entryType, signingKeyPair, signingKeyPassword, bypassPrecheck);
|
|
6832
|
-
await this.
|
|
6933
|
+
await this.putLocalEntries([prepared.fullEntry]);
|
|
6833
6934
|
await this.applyLifecycleEntryLocally(prepared, { markDirty: true });
|
|
6834
6935
|
}
|
|
6835
6936
|
/**
|
|
@@ -8648,7 +8749,7 @@ class BaseMindooDB {
|
|
|
8648
8749
|
getAfterDoc: () => this.convertAutomergeToJS(newDoc),
|
|
8649
8750
|
});
|
|
8650
8751
|
}
|
|
8651
|
-
await this.
|
|
8752
|
+
await this.putLocalEntries(entries);
|
|
8652
8753
|
internalDoc.doc = newDoc;
|
|
8653
8754
|
internalDoc.lastModified = now;
|
|
8654
8755
|
// The change entries just written are versioned and un-witnessed, so the
|
|
@@ -8775,7 +8876,7 @@ class BaseMindooDB {
|
|
|
8775
8876
|
const snapshotBytes = automerge_adapter_1.Automerge.save(internalDoc.doc);
|
|
8776
8877
|
const encryptedPayload = await this.tenant.encryptPayload(snapshotBytes, internalDoc.decryptionKeyId);
|
|
8777
8878
|
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
8778
|
-
const pseudoSnapshotHash = `snapshot-${(0,
|
|
8879
|
+
const pseudoSnapshotHash = `snapshot-${(0, idGeneration_1.generateRandomUuid)()}`;
|
|
8779
8880
|
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, pseudoSnapshotHash, headHashes, this.getSubtle());
|
|
8780
8881
|
let signature;
|
|
8781
8882
|
if (options.signingKeyPair && options.signingKeyPassword) {
|
|
@@ -8814,7 +8915,7 @@ class BaseMindooDB {
|
|
|
8814
8915
|
signingKeyPassword: options.signingKeyPassword,
|
|
8815
8916
|
}
|
|
8816
8917
|
: undefined);
|
|
8817
|
-
await this.
|
|
8918
|
+
await this.putLocalEntries([snapshotEntry]);
|
|
8818
8919
|
this.writesSinceSnapshotCheck.set(docId, 0);
|
|
8819
8920
|
this.logger.debug(`Created snapshot for document ${docId} with ${headHashes.length} heads and ${changesSinceSnapshot} changes since previous snapshot`);
|
|
8820
8921
|
}
|
|
@@ -9743,7 +9844,7 @@ class BaseMindooDB {
|
|
|
9743
9844
|
this.logger.warn(`L2 deserialize failed for ${docId}, evicting stale record: ${e}`);
|
|
9744
9845
|
try {
|
|
9745
9846
|
await store.delete("doc", key);
|
|
9746
|
-
this.
|
|
9847
|
+
this.forgetL2Record(docId);
|
|
9747
9848
|
}
|
|
9748
9849
|
catch (deleteError) {
|
|
9749
9850
|
this.logger.warn(`Failed to evict corrupt L2 record for ${docId}: ${deleteError}`);
|
|
@@ -9770,7 +9871,7 @@ class BaseMindooDB {
|
|
|
9770
9871
|
// L2 record is orphaned. Drop and fall through.
|
|
9771
9872
|
try {
|
|
9772
9873
|
await store.delete("doc", key);
|
|
9773
|
-
this.
|
|
9874
|
+
this.forgetL2Record(docId);
|
|
9774
9875
|
}
|
|
9775
9876
|
catch (deleteError) {
|
|
9776
9877
|
this.logger.warn(`Failed to evict orphaned L2 record for ${docId}: ${deleteError}`);
|
|
@@ -9782,7 +9883,7 @@ class BaseMindooDB {
|
|
|
9782
9883
|
if (persistedChangeSeq === currentChangeSeq) {
|
|
9783
9884
|
// Seed the flush-skip fingerprint with the state just read from L2 so
|
|
9784
9885
|
// the next periodic flush doesn't rewrite an identical record.
|
|
9785
|
-
this.
|
|
9886
|
+
this.rememberL2Record(docId, persistedChangeSeq, `${persistedChangeSeq}:${automerge_adapter_1.Automerge.getHeads(internal.doc).join(",")}`);
|
|
9786
9887
|
await this.storeCachedDocument(internal);
|
|
9787
9888
|
return internal;
|
|
9788
9889
|
}
|
|
@@ -9869,9 +9970,11 @@ class BaseMindooDB {
|
|
|
9869
9970
|
* First waits until the changefeed consumers (summary buffer, an
|
|
9870
9971
|
* enabled full-text index) have caught up: they materialize every
|
|
9871
9972
|
* changed document anyway, so running next to them would do the same
|
|
9872
|
-
* work twice. Then walks
|
|
9873
|
-
*
|
|
9874
|
-
*
|
|
9973
|
+
* work twice. Then walks the visible documents whose L2 record is not
|
|
9974
|
+
* known to be at the current changeSeq (see {@link l2RecordChangeSeqs},
|
|
9975
|
+
* persisted across restarts, so a pass after a small sync only visits
|
|
9976
|
+
* what changed). It skips every doc that is in L1 or turns out current
|
|
9977
|
+
* (record header check for docs with unknown state, no deserialization).
|
|
9875
9978
|
* For the rest it calls {@link tryLoadFromL2}. That path either:
|
|
9876
9979
|
*
|
|
9877
9980
|
* - hits L2 fresh and just promotes the doc to L1, or
|
|
@@ -9994,26 +10097,38 @@ class BaseMindooDB {
|
|
|
9994
10097
|
};
|
|
9995
10098
|
this.warmerProgress = {
|
|
9996
10099
|
processed: 0,
|
|
9997
|
-
|
|
10100
|
+
// Unknown until the consumers caught up and the pass is planned.
|
|
10101
|
+
total: 0,
|
|
9998
10102
|
phase: "warming",
|
|
9999
10103
|
};
|
|
10000
10104
|
await this.awaitChangefeedConsumersForWarmer(signal);
|
|
10001
10105
|
if (signal.aborted) {
|
|
10002
|
-
emitProgress({ processed: 0, total:
|
|
10106
|
+
emitProgress({ processed: 0, total: 0, phase: "cancelled" });
|
|
10003
10107
|
return;
|
|
10004
10108
|
}
|
|
10005
|
-
// Snapshot the
|
|
10006
|
-
// our iteration mid-pass.
|
|
10007
|
-
//
|
|
10008
|
-
//
|
|
10009
|
-
|
|
10109
|
+
// Snapshot the docs that may need work, so concurrent index updates
|
|
10110
|
+
// don't shift our iteration mid-pass. Docs whose L2 record is known to
|
|
10111
|
+
// be at the current changeSeq are left out up front: after a small sync
|
|
10112
|
+
// the pass (and its progress total) covers only what actually changed
|
|
10113
|
+
// instead of the whole database. Docs held in L1 are left out as well:
|
|
10114
|
+
// loads and changes mark them dirty, so the next flush (or
|
|
10115
|
+
// flush-before-evict) persists them anyway. Unknown docs (legacy
|
|
10116
|
+
// checkpoint) stay in and are checked against their record header below.
|
|
10117
|
+
const docIds = [];
|
|
10118
|
+
for (const entry of this.index) {
|
|
10119
|
+
if (entry.accessState === "visible" &&
|
|
10120
|
+
this.l2RecordChangeSeqs.get(entry.docId) !== entry.changeSeq &&
|
|
10121
|
+
!this.docCache.has(entry.docId)) {
|
|
10122
|
+
docIds.push(entry.docId);
|
|
10123
|
+
}
|
|
10124
|
+
}
|
|
10010
10125
|
const total = docIds.length;
|
|
10011
10126
|
// Progress snapshot is replaced (not mutated in place) so polled
|
|
10012
10127
|
// readers see consistent values. We initialize even when total=0
|
|
10013
10128
|
// so the UI can display a "done" terminal state.
|
|
10014
10129
|
this.warmerProgress = { processed: 0, total, phase: "warming" };
|
|
10015
10130
|
if (total === 0) {
|
|
10016
|
-
this.logger.debug("Warmer: nothing to warm,
|
|
10131
|
+
this.logger.debug("Warmer: nothing to warm, every L2 record is current.");
|
|
10017
10132
|
emitProgress({ processed: 0, total: 0, phase: "done" });
|
|
10018
10133
|
return;
|
|
10019
10134
|
}
|
|
@@ -10113,9 +10228,9 @@ class BaseMindooDB {
|
|
|
10113
10228
|
}
|
|
10114
10229
|
/**
|
|
10115
10230
|
* Whether L2 already holds this document at its current changeSeq, so
|
|
10116
|
-
* the warmer can skip it without deserializing. Answered from
|
|
10117
|
-
* {@link
|
|
10118
|
-
*
|
|
10231
|
+
* the warmer can skip it without deserializing. Answered from the
|
|
10232
|
+
* persisted {@link l2RecordChangeSeqs}; only when that has no entry for
|
|
10233
|
+
* the doc (legacy checkpoint) from the record header (no Automerge load).
|
|
10119
10234
|
*/
|
|
10120
10235
|
async isL2RecordCurrent(docId) {
|
|
10121
10236
|
const indexEntryIdx = this.getDocIndexPosition(docId);
|
|
@@ -10128,10 +10243,12 @@ class BaseMindooDB {
|
|
|
10128
10243
|
return false;
|
|
10129
10244
|
}
|
|
10130
10245
|
const currentChangeSeq = entry.changeSeq;
|
|
10131
|
-
const
|
|
10132
|
-
if (
|
|
10133
|
-
return
|
|
10246
|
+
const knownChangeSeq = this.l2RecordChangeSeqs.get(docId);
|
|
10247
|
+
if (knownChangeSeq !== undefined) {
|
|
10248
|
+
return knownChangeSeq === currentChangeSeq;
|
|
10134
10249
|
}
|
|
10250
|
+
// Unknown (checkpoint from before the per-doc state was persisted):
|
|
10251
|
+
// read the record header once and remember the answer.
|
|
10135
10252
|
try {
|
|
10136
10253
|
const bytes = await store.get("doc", `${this.getCachePrefix()}/${docId}`);
|
|
10137
10254
|
if (!bytes) {
|
|
@@ -10143,7 +10260,7 @@ class BaseMindooDB {
|
|
|
10143
10260
|
!Array.isArray(header.automergeHeads)) {
|
|
10144
10261
|
return false;
|
|
10145
10262
|
}
|
|
10146
|
-
this.
|
|
10263
|
+
this.rememberL2Record(docId, currentChangeSeq, `${currentChangeSeq}:${header.automergeHeads.join(",")}`);
|
|
10147
10264
|
return true;
|
|
10148
10265
|
}
|
|
10149
10266
|
catch {
|
|
@@ -11852,7 +11969,7 @@ class BaseMindooDB {
|
|
|
11852
11969
|
this.logger.debug(`Adding attachment to document ${docId}: ${fileName} (${fileData.length} bytes)`);
|
|
11853
11970
|
const store = this.getEffectiveAttachmentStore();
|
|
11854
11971
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
11855
|
-
const attachmentId = (0, idGeneration_1.
|
|
11972
|
+
const attachmentId = (0, idGeneration_1.generateAttachmentId)();
|
|
11856
11973
|
// Chunk the file
|
|
11857
11974
|
const chunks = [];
|
|
11858
11975
|
let prevChunkId = null;
|
|
@@ -11983,7 +12100,7 @@ class BaseMindooDB {
|
|
|
11983
12100
|
this.logger.debug(`Adding streaming attachment to document ${docId}: ${fileName}`);
|
|
11984
12101
|
const store = this.getEffectiveAttachmentStore();
|
|
11985
12102
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
11986
|
-
const attachmentId = (0, idGeneration_1.
|
|
12103
|
+
const attachmentId = (0, idGeneration_1.generateAttachmentId)();
|
|
11987
12104
|
let totalSize = 0;
|
|
11988
12105
|
let prevChunkId = null;
|
|
11989
12106
|
let lastChunkId = "";
|
|
@@ -12315,7 +12432,7 @@ class BaseMindooDB {
|
|
|
12315
12432
|
signingKeyPassword: signing?.signingKeyPassword,
|
|
12316
12433
|
attachmentRefs: this.collectAttachmentRefs(newDoc),
|
|
12317
12434
|
});
|
|
12318
|
-
await this.
|
|
12435
|
+
await this.putLocalEntries([entry]);
|
|
12319
12436
|
this.registerAutomergeHashMapping(docId, automerge_adapter_1.Automerge.decodeChange(changeBytes).hash, entry.id);
|
|
12320
12437
|
internalDoc.doc = newDoc;
|
|
12321
12438
|
internalDoc.lastModified = now;
|