mindoodb 0.0.19 → 0.0.21

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 (105) hide show
  1. package/LICENSE +200 -0
  2. package/README-server.md +14 -14
  3. package/README.md +12 -10
  4. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +6 -0
  5. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
  6. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +519 -373
  7. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
  8. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts +12 -0
  9. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -1
  10. package/dist/browser/cache/IndexedDBLocalCacheStore.js +152 -65
  11. package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -1
  12. package/dist/browser/index.d.ts +1 -1
  13. package/dist/browser/index.d.ts.map +1 -1
  14. package/dist/browser/index.js +2 -1
  15. package/dist/browser/index.js.map +1 -1
  16. package/dist/browser/index.mjs +14 -0
  17. package/dist/core/BaseMindooDB.d.ts +594 -6
  18. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  19. package/dist/core/BaseMindooDB.js +2478 -191
  20. package/dist/core/BaseMindooDB.js.map +1 -1
  21. package/dist/core/BaseMindooTenant.d.ts +65 -1
  22. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  23. package/dist/core/BaseMindooTenant.js +250 -35
  24. package/dist/core/BaseMindooTenant.js.map +1 -1
  25. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  26. package/dist/core/BaseMindooTenantFactory.js +47 -18
  27. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  28. package/dist/core/DocumentConflictAnalysis.d.ts +22 -0
  29. package/dist/core/DocumentConflictAnalysis.d.ts.map +1 -0
  30. package/dist/core/DocumentConflictAnalysis.js +101 -0
  31. package/dist/core/DocumentConflictAnalysis.js.map +1 -0
  32. package/dist/core/MindooDBServerAdmin.d.ts +26 -1
  33. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
  34. package/dist/core/MindooDBServerAdmin.js +19 -3
  35. package/dist/core/MindooDBServerAdmin.js.map +1 -1
  36. package/dist/core/appendonlystores/types.d.ts +9 -0
  37. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  38. package/dist/core/automerge-adapter.d.ts.map +1 -1
  39. package/dist/core/automerge-adapter.js +26 -14
  40. package/dist/core/automerge-adapter.js.map +1 -1
  41. package/dist/core/cache/EncryptedLocalCacheStore.d.ts +51 -1
  42. package/dist/core/cache/EncryptedLocalCacheStore.d.ts.map +1 -1
  43. package/dist/core/cache/EncryptedLocalCacheStore.js +48 -9
  44. package/dist/core/cache/EncryptedLocalCacheStore.js.map +1 -1
  45. package/dist/core/cache/LocalCacheStore.d.ts +34 -0
  46. package/dist/core/cache/LocalCacheStore.d.ts.map +1 -1
  47. package/dist/core/cache/LocalCacheStore.js +16 -0
  48. package/dist/core/cache/LocalCacheStore.js.map +1 -1
  49. package/dist/core/databaseIdValidation.d.ts +13 -0
  50. package/dist/core/databaseIdValidation.d.ts.map +1 -0
  51. package/dist/core/databaseIdValidation.js +38 -0
  52. package/dist/core/databaseIdValidation.js.map +1 -0
  53. package/dist/core/index.d.ts +2 -0
  54. package/dist/core/index.d.ts.map +1 -1
  55. package/dist/core/index.js +2 -0
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
  58. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +5 -1
  59. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
  60. package/dist/core/indexing/virtualviews/VirtualView.d.ts +14 -0
  61. package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
  62. package/dist/core/indexing/virtualviews/VirtualView.js +38 -6
  63. package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
  64. package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts +9 -2
  65. package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts.map +1 -1
  66. package/dist/core/indexing/virtualviews/VirtualViewDataChange.js +10 -3
  67. package/dist/core/indexing/virtualviews/VirtualViewDataChange.js.map +1 -1
  68. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +23 -1
  69. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
  70. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +17 -1
  71. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
  72. package/dist/core/indexing/virtualviews/types.d.ts +8 -0
  73. package/dist/core/indexing/virtualviews/types.d.ts.map +1 -1
  74. package/dist/core/indexing/virtualviews/types.js.map +1 -1
  75. package/dist/core/keys/KeyBag.d.ts +244 -7
  76. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  77. package/dist/core/keys/KeyBag.js +299 -82
  78. package/dist/core/keys/KeyBag.js.map +1 -1
  79. package/dist/core/tenantIdValidation.d.ts +19 -0
  80. package/dist/core/tenantIdValidation.d.ts.map +1 -0
  81. package/dist/core/tenantIdValidation.js +43 -0
  82. package/dist/core/tenantIdValidation.js.map +1 -0
  83. package/dist/core/types.d.ts +703 -12
  84. package/dist/core/types.d.ts.map +1 -1
  85. package/dist/core/types.js.map +1 -1
  86. package/dist/node/cache/FileSystemLocalCacheStore.d.ts +2 -1
  87. package/dist/node/cache/FileSystemLocalCacheStore.d.ts.map +1 -1
  88. package/dist/node/cache/FileSystemLocalCacheStore.js +4 -0
  89. package/dist/node/cache/FileSystemLocalCacheStore.js.map +1 -1
  90. package/dist/node/server/ServerSync.d.ts.map +1 -1
  91. package/dist/node/server/ServerSync.js +3 -1
  92. package/dist/node/server/ServerSync.js.map +1 -1
  93. package/dist/node/server/TenantManager.d.ts +1 -0
  94. package/dist/node/server/TenantManager.d.ts.map +1 -1
  95. package/dist/node/server/TenantManager.js +11 -3
  96. package/dist/node/server/TenantManager.js.map +1 -1
  97. package/dist/node/server/validation.d.ts +2 -6
  98. package/dist/node/server/validation.d.ts.map +1 -1
  99. package/dist/node/server/validation.js +14 -24
  100. package/dist/node/server/validation.js.map +1 -1
  101. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts +1 -0
  102. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts.map +1 -1
  103. package/dist/reactnative/cache/MMKVLocalCacheStore.js +14 -0
  104. package/dist/reactnative/cache/MMKVLocalCacheStore.js.map +1 -1
  105. package/package.json +25 -23
