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.
Files changed (53) hide show
  1. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +2 -2
  2. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
  3. package/dist/browser/index.mjs +2 -0
  4. package/dist/core/BaseMindooDB.d.ts +45 -6
  5. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  6. package/dist/core/BaseMindooDB.js +152 -35
  7. package/dist/core/BaseMindooDB.js.map +1 -1
  8. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  9. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +3 -3
  10. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  11. package/dist/core/appendonlystores/network/AuthenticationService.js +2 -2
  12. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  13. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
  14. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +3 -3
  15. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
  16. package/dist/core/copy/attachments.d.ts +1 -1
  17. package/dist/core/copy/attachments.js +1 -1
  18. package/dist/core/copy/entryRewrite.d.ts +1 -1
  19. package/dist/core/copy/entryRewrite.js +1 -1
  20. package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
  21. package/dist/core/expressions/evaluateExpression.js +7 -1
  22. package/dist/core/expressions/evaluateExpression.js.map +1 -1
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +4 -2
  26. package/dist/core/index.js.map +1 -1
  27. package/dist/core/indexing/summary/extractSummaryFields.d.ts +10 -1
  28. package/dist/core/indexing/summary/extractSummaryFields.d.ts.map +1 -1
  29. package/dist/core/indexing/summary/extractSummaryFields.js +34 -7
  30. package/dist/core/indexing/summary/extractSummaryFields.js.map +1 -1
  31. package/dist/core/indexing/summary/types.d.ts +6 -0
  32. package/dist/core/indexing/summary/types.d.ts.map +1 -1
  33. package/dist/core/indexing/summary/types.js +8 -1
  34. package/dist/core/indexing/summary/types.js.map +1 -1
  35. package/dist/core/types.d.ts +1 -1
  36. package/dist/core/utils/idGeneration.d.ts +29 -7
  37. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  38. package/dist/core/utils/idGeneration.js +73 -13
  39. package/dist/core/utils/idGeneration.js.map +1 -1
  40. package/dist/core/utils/index.d.ts +1 -1
  41. package/dist/core/utils/index.d.ts.map +1 -1
  42. package/dist/core/utils/index.js +3 -1
  43. package/dist/core/utils/index.js.map +1 -1
  44. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  45. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +3 -3
  46. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  47. package/dist/node/server/DeviceDiscoveryService.js +2 -2
  48. package/dist/node/server/DeviceDiscoveryService.js.map +1 -1
  49. package/dist/node/server/SystemAdminAuth.js +2 -2
  50. package/dist/node/server/SystemAdminAuth.js.map +1 -1
  51. package/dist/node/server/peer/PeerAuthService.js +2 -2
  52. package/dist/node/server/peer/PeerAuthService.js.map +1 -1
  53. 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.lastFlushedDocState.delete(docId);
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.lastFlushedDocState.delete(docId);
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
- const batchHasReplayEntries = docLifecycleEntries.some((e) => this.isDocumentReplayEntry(e));
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.store.putEntries(allEntries);
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.store.putEntries([fullEntry]);
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.store.putEntries(prepared.map((p) => p.fullEntry));
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.store.putEntries([prepared.fullEntry]);
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.store.putEntries(entries);
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, uuid_1.v7)()}`;
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.store.putEntries([snapshotEntry]);
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.lastFlushedDocState.delete(docId);
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.lastFlushedDocState.delete(docId);
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.lastFlushedDocState.set(docId, `${persistedChangeSeq}:${automerge_adapter_1.Automerge.getHeads(internal.doc).join(",")}`);
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 a snapshot of the in-memory document index and
9873
- * skips every doc that is in L1 or whose L2 record is already at the
9874
- * current changeSeq (checked from the record header, no deserialization).
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
- total: this.index.length,
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: this.index.length, phase: "cancelled" });
10106
+ emitProgress({ processed: 0, total: 0, phase: "cancelled" });
10003
10107
  return;
10004
10108
  }
10005
- // Snapshot the docId list so concurrent index updates don't shift
10006
- // our iteration mid-pass. Iterating the index directly would also
10007
- // work but a snapshot makes the loop's behavior easier to reason
10008
- // about under sync activity.
10009
- const docIds = this.index.map((entry) => entry.docId);
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, index is empty.");
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 lastFlushedDocState} for records written in this session,
10118
- * otherwise from the record header (no Automerge load).
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 flushed = this.lastFlushedDocState.get(docId);
10132
- if (flushed !== undefined) {
10133
- return flushed.startsWith(`${currentChangeSeq}:`);
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.lastFlushedDocState.set(docId, `${currentChangeSeq}:${header.automergeHeads.join(",")}`);
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.generateFileUuid7)();
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.generateFileUuid7)();
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.store.putEntries([entry]);
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;