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
@@ -102,12 +102,78 @@ export interface OpenTenantOptions {
102
102
  * Map value: true if trusted, false if explicitly revoked
103
103
  */
104
104
  additionalTrustedKeys?: ReadonlyMap<string, boolean>;
105
+ /**
106
+ * Optional pre-decrypted signing/encryption CryptoKeys for the current
107
+ * user. When supplied, the tenant uses these directly instead of
108
+ * decrypting the encrypted private keys with the user password on first
109
+ * use. Typically derived once at login (see e.g.
110
+ * {@link KeyBag.deriveWrappingKey}) and held as `extractable: false`
111
+ * CryptoKeys for the lifetime of the session.
112
+ *
113
+ * When provided, the `currentUserPassword` argument to
114
+ * {@link MindooTenantFactory.openTenant} may be empty - it will not be
115
+ * used as long as the requested cached key is already present.
116
+ */
117
+ preDecryptedUserKeys?: PreDecryptedUserKeys;
118
+ }
119
+ /**
120
+ * Pre-decrypted user CryptoKeys that callers can pass to
121
+ * {@link MindooTenantFactory.openTenant} via {@link OpenTenantOptions}.
122
+ *
123
+ * Each key is independent. Whichever key is set is used directly; the
124
+ * other is still decrypted on demand from the user password on first
125
+ * use.
126
+ */
127
+ export interface PreDecryptedUserKeys {
128
+ /**
129
+ * Pre-imported Ed25519 signing {@link CryptoKey} (`["sign"]` usage).
130
+ * Skips the password-based decryption + import for the user's signing key.
131
+ */
132
+ signingKey?: CryptoKey;
133
+ /**
134
+ * Pre-imported RSA-OAEP encryption {@link CryptoKey} (`["decrypt"]` usage).
135
+ * Skips the password-based decryption + import for the user's encryption key.
136
+ */
137
+ encryptionKey?: CryptoKey;
138
+ /**
139
+ * Pre-derived AES-GCM cache encryption {@link CryptoKey} (`["encrypt", "decrypt"]`
140
+ * usage). Skips the password-based PBKDF2 derivation that the
141
+ * `EncryptedLocalCacheStore` performs internally.
142
+ */
143
+ cacheEncryptionKey?: CryptoKey;
105
144
  }
106
145
  interface CreateTenantPasswords {
107
146
  /** Password for the admin user's private keys */
108
147
  adminPassword: string;
109
- /** Password for the regular user's private keys */
148
+ /**
149
+ * Password for the regular (app) user's private keys.
150
+ *
151
+ * When {@link existingKeyBag} **and** {@link preDecryptedAppUserKeys}
152
+ * are both supplied the password is no longer needed by `createTenant`
153
+ * (the bag has already been wrapped, and `openTenant` consumes the
154
+ * pre-imported `CryptoKey`s directly). Callers in that live-bag mode
155
+ * may pass an empty string.
156
+ */
110
157
  userPassword: string;
158
+ /**
159
+ * Optional pre-existing {@link KeyBag} to extend with the new tenant's
160
+ * keys. When supplied, `createTenant` will mutate this bag in place
161
+ * (adding the new tenant's `default` and `$publicinfos` doc keys) and
162
+ * return the same instance in {@link CreateTenantResult.keyBag}. The
163
+ * caller is then responsible for persisting the bag once.
164
+ *
165
+ * When omitted, `createTenant` constructs a fresh bag using the legacy
166
+ * password-based KeyBag wrapping derivation (existing behavior).
167
+ */
168
+ existingKeyBag?: KeyBag;
169
+ /**
170
+ * Optional pre-decrypted CryptoKeys for the *app user* identity that
171
+ * are forwarded to the underlying {@link MindooTenantFactory.openTenant}
172
+ * call. When supplied alongside {@link existingKeyBag} the
173
+ * password-based decryption + import of the app user's signing /
174
+ * encryption private keys is skipped entirely.
175
+ */
176
+ preDecryptedAppUserKeys?: PreDecryptedUserKeys;
111
177
  }
112
178
  /**
113
179
  * Options for creating a new tenant with a single convenience call.
@@ -129,6 +195,11 @@ export type CreateTenantOptions = ({
129
195
  } & CreateTenantPasswords);
130
196
  /**
131
197
  * Result of creating a new tenant via createTenant().
198
+ *
199
+ * When the caller provided {@link CreateTenantOptions.existingKeyBag}, the
200
+ * returned `keyBag` is the same instance (mutated in place with the new
201
+ * tenant's keys). Callers should not double-save in that case - persist
202
+ * the bag once after `createTenant` returns.
132
203
  */