@@ -1,4 +1,4 @@
1
- import { MindooDB, CreateOptions, DeleteOptions, UndeleteOptions, ChangeOptions, DocumentDagAnalysisTimestamp, DocumentDagAnalysisResult, DocumentDagBranchMaterializationResult, DocumentDagEntryDetails, MindooDoc, MindooTenant, ProcessChangesCursor, ProcessChangesResult, DocumentHistoryResult, AttachmentConfig, DocumentCacheConfig, SnapshotConfig, SigningKeyPair, PerformanceCallback, ProcessChangeSummaryResult, SyncOptions, SyncResult, DocumentHistoryPageOptions, DocumentHistoryPageResult, MindooTextPatch, MindooTextPatchResult } from "./types";
1
+ import { MindooDB, CreateOptions, DeleteOptions, UndeleteOptions, ChangeOptions, DocumentDagAnalysisTimestamp, DocumentDagAnalysisResult, DocumentDagBranchMaterializationResult, DocumentDagEntryDetails, DocumentConflictAnalysisEvent, DocumentConflictAnalysisOptions, DocumentConflictBaseValue, DocumentConflictBaseValueQuery, DocumentConflictReport, DocumentConflictReportOptions, ConflictScanCheckpoint, MindooDoc, MindooTenant, ProcessChangesCursor, ProcessChangesResult, DocumentHistoryResult, AttachmentConfig, DocumentCacheConfig, SnapshotConfig, SigningKeyPair, PerformanceCallback, ProcessChangeSummaryResult, IncompleteAttachmentUploadReclaimResult, SyncOptions, SyncResult, DocumentHistoryPageOptions, DocumentHistoryPageResult, MindooJsonPatch, MindooJsonPatchResult, MindooTextPatch, MindooTextPatchResult, StartBackgroundWarmerOptions, BackgroundWarmerProgress } from "./types";
2
2
  import { BaseMindooTenant } from "./BaseMindooTenant";
3
3
  import type { ContentAddressedStore } from "./appendonlystores/types";
4
4
  import { Logger } from "./logging";
@@ -59,6 +59,7 @@ export declare class BaseMindooDB implements MindooDB {
59
59
  private attachmentStore;
60
60
  private chunkSizeBytes;
61
61
  private _isAdminOnlyDb;
62
+ private readonly timeTravelDate;
62
63
  private index;
63
64
  private indexLookup;
64
65
  private nextChangeSeq;
@@ -66,7 +67,14 @@ export declare class BaseMindooDB implements MindooDB {
66
67
  private readonly maxCachedDocs;
67
68
  private readonly iteratePrefetchWindowDocs;
68
69
  private readonly cacheRestoreLimit;
70
+ private readonly cacheRestoreBatchSize;
69
71
  private readonly reconcileRestoredIndexOnInit;
72
+ /**
73
+ * When `true`, startup leaves the in-memory L1 cache empty and lets
74
+ * subsequent reads pull documents lazily from L2 via
75
+ * {@link tryLoadFromL2}. See `DocumentCacheConfig.restoreToL2`.
76
+ */
77
+ private readonly restoreToL2;
70
78
  private readonly snapshotMinChanges;
71
79
  private readonly snapshotCooldownMs;
72
80
  private processedEntryIds;
@@ -79,13 +87,57 @@ export declare class BaseMindooDB implements MindooDB {
79
87
  private cachePrefix;
80
88
  private dirtyDocIds;
81
89
  private cacheMetaDirty;
82
- constructor(tenant: BaseMindooTenant, store: ContentAddressedStore, attachmentStore: ContentAddressedStore, attachmentConfig?: AttachmentConfig, documentCacheConfig?: DocumentCacheConfig, snapshotConfig?: SnapshotConfig, adminOnlyDb?: boolean, logger?: Logger, performanceCallback?: PerformanceCallback);
90
+ /**
91
+ * Tenant KeyBag fingerprint observed at the time the in-memory index
92
+ * was last reconciled with the underlying store.
93
+ *
94
+ * Persisted alongside the cache checkpoint so warm starts can compare
95
+ * the current fingerprint against the stored one and skip a full
96
+ * metadata scan (and the associated REST round-trips against remote
97
+ * stores) when the bag composition has not changed since the last
98
+ * flush.
99
+ *
100
+ * Set to `null` when no reconciliation has been performed yet, or
101
+ * when the tenant does not expose a fingerprint method.
102
+ */
103
+ private lastReconciledKeyBagFingerprint;
104
+ private readonly warmerBatchSize;
105
+ private readonly warmerScheduler;
106
+ /**
107
+ * Resolves when the currently-running warmer pass finishes. `null`
108
+ * when no warmer is running. Re-used to enforce single-flight: a
109
+ * second `startBackgroundWarmer()` call returns the same promise.
110
+ */
111
+ private warmerPromise;
112
+ /**
113
+ * AbortController for the in-flight warmer. `null` when no warmer is
114
+ * running. {@link stopBackgroundWarmer} aborts via this controller.
115
+ */
116
+ private warmerAbort;
117
+ /**
118
+ * Snapshot of the most recent warmer pass's progress. Updated
119
+ * in-place by {@link runBackgroundWarmer} so {@link getBackgroundWarmerProgress}
120
+ * can return the current state without additional bookkeeping. Set
121
+ * back to a fresh `{ processed: 0, total, phase: "warming" }` at the
122
+ * start of each new pass; the final value (`"done"` or
123
+ * `"cancelled"`) lingers between passes so a UI that mounts after
124
+ * the warmer settled can still display its outcome.
125
+ */
126
+ private warmerProgress;
127
+ constructor(tenant: BaseMindooTenant, store: ContentAddressedStore, attachmentStore: ContentAddressedStore, attachmentConfig?: AttachmentConfig, documentCacheConfig?: DocumentCacheConfig, snapshotConfig?: SnapshotConfig, adminOnlyDb?: boolean, logger?: Logger, performanceCallback?: PerformanceCallback, timeTravelDate?: number | null);
83
128
  /**
84
129
  * Get the admin public key from the tenant.
85
130
  * Only used when adminOnlyDb is true.
86
131
  */
87
132
  private getAdminPublicKey;
88
133
  isAdminOnlyDb(): boolean;
134
+ isTimeTravelMode(): boolean;
135
+ isReadOnly(): boolean;
136
+ getTimeTravelDate(): number | null;
137
+ private assertWritable;
138
+ private metadataVisibleAtTimeTravelDate;
139
+ private applyTimeTravelFilter;
140
+ private mergeTimeTravelScanFilters;
89
141
  /**
90
142
  * Attach a CacheManager so this DB participates in periodic cache flushing.
91
143
  */
@@ -97,16 +149,68 @@ export declare class BaseMindooDB implements MindooDB {
97
149
  private getCachedDocument;
98
150
  private touchCachedDocument;
99
151
  private storeCachedDocument;
152
+ /**
153
+ * Bring the in-memory L1 cache back under `maxCachedDocs` after a new
154
+ * insert, performing flush-before-evict for any dirty entry that would
155
+ * otherwise be dropped.
156
+ *
157
+ * Without flush-before-evict, an evicted dirty doc loses its in-memory
158
+ * state before the periodic `CacheManager` flush has a chance to
159
+ * persist it, forcing the next read to fall through to the expensive
160
+ * full-materialization path. By writing the doc to L2 inline first, we
161
+ * guarantee the next read can recover it via the cheap L2 read path
162
+ * (Phase 1c) - either as a fresh hit (changeSeq matches) or as a
163
+ * stale hit that only needs the missing deltas re-applied.
164
+ *
165
+ * If no `cacheManager` is attached (i.e. there is no L2 store
166
+ * available), we fall back to the legacy behavior and just drop the
167
+ * dirty marker - matching pre-Phase-1 semantics so embedders who
168
+ * never opted into L2 see no behavior change.
169
+ */
100
170
  private evictCachedDocsIfNeeded;
171
+ /**
172
+ * Synchronous eviction variant used after the periodic flush has
173
+ * already drained `dirtyDocIds`. No L2 work is required because there
174
+ * are no dirty docs left to persist.
175
+ */
176
+ private evictCleanCachedDocsIfNeeded;
101
177
  /**
102
178
  * Flush dirty documents and metadata to the cache store.
103
179
  */
104
180
  flushToCache(store: LocalCacheStore): Promise<number>;
181
+ /**
182
+ * Serialize and persist a single document into the L2 cache store.
183
+ *
184
+ * Header format (v2):
185
+ * `{ version: 2, id, createdAt, lastModified, decryptionKeyId,
186
+ * isDeleted, changeSeq, automergeHeads }`
187
+ *
188
+ * `changeSeq` is read from the in-memory changefeed index so that on
189
+ * the next open we can compare it against the live index entry and
190
+ * decide whether the persisted Automerge state is up to date or needs
191
+ * incremental delta application. `automergeHeads` snapshots
192
+ * `Automerge.getHeads(doc)` so a future read can identify which entries
193
+ * (if any) still need to be applied to bring the persisted doc current.
194
+ *
195
+ * Both fields are derived inline (no extra index lookups beyond the
196
+ * O(1) `indexLookup`) so this stays cheap to call from the periodic
197
+ * flush as well as from the upcoming flush-before-evict path.
198
+ */
199
+ private flushDirtyDocToCache;
105
200
  private exportMetadataCheckpoint;
106
201
  /**
107
202
  * Attempt to restore state from cache. Returns true on success.
108
203
  */
109
204
  restoreFromCache(store: LocalCacheStore): Promise<boolean>;
205
+ /**
206
+ * Decode a persisted L2 document cache record.
207
+ *
208
+ * Returns `null` for legacy (v1) records - they predate the
209
+ * `changeSeq + automergeHeads` freshness sentinels and so cannot
210
+ * participate in the L2 read-path freshness check; callers should
211
+ * fall back to the normal materialization path when they encounter
212
+ * one. Throws on truly malformed records (caught by callers).
213
+ */
110
214
  private deserializeDoc;
111
215
  /**
112
216
  * Get or import a CryptoKey for signature verification, with caching.
@@ -126,14 +230,24 @@ export declare class BaseMindooDB implements MindooDB {
126
230
  private compareIndexEntries;
127
231
  /**
128
232
  * Update the index entry for a document.
129
- * When a document changes, it gets a new monotonic sequence and is moved to
130
- * maintain order by (changeSeq, docId).
131
233
  *
132
- * Optimized to only update lookup map for entries that actually moved.
234
+ * When a document changes, it gets a new monotonic sequence and is
235
+ * moved within the sorted index to maintain `(changeSeq, docId)` order.
236
+ * The lookup map is only patched for the range of entries that actually
237
+ * shifted.
238
+ *
239
+ * Idempotent: if every tracked field (`lastModified`, `isDeleted`,
240
+ * `decryptionKeyId`, `accessState`) matches the existing entry the
241
+ * call is a no-op and no new `changeSeq` is consumed. This is what
242
+ * keeps repeated visibility reconciliations from inflating the
243
+ * changefeed.
133
244
  *
134
245
  * @param docId The document ID
135
246
  * @param lastModified The new last modified timestamp
136
- * @param isDeleted Whether the document is deleted
247
+ * @param isDeleted Whether the document is a deletion/inaccessibility tombstone
248
+ * @param decryptionKeyId Encryption key id derived from the doc's store metadata
249
+ * @param accessState `"visible"` for readable docs, `"inaccessible"` for
250
+ * tombstones produced by the key visibility layer
137
251
  */
138
252
  private updateIndex;
139
253
  /**
@@ -179,11 +293,94 @@ export declare class BaseMindooDB implements MindooDB {
179
293
  * then processes only the delta. Falls back to full rebuild on cache miss.
180
294
  */
181
295
  initialize(): Promise<void>;
296
+ /**
297
+ * Compute the current tenant KeyBag fingerprint, or `null` when the
298
+ * tenant does not expose one (older `MindooTenant` implementations).
299
+ */
300
+ private computeCurrentKeyBagFingerprint;
301
+ /**
302
+ * Record that the in-memory index now reflects the given KeyBag
303
+ * fingerprint. Marks the cache metadata dirty whenever the recorded
304
+ * value changes so the new fingerprint reaches the persisted
305
+ * checkpoint at the next flush.
306
+ */
307
+ private markKeyBagFingerprintReconciled;
308
+ /**
309
+ * Derive the per-doc visibility summary needed to reconcile the index
310
+ * against the current KeyBag state.
311
+ *
312
+ * - `decryptionKeyId` is taken from the chronologically first lifecycle
313
+ * entry, mirroring how {@link syncStoreChanges} materializes a doc.
314
+ * Replay entries inside a doc cannot retroactively re-key it.
315
+ * - `lastModified` is the latest `createdAt` among the entries that
316
+ * would be replayed for this doc (or, if none exist, the lifecycle
317
+ * entries themselves). This matches the user-visible timestamp the
318
+ * existing read path produces.
319
+ * - `isDeleted` is the lifecycle state derived from the same metadata
320
+ * {@link syncStoreChanges} would have observed.
321
+ *
322
+ * Returns `null` when there are no lifecycle entries for the doc,
323
+ * meaning there is nothing to reconcile.
324
+ */
325
+ private deriveDocumentVisibilityMetadata;
326
+ /**
327
+ * Evict every locally cached form of a document.
328
+ *
329
+ * Used by the visibility layer whenever a document transitions to
330
+ * `"inaccessible"` (the user lost the decryption key) or when a stale
331
+ * L2 record for an inaccessible doc is discovered. The point is to
332
+ * make sure no decrypted plaintext or in-memory Automerge state for
333
+ * the doc survives beyond the key revocation.
334
+ *
335
+ * - L1 (`docCache`) is cleared and the dirty marker dropped so we do
336
+ * not later persist a stale entry.
337
+ * - The automerge-hash lookup is cleared so any future re-add starts
338
+ * with a clean re-materialization.
339
+ * - The L2 record is deleted via the {@link LocalCacheStore} (if a
340
+ * cache manager is attached). Failures are logged but never thrown:
341
+ * a leftover encrypted record is preferable to crashing the read
342
+ * path that triggered the purge.
343
+ */
344
+ private purgeMaterializedDocument;
345
+ /**
346
+ * Reconcile the in-memory index with the current tenant KeyBag.
347
+ *
348
+ * This is the core entry point for "the user's keys changed; refresh
349
+ * what this database thinks is visible". It is safe to call repeatedly,
350
+ * including from concurrent live updates: the underlying `updateIndex`
351
+ * call only consumes a new `changeSeq` when state actually transitions.
352
+ *
353
+ * Behaviour for each doc that exists in the underlying store:
354
+ *
355
+ * - **Key now available, no entry / inaccessible entry** -> mark
356
+ * `"visible"` and emit a new index revision so view providers can
357
+ * re-add the doc. The doc body will be materialized lazily on the
358
+ * next read.
359
+ * - **Key now available, already visible** -> no-op.
360
+ * - **Key now missing, currently visible** -> purge any materialized
361
+ * state and mark `"inaccessible"` with `isDeleted: true` so
362
+ * metadata-only feeds emit a single tombstone and views remove
363
+ * the entry.
364
+ * - **Key now missing, already inaccessible (or not yet in index)** ->
365
+ * no index change, but we still call {@link purgeMaterializedDocument}
366
+ * defensively to scrub any leftover plaintext cache that might exist.
367
+ *
368
+ * Time-travel views are immutable; the function returns early for them.
369
+ * After running, if any transition occurred we mark the cache metadata
370
+ * dirty so the new visibility state survives a process restart.
371
+ */
372
+ reconcileKeyVisibility(): Promise<void>;
182
373
  private reconcileRestoredIndexWithStore;
183
374
  /**
184
375
  * Sync changes from the content-addressed store by finding new entries and processing them.
185
376
  * This method can be called multiple times to incrementally sync new entries.
186
377
  * On first call (when processedEntryIds is empty), it will process all entries.
378
+ *
379
+ * Sync intentionally does not trigger the L2 background warmer.
380
+ * Callers that want to warm the L2 cache after a sync (e.g. the Haven
381
+ * sync page) must call {@link startBackgroundWarmer} explicitly so
382
+ * casual sync calls stay cheap and the warmer cost is paid only when
383
+ * the workload is about to benefit from it.
187
384
  */
188
385
  syncStoreChanges(): Promise<void>;
189
386
  private supportsCursorScan;
@@ -252,6 +449,9 @@ export declare class BaseMindooDB implements MindooDB {
252
449
  private syncEntriesFromStoreDense;
253
450
  getStore(): ContentAddressedStore;
254
451
  getAttachmentStore(): ContentAddressedStore;
452
+ reclaimIncompleteAttachmentUploads(options?: {
453
+ minAgeMs?: number;
454
+ }): Promise<IncompleteAttachmentUploadReclaimResult>;
255
455
  getTenant(): MindooTenant;
256
456
  createDocument(options?: CreateOptions): Promise<MindooDoc>;
257
457
  /**
@@ -289,6 +489,289 @@ export declare class BaseMindooDB implements MindooDB {
289
489
  materializeDocumentBranchAtEntry(docId: string, headEntryId: string): Promise<DocumentDagBranchMaterializationResult | null>;
290
490
  materializeDocumentBranchAtTimestamp(docId: string, timestamp: DocumentDagAnalysisTimestamp, headEntryId: string): Promise<DocumentDagBranchMaterializationResult | null>;
291
491
  describeDocumentDagEntry(docId: string, entryId: string): Promise<DocumentDagEntryDetails | null>;
492
+ /**
493
+ * Streams conflict-analysis events for one or more documents.
494
+ *
495
+ * This is the UI/server friendly entry point for conflict analysis. It first
496
+ * does a metadata-only preflight for each document, then only decrypts and
497
+ * replays document changes when the DAG contains concurrency candidates. The
498
+ * yielded DTOs intentionally expose MindooDB concepts only; callers never get
499
+ * direct access to Automerge documents, patches, operation IDs, or objects.
500
+ *
501
+ * @param docIds Document IDs to analyze. Documents are processed sequentially
502
+ * so browser callers can render progress and stop early without a large
503
+ * burst of synchronous work.
504
+ * @param options Controls quick/full mode, value detail level, cancellation,
505
+ * per-document conflict limits, and cooperative event-loop yielding.
506
+ * @returns An async generator yielding progress, conflict, resolution, done,
507
+ * and error events.
508
+ */
509
+ analyzeDocumentConflicts(docIds: string[], options?: DocumentConflictAnalysisOptions): AsyncGenerator<DocumentConflictAnalysisEvent, void, unknown>;
510
+ /**
511
+ * Builds a complete conflict report for a single document.
512
+ *
513
+ * This is a convenience wrapper around `analyzeDocumentConflicts()`. It forces
514
+ * full analysis mode, consumes the event stream internally, and returns a
515
+ * stable report object suitable for server-side jobs or UI detail panels.
516
+ *
517
+ * @param docId Document ID to analyze.
518
+ * @param options Report options. The wrapper ignores quick-mode limits because
519
+ * reports are intended to describe the full known conflict history.
520
+ * @returns Aggregated conflict and resolution information for the document.
521
+ */
522
+ getDocumentConflictReport(docId: string, options?: DocumentConflictReportOptions): Promise<DocumentConflictReport>;
523
+ /**
524
+ * Resolves the value each conflicted path had at its merge base.
525
+ *
526
+ * For every query, the database identifies the heads that contributed to
527
+ * the conflict (the merging entry's parents for `entry-after` /
528
+ * `merge-deps` conflicts, or the active heads for `active-heads`
529
+ * conflicts), computes the most recent common ancestor of those heads,
530
+ * materializes the document at that ancestor, and reads the path.
531
+ *
532
+ * Queries that share a merge base are coalesced: the document is
533
+ * materialized at most once per unique base entry.
534
+ *
535
+ * @param docId Document identifier the queries refer to.
536
+ * @param queries One entry per (location, path) lookup the caller wants.
537
+ * @returns A `DocumentConflictBaseValue` aligned 1:1 with `queries`.
538
+ */
539
+ getDocumentConflictBaseValues(docId: string, queries: DocumentConflictBaseValueQuery[]): Promise<DocumentConflictBaseValue[]>;
540
+ /**
541
+ * Returns the head entry IDs whose merge base should be resolved for one
542
+ * conflict location. Filters out IDs that aren't present in the local
543
+ * metadata so callers don't try to materialize against missing entries.
544
+ */
545
+ private resolveConflictBaseParents;
546
+ /**
547
+ * Computes the most recent common ancestor of `parents` in the document's
548
+ * dependency DAG.
549
+ *
550
+ * Returns the deepest common ancestor (an ancestor that is not itself a
551
+ * proper ancestor of another common ancestor). If multiple deepest
552
+ * candidates exist, the one with the highest `createdAt` wins so the UI
553
+ * shows a base value as close to the conflict as possible.
554
+ */
555
+ private computeConflictMergeBaseEntryId;
556
+ /**
557
+ * Captures the current local receipt/index position for future incremental
558
+ * conflict scans.
559
+ */
560
+ getConflictScanCheckpoint(): Promise<ConflictScanCheckpoint>;
561
+ /**
562
+ * Narrows scan work to documents whose indexed latest state changed after the
563
+ * caller's checkpoint. When the caller asks for pre-existing unresolved
564
+ * conflicts, every requested document remains a candidate because an old active
565
+ * conflict may still need to be returned.
566
+ */
567
+ private resolveConflictAnalysisCandidateDocIds;
568
+ /**
569
+ * Finds the latest local store receipt order. This is intentionally metadata
570
+ * only and works with stores that do not expose a direct max-receipt query.
571
+ */
572
+ private getStoreMaxReceiptOrder;
573
+ /**
574
+ * Replays verified document changes and detects conflict/resolution events.
575
+ *
576
+ * The input `plan` is metadata-only and has already determined that analyzing
577
+ * this document is worth the cost. This method performs the private Automerge
578
+ * work: decrypting changes, replaying them in dependency order, inspecting
579
+ * changed paths, and optionally resolving conflicting value summaries. All
580
+ * Automerge-specific objects are converted to MindooDB DTOs before returning.
581
+ *
582
+ * Conflict detection has three phases:
583
+ * 1. Before applying a change, inspect existing multi-head states. This catches
584
+ * conflicts that exist between active heads and conflicts that are about to
585
+ * be resolved by a merge/resolution change.
586
+ * 2. After applying a change, inspect Automerge patches for changed paths and
587
+ * confirm conflicts with `getConflicts()` on those changed paths only.
588
+ * 3. After replay completes, inspect remaining active heads so unresolved
589
+ * conflicts are still reported for documents that never wrote a merge change.
590
+ *
591
+ * @param docId Document being analyzed.
592
+ * @param plan Metadata-only replay/concurrency plan for the document.
593
+ * @param options Analysis options propagated from the public API.
594
+ * @returns Conflict findings, resolution events, and the number of entries
595
+ * successfully replayed.
596
+ */
597
+ private detectDocumentConflictsFromPlan;
598
+ /**
599
+ * Applies incremental-scan visibility rules to conflict and resolution events.
600
+ *
601
+ * Receipt order is preferred because it represents local arrival. Created-at is
602
+ * only a fallback for legacy metadata that predates receipt-order assignment.
603
+ */
604
+ private shouldEmitConflictObservation;
605
+ /**
606
+ * Loads, verifies, decrypts, and decodes replay changes for conflict analysis.
607
+ *
608
+ * This mirrors the security checks used by normal materialization paths:
609
+ * admin-only databases ignore non-admin entries, signatures are verified
610
+ * before decryption, and malformed/missing entries are skipped with warnings.
611
+ *
612
+ * @param replayEntries Metadata entries selected by the conflict planner.
613
+ * @returns Map keyed by store entry ID containing decrypted change bytes and
614
+ * decoded Automerge hash/dependency metadata for private replay.
615
+ */
616
+ private loadVerifiedReplayChanges;
617
+ /**
618
+ * Builds a private lookup from Automerge change hashes to MindooDB entry IDs.
619
+ *
620
+ * Public conflict DTOs use MindooDB entry IDs, while Automerge heads are
621
+ * hashes. This lookup lets us translate internal head sets back to stable
622
+ * store-entry references for UI and report consumers.
623
+ *
624
+ * @param replayEntries Store metadata for replay entries.
625
+ * @param verifiedChanges Decrypted/decoded changes keyed by entry ID.
626
+ * @returns Map from Automerge hash to MindooDB store entry ID.
627
+ */
628
+ private buildAutomergeHashToEntryId;
629
+ /**
630
+ * Calls Automerge's diff API through a defensive private adapter.
631
+ *
632
+ * The public API must not depend on Automerge patch types. This wrapper keeps
633
+ * the cast and optional-method handling localized so a future Automerge API
634
+ * change affects only this internal method.
635
+ *
636
+ * @param doc Materialized internal Automerge document containing both head sets.
637
+ * @param beforeHeads Heads before applying the current change.
638
+ * @param afterHeads Heads after applying the current change.
639
+ * @returns Raw patch-like records for internal inspection only.
640
+ */
641
+ private diffAutomergeHeads;
642
+ /**
643
+ * Reads a patch action without leaking Automerge patch types into the codebase.
644
+ *
645
+ * @param patch Raw patch-like record returned by the private diff adapter.
646
+ * @returns Patch action string, or null when unavailable.
647
+ */
648
+ private getConflictPatchAction;
649
+ /**
650
+ * Normalizes a raw patch path into a JSON-safe MindooDB path array.
651
+ *
652
+ * @param patch Raw patch-like record returned by the private diff adapter.
653
+ * @returns A path made of string/number segments, or null for unsupported
654
+ * patch shapes.
655
+ */
656
+ private getConflictPatchPath;
657
+ /**
658
+ * Checks Automerge's optional conflict marker on a raw patch.
659
+ *
660
+ * Some conflict states are not surfaced solely by this flag, so callers also
661
+ * verify changed paths with `hasDocumentConflictAtPath()`.
662
+ *
663
+ * @param patch Raw patch-like record returned by the private diff adapter.
664
+ * @returns True when the patch explicitly says it wrote a conflicted value.
665
+ */
666
+ private isConflictPatch;
667
+ /**
668
+ * Filters MindooDB bookkeeping fields out of app-facing conflict results.
669
+ *
670
+ * `_lastModified` can conflict naturally because independent replicas update
671
+ * it during every change. Reporting it would be noisy and not useful to app
672
+ * developers analyzing their own JSON payload.
673
+ *
674
+ * @param path Candidate conflict path.
675
+ * @returns True when the path belongs to MindooDB internals.
676
+ */
677
+ private isInternalConflictPath;
678
+ /**
679
+ * Confirms whether a specific document path currently has multiple values.
680
+ *
681
+ * This is the narrow, path-level use of Automerge conflict inspection. It is
682
+ * called only for paths surfaced by patches or by the bounded active-head scan.
683
+ *
684
+ * @param doc Internal document or nested object containing the candidate path.
685
+ * @param path Path relative to `doc`.
686
+ * @returns True when Automerge reports more than one conflicting value.
687
+ */
688
+ private hasDocumentConflictAtPath;
689
+ /**
690
+ * Converts a conflict path into the public DTO representation.
691
+ *
692
+ * @param doc Internal document used to optionally resolve value summaries.
693
+ * @param path JSON-style path segments for the conflicted field.
694
+ * @param includeValues Whether to include winner/loser value summaries.
695
+ * @returns Public path DTO with optional conflict values.
696
+ */
697
+ private buildDocumentConflictPath;
698
+ /**
699
+ * Scans a materialized document for current conflicts.
700
+ *
701
+ * This is intentionally used only for bounded multi-head states: before a
702
+ * resolving change is applied and after replay if unresolved active heads
703
+ * remain. Regular per-change analysis stays patch-driven for performance.
704
+ *
705
+ * @param doc Internal materialized document to inspect.
706
+ * @param includeValues Whether to include value summaries in returned paths.
707
+ * @returns Sorted conflict path DTOs.
708
+ */
709
+ private collectDocumentConflictPaths;
710
+ /**
711
+ * Recursive worker for `collectDocumentConflictPaths()`.
712
+ *
713
+ * @param root Root internal document used when value summaries need absolute
714
+ * paths.
715
+ * @param parent Current object/array being inspected.
716
+ * @param basePath Path from `root` to `parent`.
717
+ * @param includeValues Whether to include value summaries.
718
+ * @param results Mutable result map keyed by path string.
719
+ * @param seen Object identity set used to avoid cycles/proxy revisits.
720
+ */
721
+ private collectDocumentConflictPathsFromParent;
722
+ /**
723
+ * Reads winner/loser summaries for one conflicted path.
724
+ *
725
+ * Values are converted into JSON-safe summaries so callers can inspect them
726
+ * without seeing Automerge-specific wrappers or mutable document objects.
727
+ *
728
+ * @param doc Internal document containing the conflict.
729
+ * @param path Absolute path to the conflicted field.
730
+ * @returns Stable list of conflict value summaries.
731
+ */
732
+ private getDocumentConflictValueSummaries;
733
+ /**
734
+ * Reads a nested value using the public path representation.
735
+ *
736
+ * @param root Root object/document to read from.
737
+ * @param path String/number path segments.
738
+ * @returns The value at the path, or undefined when any segment is missing.
739
+ */
740
+ private readValueAtDocumentPath;
741
+ /**
742
+ * Produces a stable comparison key for conflict winner detection.
743
+ *
744
+ * @param value Value to fingerprint.
745
+ * @returns String key derived from the JSON-safe representation.
746
+ */
747
+ private fingerprintConflictValue;
748
+ /**
749
+ * Converts arbitrary conflict values to bounded, JSON-safe data.
750
+ *
751
+ * This avoids exposing live Automerge objects and prevents very deep or cyclic
752
+ * structures from overwhelming reports.
753
+ *
754
+ * @param value Value to convert.
755
+ * @param depth Current recursion depth.
756
+ * @param seen Objects already visited during this conversion.
757
+ * @returns JSON-safe scalar, array/object subset, or compact placeholder.
758
+ */
759
+ private toJsonSafeConflictValue;
760
+ /**
761
+ * Throws when the caller cancels conflict analysis.
762
+ *
763
+ * @param signal Optional `AbortSignal` supplied through analysis options.
764
+ */
765
+ private throwIfConflictAnalysisAborted;
766
+ /**
767
+ * Cooperatively yields to the event loop during long browser/server scans.
768
+ *
769
+ * @param yieldEveryMs Minimum elapsed time before yielding. Undefined disables
770
+ * cooperative yielding; `0` yields at every call site.
771
+ * @param lastYieldAt Timestamp of the previous yield.
772
+ * @returns Updated timestamp to carry into the next call.
773
+ */
774
+ private maybeYieldForConflictAnalysis;
292
775
  /**
293
776
  * Derives human-readable changed field paths for one DAG entry.
294
777
  *
@@ -322,8 +805,17 @@ export declare class BaseMindooDB implements MindooDB {
322
805
  changeDoc(doc: MindooDoc, changeFunc: (doc: MindooDoc) => void | Promise<void>, options?: ChangeOptions): Promise<void>;
323
806
  changeDocWithSigningKey(doc: MindooDoc, changeFunc: (doc: MindooDoc) => void | Promise<void>, signingKeyPair: SigningKeyPair, signingKeyPassword: string): Promise<void>;
324
807
  applyTextPatch(doc: MindooDoc, patch: MindooTextPatch): Promise<MindooTextPatchResult>;
808
+ applyJsonPatch(doc: MindooDoc, patch: MindooJsonPatch): Promise<MindooJsonPatchResult>;
325
809
  private changeDocInternal;
326
810
  private validateTextPatch;
811
+ private validateJsonPatch;
812
+ private validateJsonPath;
813
+ private applyJsonPatchOperations;
814
+ private setJsonValueAtPath;
815
+ private unsetJsonValueAtPath;
816
+ private readJsonListAtPath;
817
+ private ensureJsonParentAtPath;
818
+ private readJsonParentAtPath;
327
819
  private ensureTextPath;
328
820
  private persistDocumentChange;
329
821
  /**
@@ -388,6 +880,97 @@ export declare class BaseMindooDB implements MindooDB {
388
880
  * Returns the updated document, or null if document wasn't actually changed.
389
881
  */
390
882
  private applyNewEntriesToCachedDocument;
883
+ /**
884
+ * Try to satisfy a document load from the persisted L2 cache without
885
+ * going through the full signature-verify + decrypt + Automerge replay
886
+ * pipeline.
887
+ *
888
+ * Three outcomes:
889
+ *
890
+ * 1. **L2 miss / unusable** (no record, legacy v1 record, deserialize
891
+ * failure, no `cacheManager`, etc.): returns `null`. The caller
892
+ * falls through to the existing full-materialization path.
893
+ *
894
+ * 2. **L2 fresh hit** (`persistedChangeSeq === currentChangeSeq`):
895
+ * promote the deserialized doc to L1 and return it. No store
896
+ * traffic, no crypto, no Automerge replay. This is the common case
897
+ * for warm databases and is what makes view rebuilds fast.
898
+ *
899
+ * 3. **L2 stale hit** (`persistedChangeSeq < currentChangeSeq`): scan
900
+ * the doc's store metadata, identify entries whose `automergeHash`
901
+ * is *not* already in the persisted doc's history, and re-use the
902
+ * existing {@link applyNewEntriesToCachedDocument} pipeline to
903
+ * verify+decrypt+apply only those deltas. Pre-filtering by hash
904
+ * means we never pay signature/decrypt cost for changes already
905
+ * captured in the persisted Automerge state.
906
+ *
907
+ * Any unexpected error degrades gracefully to the full path - the L2
908
+ * read path is purely an optimization and must never lose data.
909
+ */
910
+ private tryLoadFromL2;
911
+ /**
912
+ * Returns whether a background warmer pass is currently running.
913
+ *
914
+ * @see startBackgroundWarmer
915
+ * @see stopBackgroundWarmer
916
+ */
917
+ isWarmerRunning(): boolean;
918
+ /**
919
+ * Start the L2 background warmer.
920
+ *
921
+ * Walks a snapshot of the in-memory document index and, for every
922
+ * doc not already in L1, calls {@link tryLoadFromL2}. That path
923
+ * either:
924
+ *
925
+ * - hits L2 fresh and just promotes the doc to L1, or
926
+ * - hits L2 stale and applies missing deltas (re-flushed via the
927
+ * next CacheManager flush), or
928
+ * - falls through to full materialization for L2 misses, which then
929
+ * flushes a fresh L2 record via flush-before-evict.
930
+ *
931
+ * In all cases, after the warmer has visited every doc, every L2
932
+ * record has the current `changeSeq + automergeHeads`. Subsequent
933
+ * read-heavy workloads (most importantly virtual view rebuilds) can
934
+ * then satisfy every doc lookup via the cheap L2 read path.
935
+ *
936
+ * Single-flight: if a warmer is already running, the existing promise
937
+ * is returned. Yields to {@link warmerScheduler} between batches of
938
+ * `warmerBatchSize` docs so the foreground stays responsive.
939
+ *
940
+ * Cancellable via {@link stopBackgroundWarmer} or by passing an
941
+ * external {@link AbortSignal}. Cancellation is observed at batch
942
+ * boundaries; the in-flight doc is allowed to complete.
943
+ */
944
+ startBackgroundWarmer(options?: StartBackgroundWarmerOptions): Promise<void>;
945
+ /**
946
+ * Returns a snapshot of the most recent background warmer pass's
947
+ * progress, or `null` if no warmer has ever run on this instance.
948
+ *
949
+ * The returned object is the live snapshot used internally by
950
+ * {@link runBackgroundWarmer}; callers MUST NOT mutate it. The snapshot
951
+ * is replaced (not mutated in place) on the next
952
+ * {@link startBackgroundWarmer} call, so consumers polling this
953
+ * method see consistent integer readings.
954
+ *
955
+ * @see startBackgroundWarmer
956
+ */
957
+ getBackgroundWarmerProgress(): BackgroundWarmerProgress | null;
958
+ /**
959
+ * Cancel the in-flight warmer (if any) and wait for it to settle.
960
+ * Safe to call when no warmer is running - returns a resolved
961
+ * promise immediately.
962
+ */
963
+ stopBackgroundWarmer(): Promise<void>;
964
+ /**
965
+ * Body of one warmer pass. Pulled out so {@link startBackgroundWarmer}
966
+ * can wrap it in single-flight + abort plumbing without nesting.
967
+ *
968
+ * Maintains `this.warmerProgress` as a fresh snapshot for
969
+ * {@link getBackgroundWarmerProgress} consumers and invokes
970
+ * `onProgress` once per scheduler yield (per `warmerBatchSize` docs)
971
+ * plus once at the end of the pass with the terminal phase.
972
+ */
973
+ private runBackgroundWarmer;
391
974
  /**
392
975
  * Internal method to load a document from the content-addressed store
393
976
  */
@@ -434,6 +1017,11 @@ export declare class BaseMindooDB implements MindooDB {
434
1017
  * Internal method to add an attachment by chunking the file and storing chunks.
435
1018
  */
436
1019
  private addAttachmentInternal;
1020
+ private pendingAttachmentUploadLedgerId;
1021
+ private putEntriesWithRetry;
1022
+ private createPendingAttachmentUploadLedger;
1023
+ private clearPendingAttachmentUploadLedger;
1024
+ private cleanupIncompleteAttachmentUpload;
437
1025
  /**
438
1026
  * Internal method to add an attachment from a streaming data source.
439
1027
  * Memory efficient - processes data chunk by chunk without loading entire file into memory.