133
204
  export interface CreateTenantResult {
134
205
  /** The opened tenant, ready to use */
@@ -170,9 +241,29 @@ export interface ApproveJoinRequestOptions {
170
241
  serverUrl?: string;
171
242
  /** Optional admin username to include in the response so the joining user can display it */
172
243
  adminUsername?: string;
244
+ /**
245
+ * Optional document key ids to share with the joining user.
246
+ *
247
+ * `$publicinfos` is always included because it is required for directory
248
+ * access. When this option is omitted, the response includes the historical
249
+ * default set: `$publicinfos` and `default`.
250
+ */
251
+ sharedDocKeyIds?: string[];
173
252
  /** If "uri", approveJoinRequest returns a mdb://join-response/... URI string instead of an object */
174
253
  format?: "object" | "uri";
175
254
  }
255
+ export interface JoinResponseEncryptedDocKeyVersion {
256
+ /** Original KeyBag creation timestamp for this key version, used for rotation ordering. */
257
+ createdAt?: number;
258
+ /** Symmetric document key version encrypted with the shared password. */
259
+ encryptedKey: EncryptedPrivateKey;
260
+ }
261
+ export interface JoinResponseEncryptedDocKey {
262
+ /** Logical document key id within the tenant, e.g. "$publicinfos", "default", or a named key. */
263
+ keyId: string;
264
+ /** Encrypted key versions, sorted newest first by createdAt when timestamps are available. */
265
+ versions: JoinResponseEncryptedDocKeyVersion[];
266
+ }
176
267
  /**
177
268
  * A join response is created by the admin after approving a join request.
178
269
  * It contains encrypted symmetric keys and tenant metadata needed to join.
@@ -181,8 +272,8 @@ export interface ApproveJoinRequestOptions {
181
272
  * The encrypted keys can only be unlocked with the sharePassword communicated separately.
182
273
  */
183
274
  export interface JoinResponse {
184
- /** Protocol version (always 1 for now) */
185
- v: 1;
275
+ /** Protocol version. v2 carries versioned document-key bundles. */
276
+ v: 2;
186
277
  /** Tenant identifier */
187
278
  tenantId: string;
188
279
  /** Admin's public signing key (Ed25519, PEM) — needed to open the tenant */
@@ -193,10 +284,8 @@ export interface JoinResponse {
193
284
  serverUrl?: string;
194
285
  /** Optional admin username for display purposes */
195
286
  adminUsername?: string;
196
- /** Tenant symmetric key, encrypted with the sharePassword */
197
- encryptedTenantKey: EncryptedPrivateKey;
198
- /** $publicinfos symmetric key, encrypted with the sharePassword */
199
- encryptedPublicInfosKey: EncryptedPrivateKey;
287
+ /** Selected tenant document keys, encrypted with the sharePassword. */
288
+ encryptedDocKeys: JoinResponseEncryptedDocKey[];
200
289
  }
201
290
  /**
202
291
  * Options for joining a tenant with a join response.
@@ -204,13 +293,49 @@ export interface JoinResponse {
204
293
  export interface JoinTenantOptions {
205
294
  /** The user's locally-generated private identity (keys never leave this device) */
206
295
  user: PrivateUserId;
207
- /** The password for the user's private keys */
296
+ /**
297
+ * The password for the user's private keys.
298
+ *
299
+ * Required when neither {@link existingKeyBag} nor
300
+ * {@link preDecryptedUserKeys} is supplied. When `existingKeyBag` is
301
+ * provided the password is *not* used to construct a new KeyBag; when
302
+ * `preDecryptedUserKeys.signingKey` and `preDecryptedUserKeys.encryptionKey`
303
+ * are both supplied the password is *not* used to decrypt the user's
304
+ * private keys before opening the tenant. In those live-bag cases
305
+ * callers may pass an empty string.
306
+ */
208
307
  password: string;
209
308
  /** The shared password used to decrypt the keys in the join response */
210
309
  sharePassword: string;
310
+ /**
311
+ * Optional pre-existing {@link KeyBag} to extend with the joined tenant's
312
+ * keys. When supplied, `joinTenant` will mutate this bag in place (adding
313
+ * the joined tenant's shared document keys) and return
314
+ * the same instance in {@link JoinTenantResult.keyBag}. The caller is
315
+ * then responsible for persisting the bag once.
316
+ *
317
+ * When omitted, `joinTenant` constructs a fresh bag using the legacy
318
+ * password-based KeyBag wrapping derivation (existing behavior).
319
+ */
320
+ existingKeyBag?: KeyBag;
321
+ /**
322
+ * Optional pre-decrypted user keys forwarded to the underlying
323
+ * {@link MindooTenantFactory.openTenant} call. When the joining user's
324
+ * signing/encryption {@link CryptoKey}s are already available
325
+ * (e.g. because they were unlocked once at app startup via a live
326
+ * Haven session) supplying them here avoids the password-based
327
+ * decryption + import that `openTenant` would otherwise perform on
328
+ * the encrypted private keys carried on the user identity.
329
+ */
330
+ preDecryptedUserKeys?: PreDecryptedUserKeys;
211
331
  }
212
332
  /**
213
333
  * Result of joining a tenant via joinTenant().
334
+ *
335
+ * When the caller provided {@link JoinTenantOptions.existingKeyBag}, the
336
+ * returned `keyBag` is the same instance (mutated in place with the joined
337
+ * tenant's keys). Callers should not double-save in that case - persist the
338
+ * bag once after `joinTenant` returns.
214
339
  */
215
340
  export interface JoinTenantResult {
216
341
  /** The opened tenant, ready to use */
@@ -450,6 +575,32 @@ export interface MindooTenant {
450
575
  * @return The database instance
451
576
  */
452
577
  openDB(id: string, options?: OpenDBOptions): Promise<MindooDB>;
578
+ /**
579
+ * Replay any pending local KeyBag mutations and refresh open live
580
+ * databases so document visibility matches the current key set.
581
+ *
582
+ * Implementations must be idempotent and safe to call concurrently.
583
+ * The optional shape allows alternative tenant implementations (e.g.
584
+ * server-side, test doubles) to omit visibility reconciliation when it
585
+ * is not applicable.
586
+ */
587
+ reconcileKeyBagChanges?(): Promise<void>;
588
+ /**
589
+ * Return whether the current KeyBag can resolve the given document
590
+ * encryption key id. Implementations must not throw when the key is
591
+ * missing; they should return `false` instead so callers can use the
592
+ * result as a guard before attempting decryption.
593
+ */
594
+ hasDecryptionKey?(decryptionKeyId: string): Promise<boolean>;
595
+ /**
596
+ * Return a stable, opaque fingerprint of the doc keys this tenant
597
+ * currently has access to. The fingerprint must change whenever the
598
+ * set of available doc keys changes for the tenant, and must be safe
599
+ * to persist (no raw key material). Used by databases to skip
600
+ * visibility reconciliation on warm starts when the KeyBag composition
601
+ * has not changed since the last cache flush.
602
+ */
603
+ getDocKeyFingerprint?(): Promise<string>;
453
604
  /**
454
605
  * Creates a MindooDocSigner instance for signing and verifying document items.
455
606
  * The signer uses the tenant's cryptographic infrastructure for key management.
@@ -470,7 +621,7 @@ export interface MindooTenant {
470
621
  * This orchestrates:
471
622
  * 1. Parsing the join request (object or mdb:// URI string)
472
623
  * 2. Registering the user in the tenant directory
473
- * 3. Exporting the tenant key and $publicinfos key, encrypted with the sharePassword
624
+ * 3. Exporting selected document keys, encrypted with the sharePassword
474
625
  * 4. Packaging everything into a JoinResponse
475
626
  *
476
627
  * @param joinRequest A JoinRequest object or a mdb://join-request/... URI string
@@ -512,7 +663,7 @@ export { StoreKind } from "./appendonlystores/types";
512
663
  * - doc_* entries are for Automerge document operations
513
664
  * - attachment_* entries are for file attachment storage
514
665
  */
515
- export type StoreEntryType = "doc_create" | "doc_change" | "doc_snapshot" | "doc_delete" | "doc_undelete" | "attachment_chunk";
666
+ export type StoreEntryType = "doc_create" | "doc_change" | "doc_snapshot" | "doc_delete" | "doc_undelete" | "pending_attachment_upload" | "attachment_chunk";
516
667
  /**
517
668
  * Metadata for entries stored in the ContentAddressedStore.
518
669
  * This contains all information except the actual encrypted payload,
@@ -580,6 +731,21 @@ export interface StoreEntryMetadata {
580
731
  * must only return cursors built from entries that have a concrete value.
581
732
  */
582
733
  receiptOrder?: number;
734
+ /**
735
+ * Attachment id associated with attachment chunks or pending upload ledgers.
736
+ * For historical safety, cleanup only acts on pending upload ledgers that
737
+ * prove an attachment never reached a committed document revision.
738
+ */
739
+ attachmentId?: string;
740
+ /**
741
+ * Attachment ids committed by this document entry. Used by local recovery to
742
+ * distinguish never-committed uploads from attachments required by history.
743
+ */
744
+ attachmentIds?: string[];
745
+ /**
746
+ * Start timestamp for `pending_attachment_upload` recovery ledgers.
747
+ */
748
+ uploadStartedAt?: number;
583
749
  /**
584
750
  * The public signing key of the user who created this entry (Ed25519, PEM format).
585
751
  * Used for signature verification and audit trails.
@@ -664,6 +830,13 @@ export interface AttachmentReference {
664
830
  */
665
831
  createdBy: string;
666
832
  }
833
+ export interface IncompleteAttachmentUploadReclaimResult {
834
+ scannedLedgers: number;
835
+ reclaimedUploads: number;
836
+ reclaimedChunks: number;
837
+ keptCommittedUploads: number;
838
+ keptRecentUploads: number;
839
+ }
667
840
  /**
668
841
  * Configuration for attachment handling in MindooDB.
669
842
  */
@@ -698,6 +871,27 @@ export interface DocumentCacheConfig {
698
871
  * Default: same as `maxEntries`
699
872
  */
700
873
  restoreLimit?: number;
874
+ /**
875
+ * Number of cached documents fetched per batch during the eager
876
+ * startup restore (`restoreFromCache`). Each batch issues a single
877
+ * `LocalCacheStore.getMany` call, so this knob trades transaction
878
+ * overhead for transient memory pressure:
879
+ *
880
+ * - smaller values (e.g. 32-64) reduce peak memory at the cost of
881
+ * more cache-store transactions; sensible on memory-constrained
882
+ * targets (mobile, RN) or with very large average doc bodies (e.g.
883
+ * attachments-as-documents),
884
+ * - larger values (e.g. 512-1024) amortise per-transaction overhead
885
+ * but hold more encrypted+decrypted byte buffers in memory at the
886
+ * same time.
887
+ *
888
+ * Has no effect when `restoreToL2` is `true` (no eager fill happens).
889
+ * Has no effect on lazy L2 reads (`tryLoadFromL2`) or on the
890
+ * background warmer, both of which load one document at a time.
891
+ *
892
+ * Default: 256 (~25 MB transient peak per batch at 50 KB/doc).
893
+ */
894
+ restoreBatchSize?: number;
701
895
  /**
702
896
  * When enabled, verifies after cache restore that the restored document
703
897
  * index still matches the underlying local store and rebuilds metadata if a
@@ -706,6 +900,136 @@ export interface DocumentCacheConfig {
706
900
  * Default: false
707
901
  */
708
902
  reconcileRestoredIndexOnInit?: boolean;
903
+ /**
904
+ * When `true`, the persisted document cache is treated as the L2 tier
905
+ * of an L1/L2 cache split: startup does **not** eagerly materialize
906
+ * any cached documents into the in-memory L1 cache. Instead, the L2
907
+ * payloads stay on disk and individual `getDocument()` calls pull
908
+ * them in lazily via the L2 read path (which compares the persisted
909
+ * `changeSeq + automergeHeads` against the live index, applies
910
+ * deltas if stale, and then promotes to L1).
911
+ *
912
+ * This unlocks the "L2 much larger than L1" use case (e.g. 30k docs
913
+ * persisted, 128 in RAM) without burning startup time and memory on
914
+ * documents the user may never read.
915
+ *
916
+ * When `false` (the default), startup behavior is unchanged: cached
917
+ * documents are eagerly loaded into L1 up to `restoreLimit`. Even in
918
+ * eager mode the restore now uses the new batched `getMany` API to
919
+ * amortize per-key transaction overhead.
920
+ */
921
+ restoreToL2?: boolean;
922
+ /**
923
+ * Tuning for the optional background L2 warmer.
924
+ *
925
+ * The warmer is started explicitly via `startBackgroundWarmer()` and
926
+ * walks the in-memory document index, ensuring every doc has an
927
+ * up-to-date L2 cache record (using the same `tryLoadFromL2` path
928
+ * foreground reads use). It yields to the host runtime between
929
+ * batches so it does not starve other work.
930
+ */
931
+ warmer?: DocumentCacheWarmerConfig;
932
+ }
933
+ /**
934
+ * Cooperative scheduler used by the L2 background warmer to yield
935
+ * between batches so it does not monopolize the event loop.
936
+ *
937
+ * The default implementation uses `setTimeout(0)` which works in both
938
+ * Node and browsers. Embedders can plug in custom schedulers (e.g. one
939
+ * that yields via `requestIdleCallback` in the browser, or that
940
+ * coalesces yields with their own task queue).
941
+ */
942
+ export interface WarmerScheduler {
943
+ /**
944
+ * Yield control back to the host runtime. The warmer will resume
945
+ * processing once the returned promise resolves.
946
+ */
947
+ yield(): Promise<void>;
948
+ }
949
+ /**
950
+ * Tuning options for the background L2 warmer.
951
+ */
952
+ export interface DocumentCacheWarmerConfig {
953
+ /**
954
+ * Number of documents to process between scheduler yields. Smaller
955
+ * values yield more often (better responsiveness on the foreground
956
+ * thread, slightly higher per-doc overhead). Larger values run faster
957
+ * but block the event loop in larger bursts.
958
+ * Default: 50
959
+ */
960
+ batchSize?: number;
961
+ /**
962
+ * Custom scheduler implementation. Defaults to a `setTimeout(0)`-based
963
+ * scheduler that works in any JavaScript runtime.
964
+ */
965
+ scheduler?: WarmerScheduler;
966
+ }
967
+ /**
968
+ * Lifecycle phases for a background L2 warmer pass.
969
+ *
970
+ * - `warming` - the warmer is actively processing documents; `processed`
971
+ * will continue to grow until it reaches `total`.
972
+ * - `done` - the warmer reached the end of its document snapshot; the
973
+ * pass terminated successfully.
974
+ * - `cancelled`- the warmer observed an abort signal mid-pass and exited
975
+ * early. `processed` reflects how far it got; the
976
+ * remaining `total - processed` documents were not warmed.
977
+ */
978
+ export type BackgroundWarmerPhase = "warming" | "done" | "cancelled";
979
+ /**
980
+ * Snapshot of a background L2 warmer pass, suitable for driving a
981
+ * progress UI.
982
+ *
983
+ * `total` is captured once at the start of the pass (a snapshot of
984
+ * `index.length`); concurrent doc creations during the pass do not
985
+ * grow `total` so the bar can advance monotonically toward 100%.
986
+ *
987
+ * `processed` includes both docs that were warmed (an L2 record was
988
+ * read or refreshed) and docs that were skipped because they were
989
+ * already hot in L1 - in both cases the warmer is "done" with that doc.
990
+ */
991
+ export interface BackgroundWarmerProgress {
992
+ /** Number of docs the warmer has finished visiting in this pass. */
993
+ processed: number;
994
+ /** Total docs the warmer intends to visit in this pass. */
995
+ total: number;
996
+ /** Lifecycle phase. See {@link BackgroundWarmerPhase}. */
997
+ phase: BackgroundWarmerPhase;
998
+ }
999
+ /**
1000
+ * Options accepted by {@link MindooDB.startBackgroundWarmer}.
1001
+ */
1002
+ export interface StartBackgroundWarmerOptions {
1003
+ /**
1004
+ * Optional `AbortSignal` that the caller can use to cancel this
1005
+ * warmer pass without affecting any other in-flight passes.
1006
+ * Aborting via the signal has the same effect as calling
1007
+ * {@link MindooDB.stopBackgroundWarmer}.
1008
+ *
1009
+ * Note: warmer single-flight semantics mean that if a warmer is
1010
+ * already running when `startBackgroundWarmer` is called, the
1011
+ * existing in-flight promise is returned and the new `signal` is
1012
+ * NOT honored (since the existing pass owns its own internal
1013
+ * cancellation lifecycle). Callers that need precise cancellation
1014
+ * control should call {@link MindooDB.stopBackgroundWarmer} first.
1015
+ */
1016
+ signal?: AbortSignal;
1017
+ /**
1018
+ * Optional progress callback invoked by the warmer once per
1019
+ * scheduler-yield batch (i.e. roughly every
1020
+ * `DocumentCacheWarmerConfig.batchSize` documents) and once more
1021
+ * with `phase: "done"` or `phase: "cancelled"` when the pass
1022
+ * settles.
1023
+ *
1024
+ * Designed for driving a progress bar / "Warming…" UI affordance.
1025
+ * Per-doc invocation is intentionally avoided so the callback
1026
+ * doesn't add measurable overhead even on warmers that walk
1027
+ * 30,000+ documents.
1028
+ *
1029
+ * Errors thrown from this callback are caught and logged so a buggy
1030
+ * progress consumer cannot break the warmer.
1031
+ */
1032
+ onProgress?: (progress: BackgroundWarmerProgress) => void;
709
1033
  }
710
1034
  /**
711
1035
  * Configuration for automatic snapshot creation.
@@ -959,24 +1283,81 @@ export interface MindooDoc {
959
1283
  */
960
1284
  streamAttachment(attachmentId: string, startOffset?: number): AsyncGenerator<Uint8Array, void, unknown>;
961
1285
  }
1286
+ /** Plain JSON-compatible document payload materialized from a MindooDoc. */
962
1287
  export interface MindooDocPayload {
963
1288
  [key: string]: unknown;
964
1289
  }
1290
+ /**
1291
+ * Single text splice applied to the string at a text patch path.
1292
+ *
1293
+ * `index` is a character offset in the current text value, `deleteCount`
1294
+ * removes characters from that offset, and `insert` adds replacement text.
1295
+ */
965
1296
  export interface MindooTextEdit {
966
1297
  index: number;
967
1298
  deleteCount: number;
968
1299
  insert?: string;
969
1300
  }
1301
+ /**
1302
+ * Ordered text edits for one string field in a MindooDoc.
1303
+ *
1304
+ * `path` addresses the target string inside the document payload. When
1305
+ * supplied, `baseHeads` identifies the Automerge heads the caller based the
1306
+ * edit on, allowing the backend to apply order-sensitive text changes in the
1307
+ * intended causal context.
1308
+ */
970
1309
  export interface MindooTextPatch {
971
1310
  path: Array<string | number>;
972
1311
  baseHeads?: string[];
973
1312
  edits: MindooTextEdit[];
974
1313
  }
1314
+ /** Result returned after applying a text patch and materializing the document. */
975
1315
  export interface MindooTextPatchResult {
976
1316
  doc: MindooDoc;
977
1317
  heads: string[];
978
1318
  data: MindooDocPayload;
979
1319
  }
1320
+ /** Sets a value at `path`, creating or replacing that payload field. */
1321
+ export interface MindooJsonSetPatch {
1322
+ path: Array<string | number>;
1323
+ value: unknown;
1324
+ }
1325
+ /** Removes the value at `path` from the payload object or list. */
1326
+ export interface MindooJsonUnsetPatch {
1327
+ path: Array<string | number>;
1328
+ }
1329
+ /** Inserts one or more values into the list located at `path`. */
1330
+ export interface MindooJsonListInsertPatch {
1331
+ path: Array<string | number>;
1332
+ index: number;
1333
+ values: unknown[];
1334
+ }
1335
+ /** Deletes one or more values from the list located at `path`. */
1336
+ export interface MindooJsonListDeletePatch {
1337
+ path: Array<string | number>;
1338
+ index: number;
1339
+ deleteCount: number;
1340
+ }
1341
+ /**
1342
+ * Granular JSON mutation batch for a MindooDoc payload.
1343
+ *
1344
+ * Operations are applied in patch order by operation group. `baseHeads` has the
1345
+ * same role as in text patches: it records the document heads the caller saw
1346
+ * before constructing operations whose order matters, such as list inserts.
1347
+ */
1348
+ export interface MindooJsonPatch {
1349
+ baseHeads?: string[];
1350
+ set?: MindooJsonSetPatch[];
1351
+ unset?: MindooJsonUnsetPatch[];
1352
+ listInsert?: MindooJsonListInsertPatch[];
1353
+ listDelete?: MindooJsonListDeletePatch[];
1354
+ }
1355
+ /** Result returned after applying a JSON patch and materializing the document. */
1356
+ export interface MindooJsonPatchResult {
1357
+ doc: MindooDoc;
1358
+ heads: string[];
1359
+ data: MindooDocPayload;
1360
+ }
980
1361
  export interface DirectoryUserDetails {
981
1362
  [key: string]: string;
982
1363
  }
@@ -1281,6 +1662,13 @@ export interface OpenDBOptions extends OpenStoreOptions {
1281
1662
  * This is used for the directory database to prevent recursion and ensure security.
1282
1663
  */
1283
1664
  adminOnlyDb?: boolean;
1665
+ /**
1666
+ * Optional read-only historical cutoff for opening the database.
1667
+ *
1668
+ * When set, entries with createdAt >= timeTravelDate are ignored. The
1669
+ * normalized value maps to store-level creationDateUntil semantics.
1670
+ */
1671
+ timeTravelDate?: number | Date | string;
1284
1672
  /**
1285
1673
  * Configuration for attachment handling (chunk size, etc.)
1286
1674
  * If not provided, defaults are used.
@@ -1341,7 +1729,7 @@ export interface PerformanceCallback {
1341
1729
  * Called for bounded history/time-travel workflows.
1342
1730
  */
1343
1731
  onHistoryOperation?: (metrics: {
1344
- operation: 'getDocumentAtTimestamp' | 'getDocumentHistoryPage' | 'analyzeDocumentDagAtTimestamp' | 'materializeDocumentBranchAtEntry' | 'materializeDocumentBranchAtTimestamp' | 'describeDocumentDagEntry';
1732
+ operation: 'getDocumentAtTimestamp' | 'getDocumentHistoryPage' | 'analyzeDocumentDagAtTimestamp' | 'materializeDocumentBranchAtEntry' | 'materializeDocumentBranchAtTimestamp' | 'describeDocumentDagEntry' | 'analyzeDocumentConflicts' | 'getDocumentConflictReport' | 'getDocumentConflictBaseValues';
1345
1733
  docId: string;
1346
1734
  time: number;
1347
1735
  scannedEntries: number;
@@ -1472,6 +1860,177 @@ export interface DocumentDagEntryDetails {
1472
1860
  automergeHash: string | null;
1473
1861
  decodedChange: DocumentDagDecodedChangeSummary | null;
1474
1862
  }
1863
+ /**
1864
+ * Conflict analysis mode controls how much history the analyzer traverses.
1865
+ */
1866
+ export type DocumentConflictAnalysisMode = "quick" | "full";
1867
+ /**
1868
+ * Conflict detail level controls whether conflicting values are summarized.
1869
+ */
1870
+ export type DocumentConflictDetailLevel = "paths-only" | "values";
1871
+ /**
1872
+ * JSON-safe path segment returned by conflict analysis.
1873
+ */
1874
+ export type DocumentConflictPathSegment = string | number;
1875
+ /**
1876
+ * JSON-safe value summary for one conflicting value.
1877
+ *
1878
+ * `value` is included only when it can be represented safely without exposing
1879
+ * CRDT-specific objects. `preview` is always intended for compact UI display.
1880
+ */
1881
+ export interface DocumentConflictValueSummary {
1882
+ conflictId: string;
1883
+ preview: string | null;
1884
+ value?: unknown;
1885
+ isWinner: boolean;
1886
+ }
1887
+ /**
1888
+ * One conflicted document path detected during analysis.
1889
+ */
1890
+ export interface DocumentConflictPath {
1891
+ path: DocumentConflictPathSegment[];
1892
+ pathString: string;
1893
+ values?: DocumentConflictValueSummary[];
1894
+ }
1895
+ /**
1896
+ * Describes where in the document DAG a conflict observation was made.
1897
+ */
1898
+ export interface DocumentConflictLocation {
1899
+ kind: "entry-after" | "active-heads" | "merge-deps";
1900
+ entryId?: string;
1901
+ createdAt?: number;
1902
+ receiptOrder?: number;
1903
+ createdByPublicKey?: string;
1904
+ headEntryIds: string[];
1905
+ automergeHeads: string[];
1906
+ }
1907
+ /**
1908
+ * Conflict observation or resolution event for one document.
1909
+ */
1910
+ export interface DocumentConflictSummary {
1911
+ docId: string;
1912
+ location: DocumentConflictLocation;
1913
+ paths: DocumentConflictPath[];
1914
+ }
1915
+ /**
1916
+ * Durable cursor for incremental conflict scans.
1917
+ *
1918
+ * `changeSeqAsOf` is used for cheap document-level pre-filtering through the
1919
+ * document index. `storeReceiptOrderAsOf` is used for per-event filtering so
1920
+ * late-arriving sync entries are surfaced even if their author timestamp is old.
1921
+ */
1922
+ export interface ConflictScanCheckpoint {
1923
+ changeSeqAsOf: number;
1924
+ storeReceiptOrderAsOf?: number;
1925
+ takenAt: number;
1926
+ }
1927
+ /**
1928
+ * Options accepted by conflict analysis APIs.
1929
+ */
1930
+ export interface DocumentConflictAnalysisOptions {
1931
+ mode?: DocumentConflictAnalysisMode;
1932
+ detail?: DocumentConflictDetailLevel;
1933
+ pageSize?: number;
1934
+ yieldEveryMs?: number;
1935
+ maxConflictsPerDoc?: number;
1936
+ since?: ConflictScanCheckpoint;
1937
+ includeUnresolvedFromBefore?: boolean;
1938
+ signal?: AbortSignal;
1939
+ }
1940
+ /**
1941
+ * Options accepted by the convenience report API.
1942
+ */
1943
+ export interface DocumentConflictReportOptions extends Omit<DocumentConflictAnalysisOptions, "mode" | "maxConflictsPerDoc"> {
1944
+ }
1945
+ /**
1946
+ * Streaming event emitted by `analyzeDocumentConflicts()`.
1947
+ */
1948
+ export type DocumentConflictAnalysisEvent = {
1949
+ type: "progress";
1950
+ scannedDocs: number;
1951
+ totalDocs: number;
1952
+ docId?: string;
1953
+ scannedEntries?: number;
1954
+ message: string;
1955
+ } | {
1956
+ type: "docStart";
1957
+ docId: string;
1958
+ } | {
1959
+ type: "conflictDetected";
1960
+ conflict: DocumentConflictSummary;
1961
+ quick: boolean;
1962
+ } | {
1963
+ type: "conflictResolved";
1964
+ docId: string;
1965
+ entryId: string;
1966
+ createdAt: number;
1967
+ receiptOrder?: number;
1968
+ createdByPublicKey: string;
1969
+ path: DocumentConflictPath;
1970
+ automergeHash: string | null;
1971
+ } | {
1972
+ type: "docDone";
1973
+ docId: string;
1974
+ hadConflicts: boolean;
1975
+ conflictsFound: number;
1976
+ entriesScanned: number;
1977
+ } | {
1978
+ type: "error";
1979
+ docId?: string;
1980
+ entryId?: string;
1981
+ error: unknown;
1982
+ } | {
1983
+ type: "scanCheckpoint";
1984
+ checkpoint: ConflictScanCheckpoint;
1985
+ };
1986
+ /**
1987
+ * Full conflict report for one document.
1988
+ */
1989
+ export interface DocumentConflictReport {
1990
+ docId: string;
1991
+ hadConflicts: boolean;
1992
+ conflictsFound: number;
1993
+ conflicts: DocumentConflictSummary[];
1994
+ resolutions: Array<Extract<DocumentConflictAnalysisEvent, {
1995
+ type: "conflictResolved";
1996
+ }>>;
1997
+ entriesScanned: number;
1998
+ errors: Array<Extract<DocumentConflictAnalysisEvent, {
1999
+ type: "error";
2000
+ }>>;
2001
+ scanCheckpoint: ConflictScanCheckpoint | null;
2002
+ }
2003
+ /**
2004
+ * Request to look up the value at a conflicted path as it stood at the merge
2005
+ * base of the contributing branches (the "old" value before the conflict).
2006
+ */
2007
+ export interface DocumentConflictBaseValueQuery {
2008
+ /** Conflict location as returned by `getDocumentConflictReport`. */
2009
+ location: DocumentConflictLocation;
2010
+ /** Structured path segments (matches `DocumentConflictPath.path`). */
2011
+ path: DocumentConflictPathSegment[];
2012
+ /** Stable string key (matches `DocumentConflictPath.pathString`). */
2013
+ pathString: string;
2014
+ }
2015
+ /**
2016
+ * Result for one base-value lookup.
2017
+ *
2018
+ * - `available`: a previous value existed at the merge base; `preview` and
2019
+ * `value` are populated.
2020
+ * - `no-prior-value`: a merge base was found but the path did not exist there
2021
+ * (e.g. both branches independently created the field).
2022
+ * - `no-base`: no shared ancestor could be determined for the contributing
2023
+ * heads (e.g. the conflict has only one head, or the parent set is empty).
2024
+ * - `missing-entry`: the conflict's `entryId` is not (or no longer) in the
2025
+ * local store; the base value cannot be resolved.
2026
+ */
2027
+ export interface DocumentConflictBaseValue {
2028
+ pathString: string;
2029
+ baseEntryId: string | null;
2030
+ status: "available" | "no-prior-value" | "no-base" | "missing-entry";
2031
+ preview: string | null;
2032
+ value?: unknown;
2033
+ }
1475
2034
  /**
1476
2035
  * Result of reconstructing one branch-local document state for a selected DAG head.
1477
2036
  */
@@ -1568,6 +2127,18 @@ export interface MindooDB {
1568
2127
  * @return True if admin-only mode is enabled
1569
2128
  */
1570
2129
  isAdminOnlyDb(): boolean;
2130
+ /**
2131
+ * True when this database was opened with a timeTravelDate cutoff.
2132
+ */
2133
+ isTimeTravelMode(): boolean;
2134
+ /**
2135
+ * True when mutating APIs are disabled for this database instance.
2136
+ */
2137
+ isReadOnly(): boolean;
2138
+ /**
2139
+ * Normalized time travel cutoff in epoch milliseconds, or null for live mode.
2140
+ */
2141
+ getTimeTravelDate(): number | null;
1571
2142
  /**
1572
2143
  * Get the content-addressed store that is used to store document changes for this database.
1573
2144
  *
@@ -1580,6 +2151,9 @@ export interface MindooDB {
1580
2151
  * @return The content-addressed store for attachments
1581
2152
  */
1582
2153
  getAttachmentStore(): ContentAddressedStore;
2154
+ reclaimIncompleteAttachmentUploads?(options?: {
2155
+ minAgeMs?: number;
2156
+ }): Promise<IncompleteAttachmentUploadReclaimResult>;
1583
2157
  /**
1584
2158
  * Create a new document.
1585
2159
  *
@@ -1696,6 +2270,36 @@ export interface MindooDB {
1696
2270
  * Decode and summarize one DAG entry for hover/tooling use cases.
1697
2271
  */
1698
2272
  describeDocumentDagEntry(docId: string, entryId: string): Promise<DocumentDagEntryDetails | null>;
2273
+ /**
2274
+ * Stream conflict analysis events for one or more documents.
2275
+ *
2276
+ * The API intentionally returns MindooDB DTOs only. It does not expose
2277
+ * Automerge documents, patches, or operation objects so the storage backend
2278
+ * can evolve without changing callers.
2279
+ */
2280
+ analyzeDocumentConflicts(docIds: string[], options?: DocumentConflictAnalysisOptions): AsyncGenerator<DocumentConflictAnalysisEvent, void, unknown>;
2281
+ /**
2282
+ * Build a full conflict report for one document by consuming the streaming
2283
+ * analyzer internally.
2284
+ */
2285
+ getDocumentConflictReport(docId: string, options?: DocumentConflictReportOptions): Promise<DocumentConflictReport>;
2286
+ /**
2287
+ * Resolve the value each conflicted path had at its merge base (the most
2288
+ * recent common ancestor of the branches that contributed to the conflict).
2289
+ *
2290
+ * Each query independently maps to a base value so the caller can request
2291
+ * lookups lazily — for example, only for the paths it currently displays.
2292
+ * Queries that share a merge base are coalesced internally so the
2293
+ * underlying document state is materialized at most once per base entry.
2294
+ */
2295
+ getDocumentConflictBaseValues(docId: string, queries: DocumentConflictBaseValueQuery[]): Promise<DocumentConflictBaseValue[]>;
2296
+ /**
2297
+ * Return an incremental conflict-scan checkpoint for the current local view.
2298
+ *
2299
+ * Callers can persist this value after presenting scan results and pass it
2300
+ * back as `DocumentConflictAnalysisOptions.since` on the next run.
2301
+ */
2302
+ getConflictScanCheckpoint(): Promise<ConflictScanCheckpoint>;
1699
2303
  /**
1700
2304
  * Get all non-deleted document IDs in this database.
1701
2305
  *
@@ -1789,6 +2393,15 @@ export interface MindooDB {
1789
2393
  * overwriting concurrent edits that arrived meanwhile.
1790
2394
  */
1791
2395
  applyTextPatch(doc: MindooDoc, patch: MindooTextPatch): Promise<MindooTextPatchResult>;
2396
+ /**
2397
+ * Apply granular JSON edits at document paths.
2398
+ *
2399
+ * If `baseHeads` is provided, edits are applied at that historical Automerge
2400
+ * version using `changeAt`, then merged into the current document. This lets
2401
+ * apps flush buffered object/list operations based on a stale local copy
2402
+ * without replacing concurrent changes that arrived meanwhile.
2403
+ */
2404
+ applyJsonPatch(doc: MindooDoc, patch: MindooJsonPatch): Promise<MindooJsonPatchResult>;
1792
2405
  /**
1793
2406
  * Change a document using a specific signing key.
1794
2407
  * This is like changeDoc but allows signing with a different key than the current user's.
@@ -1850,9 +2463,87 @@ export interface MindooDB {
1850
2463
  * then processes them to update the cached database documents and index.
1851
2464
  * After processing, the new change hashes are appended to the stored list.
1852
2465
  *
1853
- * @return A promise that resolves when the sync is complete
2466
+ * Sync deliberately does NOT trigger the L2 background warmer. Callers
2467
+ * that want to warm the L2 cache after a sync (e.g. the Haven sync
2468
+ * page once a sync settles) should call {@link startBackgroundWarmer}
2469
+ * explicitly. This keeps casual `syncStoreChanges()` calls cheap and
2470
+ * leaves warmer scheduling to the caller, which knows whether the
2471
+ * cost of a full L2 walk is worth paying for the current workload.
2472
+ *
2473
+ * @return A promise that resolves when the sync is complete.
1854
2474
  */
1855
2475
  syncStoreChanges(): Promise<void>;
2476
+ /**
2477
+ * Reconcile local document visibility with the current tenant KeyBag.
2478
+ *
2479
+ * Reveals documents whose decryption keys are now available, hides
2480
+ * documents whose keys have been removed, and purges any plaintext
2481
+ * caches for inaccessible documents. The method only touches in-memory
2482
+ * and L2 caches owned by this database instance - it does not write
2483
+ * synthetic store entries and is safe to call repeatedly.
2484
+ *
2485
+ * Optional so alternative database implementations (e.g. read-only
2486
+ * views, time-travel snapshots) can omit it.
2487
+ */
2488
+ reconcileKeyVisibility?(): Promise<void>;
2489
+ /**
2490
+ * Start the L2 background warmer.
2491
+ *
2492
+ * Walks the in-memory document index and ensures every document has
2493
+ * an up-to-date L2 cache record (using the same internal load-from-L2
2494
+ * code path foreground reads use). Runs in batches with cooperative
2495
+ * yields so foreground operations are not starved.
2496
+ *
2497
+ * Intended to be called explicitly by long-lived sessions that just
2498
+ * finished a sync (e.g. the Haven sync page after pulling/pushing
2499
+ * completes) so subsequent reads - especially virtual view rebuilds
2500
+ * - hit the fast L2 path. Casual database opens should NOT call this;
2501
+ * pay the warmer cost only when the workload is about to benefit
2502
+ * from a fully warm L2.
2503
+ *
2504
+ * Single-flight: a second call while the warmer is already running
2505
+ * returns the same in-flight promise rather than starting a second
2506
+ * pass. The returned promise resolves when the warmer has finished
2507
+ * (either by visiting every document or by being cancelled via
2508
+ * {@link stopBackgroundWarmer} or via the optional `signal`).
2509
+ *
2510
+ * @param options See {@link StartBackgroundWarmerOptions}: optional
2511
+ * `signal` for per-call cancellation and `onProgress` for driving
2512
+ * a progress UI.
2513
+ */
2514
+ startBackgroundWarmer?(options?: StartBackgroundWarmerOptions): Promise<void>;
2515
+ /**
2516
+ * Cancel the running background warmer (if any). Returns a promise
2517
+ * that resolves once the in-flight warmer pass has observed the
2518
+ * cancellation and returned (so callers can `await` clean shutdown).
2519
+ * Safe to call when no warmer is running - resolves immediately in
2520
+ * that case.
2521
+ *
2522
+ * Useful before initiating destructive operations (key rotation, data
2523
+ * wipe, etc.) so the warmer cannot race against the mutation, and at
2524
+ * the end of tests to ensure no background work outlives the test.
2525
+ */
2526
+ stopBackgroundWarmer?(): Promise<void>;
2527
+ /**
2528
+ * Returns `true` while the L2 background warmer is actively
2529
+ * processing documents. Useful for "Warming…" UI indicators and
2530
+ * tests that need to coordinate with warmer progress.
2531
+ */
2532
+ isWarmerRunning?(): boolean;
2533
+ /**
2534
+ * Snapshot of the current (or most recent) background warmer pass's
2535
+ * progress, or `null` if no warmer has ever run on this database
2536
+ * instance. Designed for UIs that mount AFTER the warmer has already
2537
+ * started (e.g. the user navigates back to the Sync page mid-warm)
2538
+ * and need to "catch up" on state without missing onProgress events.
2539
+ *
2540
+ * After a warmer settles, the returned snapshot persists with its
2541
+ * final `phase` (`"done"` or `"cancelled"`) so the UI can decide
2542
+ * whether to keep showing a "Optimized" message or fade away. The
2543
+ * snapshot is replaced on the next {@link startBackgroundWarmer}
2544
+ * call.
2545
+ */
2546
+ getBackgroundWarmerProgress?(): BackgroundWarmerProgress | null;
1856
2547
  /**
1857
2548
  * Pull changes from a remote content-addressed store or another MindooDB instance.
1858
2549
  *