mindoodb 0.0.19 → 0.0.20

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 (101) 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 +7 -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 +63 -1
  22. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  23. package/dist/core/BaseMindooTenant.js +198 -21
  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 +18 -7
  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 +1 -0
  54. package/dist/core/index.d.ts.map +1 -1
  55. package/dist/core/index.js +1 -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/types.d.ts +678 -5
  80. package/dist/core/types.d.ts.map +1 -1
  81. package/dist/core/types.js.map +1 -1
  82. package/dist/node/cache/FileSystemLocalCacheStore.d.ts +2 -1
  83. package/dist/node/cache/FileSystemLocalCacheStore.d.ts.map +1 -1
  84. package/dist/node/cache/FileSystemLocalCacheStore.js +4 -0
  85. package/dist/node/cache/FileSystemLocalCacheStore.js.map +1 -1
  86. package/dist/node/server/ServerSync.d.ts.map +1 -1
  87. package/dist/node/server/ServerSync.js +3 -1
  88. package/dist/node/server/ServerSync.js.map +1 -1
  89. package/dist/node/server/TenantManager.d.ts +1 -0
  90. package/dist/node/server/TenantManager.d.ts.map +1 -1
  91. package/dist/node/server/TenantManager.js +11 -3
  92. package/dist/node/server/TenantManager.js.map +1 -1
  93. package/dist/node/server/validation.d.ts +2 -2
  94. package/dist/node/server/validation.d.ts.map +1 -1
  95. package/dist/node/server/validation.js +6 -10
  96. package/dist/node/server/validation.js.map +1 -1
  97. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts +1 -0
  98. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts.map +1 -1
  99. package/dist/reactnative/cache/MMKVLocalCacheStore.js +14 -0
  100. package/dist/reactnative/cache/MMKVLocalCacheStore.js.map +1 -1
  101. 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 */
@@ -204,13 +275,49 @@ export interface JoinResponse {
204
275
  export interface JoinTenantOptions {
205
276
  /** The user's locally-generated private identity (keys never leave this device) */
206
277
  user: PrivateUserId;
207
- /** The password for the user's private keys */
278
+ /**
279
+ * The password for the user's private keys.
280
+ *
281
+ * Required when neither {@link existingKeyBag} nor
282
+ * {@link preDecryptedUserKeys} is supplied. When `existingKeyBag` is
283
+ * provided the password is *not* used to construct a new KeyBag; when
284
+ * `preDecryptedUserKeys.signingKey` and `preDecryptedUserKeys.encryptionKey`
285
+ * are both supplied the password is *not* used to decrypt the user's
286
+ * private keys before opening the tenant. In those live-bag cases
287
+ * callers may pass an empty string.
288
+ */
208
289
  password: string;
209
290
  /** The shared password used to decrypt the keys in the join response */
210
291
  sharePassword: string;
292
+ /**
293
+ * Optional pre-existing {@link KeyBag} to extend with the joined tenant's
294
+ * keys. When supplied, `joinTenant` will mutate this bag in place (adding
295
+ * the joined tenant's `default` and `$publicinfos` doc keys) and return
296
+ * the same instance in {@link JoinTenantResult.keyBag}. The caller is
297
+ * then responsible for persisting the bag once.
298
+ *
299
+ * When omitted, `joinTenant` constructs a fresh bag using the legacy
300
+ * password-based KeyBag wrapping derivation (existing behavior).
301
+ */
302
+ existingKeyBag?: KeyBag;
303
+ /**
304
+ * Optional pre-decrypted user keys forwarded to the underlying
305
+ * {@link MindooTenantFactory.openTenant} call. When the joining user's
306
+ * signing/encryption {@link CryptoKey}s are already available
307
+ * (e.g. because they were unlocked once at app startup via a live
308
+ * Haven session) supplying them here avoids the password-based
309
+ * decryption + import that `openTenant` would otherwise perform on
310
+ * the encrypted private keys carried on the user identity.
311
+ */
312
+ preDecryptedUserKeys?: PreDecryptedUserKeys;
211
313
  }
212
314
  /**
213
315
  * Result of joining a tenant via joinTenant().
316
+ *
317
+ * When the caller provided {@link JoinTenantOptions.existingKeyBag}, the
318
+ * returned `keyBag` is the same instance (mutated in place with the joined
319
+ * tenant's keys). Callers should not double-save in that case - persist the
320
+ * bag once after `joinTenant` returns.
214
321
  */
215
322
  export interface JoinTenantResult {
216
323
  /** The opened tenant, ready to use */
@@ -450,6 +557,32 @@ export interface MindooTenant {
450
557
  * @return The database instance
451
558
  */
452
559
  openDB(id: string, options?: OpenDBOptions): Promise<MindooDB>;
560
+ /**
561
+ * Replay any pending local KeyBag mutations and refresh open live
562
+ * databases so document visibility matches the current key set.
563
+ *
564
+ * Implementations must be idempotent and safe to call concurrently.
565
+ * The optional shape allows alternative tenant implementations (e.g.
566
+ * server-side, test doubles) to omit visibility reconciliation when it
567
+ * is not applicable.
568
+ */
569
+ reconcileKeyBagChanges?(): Promise<void>;
570
+ /**
571
+ * Return whether the current KeyBag can resolve the given document
572
+ * encryption key id. Implementations must not throw when the key is
573
+ * missing; they should return `false` instead so callers can use the
574
+ * result as a guard before attempting decryption.
575
+ */
576
+ hasDecryptionKey?(decryptionKeyId: string): Promise<boolean>;
577
+ /**
578
+ * Return a stable, opaque fingerprint of the doc keys this tenant
579
+ * currently has access to. The fingerprint must change whenever the
580
+ * set of available doc keys changes for the tenant, and must be safe
581
+ * to persist (no raw key material). Used by databases to skip
582
+ * visibility reconciliation on warm starts when the KeyBag composition
583
+ * has not changed since the last cache flush.
584
+ */
585
+ getDocKeyFingerprint?(): Promise<string>;
453
586
  /**
454
587
  * Creates a MindooDocSigner instance for signing and verifying document items.
455
588
  * The signer uses the tenant's cryptographic infrastructure for key management.
@@ -512,7 +645,7 @@ export { StoreKind } from "./appendonlystores/types";
512
645
  * - doc_* entries are for Automerge document operations
513
646
  * - attachment_* entries are for file attachment storage
514
647
  */
515
- export type StoreEntryType = "doc_create" | "doc_change" | "doc_snapshot" | "doc_delete" | "doc_undelete" | "attachment_chunk";
648
+ export type StoreEntryType = "doc_create" | "doc_change" | "doc_snapshot" | "doc_delete" | "doc_undelete" | "pending_attachment_upload" | "attachment_chunk";
516
649
  /**
517
650
  * Metadata for entries stored in the ContentAddressedStore.
518
651
  * This contains all information except the actual encrypted payload,
@@ -580,6 +713,21 @@ export interface StoreEntryMetadata {
580
713
  * must only return cursors built from entries that have a concrete value.
581
714
  */
582
715
  receiptOrder?: number;
716
+ /**
717
+ * Attachment id associated with attachment chunks or pending upload ledgers.
718
+ * For historical safety, cleanup only acts on pending upload ledgers that
719
+ * prove an attachment never reached a committed document revision.
720
+ */
721
+ attachmentId?: string;
722
+ /**
723
+ * Attachment ids committed by this document entry. Used by local recovery to
724
+ * distinguish never-committed uploads from attachments required by history.
725
+ */
726
+ attachmentIds?: string[];
727
+ /**
728
+ * Start timestamp for `pending_attachment_upload` recovery ledgers.
729
+ */
730
+ uploadStartedAt?: number;
583
731
  /**
584
732
  * The public signing key of the user who created this entry (Ed25519, PEM format).
585
733
  * Used for signature verification and audit trails.
@@ -664,6 +812,13 @@ export interface AttachmentReference {
664
812
  */
665
813
  createdBy: string;
666
814
  }
815
+ export interface IncompleteAttachmentUploadReclaimResult {
816
+ scannedLedgers: number;
817
+ reclaimedUploads: number;
818
+ reclaimedChunks: number;
819
+ keptCommittedUploads: number;
820
+ keptRecentUploads: number;
821
+ }
667
822
  /**
668
823
  * Configuration for attachment handling in MindooDB.
669
824
  */
@@ -698,6 +853,27 @@ export interface DocumentCacheConfig {
698
853
  * Default: same as `maxEntries`
699
854
  */
700
855
  restoreLimit?: number;
856
+ /**
857
+ * Number of cached documents fetched per batch during the eager
858
+ * startup restore (`restoreFromCache`). Each batch issues a single
859
+ * `LocalCacheStore.getMany` call, so this knob trades transaction
860
+ * overhead for transient memory pressure:
861
+ *
862
+ * - smaller values (e.g. 32-64) reduce peak memory at the cost of
863
+ * more cache-store transactions; sensible on memory-constrained
864
+ * targets (mobile, RN) or with very large average doc bodies (e.g.
865
+ * attachments-as-documents),
866
+ * - larger values (e.g. 512-1024) amortise per-transaction overhead
867
+ * but hold more encrypted+decrypted byte buffers in memory at the
868
+ * same time.
869
+ *
870
+ * Has no effect when `restoreToL2` is `true` (no eager fill happens).
871
+ * Has no effect on lazy L2 reads (`tryLoadFromL2`) or on the
872
+ * background warmer, both of which load one document at a time.
873
+ *
874
+ * Default: 256 (~25 MB transient peak per batch at 50 KB/doc).
875
+ */
876
+ restoreBatchSize?: number;
701
877
  /**
702
878
  * When enabled, verifies after cache restore that the restored document
703
879
  * index still matches the underlying local store and rebuilds metadata if a
@@ -706,6 +882,136 @@ export interface DocumentCacheConfig {
706
882
  * Default: false
707
883
  */
708
884
  reconcileRestoredIndexOnInit?: boolean;
885
+ /**
886
+ * When `true`, the persisted document cache is treated as the L2 tier
887
+ * of an L1/L2 cache split: startup does **not** eagerly materialize
888
+ * any cached documents into the in-memory L1 cache. Instead, the L2
889
+ * payloads stay on disk and individual `getDocument()` calls pull
890
+ * them in lazily via the L2 read path (which compares the persisted
891
+ * `changeSeq + automergeHeads` against the live index, applies
892
+ * deltas if stale, and then promotes to L1).
893
+ *
894
+ * This unlocks the "L2 much larger than L1" use case (e.g. 30k docs
895
+ * persisted, 128 in RAM) without burning startup time and memory on
896
+ * documents the user may never read.
897
+ *
898
+ * When `false` (the default), startup behavior is unchanged: cached
899
+ * documents are eagerly loaded into L1 up to `restoreLimit`. Even in
900
+ * eager mode the restore now uses the new batched `getMany` API to
901
+ * amortize per-key transaction overhead.
902
+ */
903
+ restoreToL2?: boolean;
904
+ /**
905
+ * Tuning for the optional background L2 warmer.
906
+ *
907
+ * The warmer is started explicitly via `startBackgroundWarmer()` and
908
+ * walks the in-memory document index, ensuring every doc has an
909
+ * up-to-date L2 cache record (using the same `tryLoadFromL2` path
910
+ * foreground reads use). It yields to the host runtime between
911
+ * batches so it does not starve other work.
912
+ */
913
+ warmer?: DocumentCacheWarmerConfig;
914
+ }
915
+ /**
916
+ * Cooperative scheduler used by the L2 background warmer to yield
917
+ * between batches so it does not monopolize the event loop.
918
+ *
919
+ * The default implementation uses `setTimeout(0)` which works in both
920
+ * Node and browsers. Embedders can plug in custom schedulers (e.g. one
921
+ * that yields via `requestIdleCallback` in the browser, or that
922
+ * coalesces yields with their own task queue).
923
+ */
924
+ export interface WarmerScheduler {
925
+ /**
926
+ * Yield control back to the host runtime. The warmer will resume
927
+ * processing once the returned promise resolves.
928
+ */
929
+ yield(): Promise<void>;
930
+ }
931
+ /**
932
+ * Tuning options for the background L2 warmer.
933
+ */
934
+ export interface DocumentCacheWarmerConfig {
935
+ /**
936
+ * Number of documents to process between scheduler yields. Smaller
937
+ * values yield more often (better responsiveness on the foreground
938
+ * thread, slightly higher per-doc overhead). Larger values run faster
939
+ * but block the event loop in larger bursts.
940
+ * Default: 50
941
+ */
942
+ batchSize?: number;
943
+ /**
944
+ * Custom scheduler implementation. Defaults to a `setTimeout(0)`-based
945
+ * scheduler that works in any JavaScript runtime.
946
+ */
947
+ scheduler?: WarmerScheduler;
948
+ }
949
+ /**
950
+ * Lifecycle phases for a background L2 warmer pass.
951
+ *
952
+ * - `warming` - the warmer is actively processing documents; `processed`
953
+ * will continue to grow until it reaches `total`.
954
+ * - `done` - the warmer reached the end of its document snapshot; the
955
+ * pass terminated successfully.
956
+ * - `cancelled`- the warmer observed an abort signal mid-pass and exited
957
+ * early. `processed` reflects how far it got; the
958
+ * remaining `total - processed` documents were not warmed.
959
+ */
960
+ export type BackgroundWarmerPhase = "warming" | "done" | "cancelled";
961
+ /**
962
+ * Snapshot of a background L2 warmer pass, suitable for driving a
963
+ * progress UI.
964
+ *
965
+ * `total` is captured once at the start of the pass (a snapshot of
966
+ * `index.length`); concurrent doc creations during the pass do not
967
+ * grow `total` so the bar can advance monotonically toward 100%.
968
+ *
969
+ * `processed` includes both docs that were warmed (an L2 record was
970
+ * read or refreshed) and docs that were skipped because they were
971
+ * already hot in L1 - in both cases the warmer is "done" with that doc.
972
+ */
973
+ export interface BackgroundWarmerProgress {
974
+ /** Number of docs the warmer has finished visiting in this pass. */
975
+ processed: number;
976
+ /** Total docs the warmer intends to visit in this pass. */
977
+ total: number;
978
+ /** Lifecycle phase. See {@link BackgroundWarmerPhase}. */
979
+ phase: BackgroundWarmerPhase;
980
+ }
981
+ /**
982
+ * Options accepted by {@link MindooDB.startBackgroundWarmer}.
983
+ */
984
+ export interface StartBackgroundWarmerOptions {
985
+ /**
986
+ * Optional `AbortSignal` that the caller can use to cancel this
987
+ * warmer pass without affecting any other in-flight passes.
988
+ * Aborting via the signal has the same effect as calling
989
+ * {@link MindooDB.stopBackgroundWarmer}.
990
+ *
991
+ * Note: warmer single-flight semantics mean that if a warmer is
992
+ * already running when `startBackgroundWarmer` is called, the
993
+ * existing in-flight promise is returned and the new `signal` is
994
+ * NOT honored (since the existing pass owns its own internal
995
+ * cancellation lifecycle). Callers that need precise cancellation
996
+ * control should call {@link MindooDB.stopBackgroundWarmer} first.
997
+ */
998
+ signal?: AbortSignal;
999
+ /**
1000
+ * Optional progress callback invoked by the warmer once per
1001
+ * scheduler-yield batch (i.e. roughly every
1002
+ * `DocumentCacheWarmerConfig.batchSize` documents) and once more
1003
+ * with `phase: "done"` or `phase: "cancelled"` when the pass
1004
+ * settles.
1005
+ *
1006
+ * Designed for driving a progress bar / "Warming…" UI affordance.
1007
+ * Per-doc invocation is intentionally avoided so the callback
1008
+ * doesn't add measurable overhead even on warmers that walk
1009
+ * 30,000+ documents.
1010
+ *
1011
+ * Errors thrown from this callback are caught and logged so a buggy
1012
+ * progress consumer cannot break the warmer.
1013
+ */
1014
+ onProgress?: (progress: BackgroundWarmerProgress) => void;
709
1015
  }
710
1016
  /**
711
1017
  * Configuration for automatic snapshot creation.
@@ -959,24 +1265,81 @@ export interface MindooDoc {
959
1265
  */
960
1266
  streamAttachment(attachmentId: string, startOffset?: number): AsyncGenerator<Uint8Array, void, unknown>;
961
1267
  }
1268
+ /** Plain JSON-compatible document payload materialized from a MindooDoc. */
962
1269
  export interface MindooDocPayload {
963
1270
  [key: string]: unknown;
964
1271
  }
1272
+ /**
1273
+ * Single text splice applied to the string at a text patch path.
1274
+ *
1275
+ * `index` is a character offset in the current text value, `deleteCount`
1276
+ * removes characters from that offset, and `insert` adds replacement text.
1277
+ */
965
1278
  export interface MindooTextEdit {
966
1279
  index: number;
967
1280
  deleteCount: number;
968
1281
  insert?: string;
969
1282
  }
1283
+ /**
1284
+ * Ordered text edits for one string field in a MindooDoc.
1285
+ *
1286
+ * `path` addresses the target string inside the document payload. When
1287
+ * supplied, `baseHeads` identifies the Automerge heads the caller based the
1288
+ * edit on, allowing the backend to apply order-sensitive text changes in the
1289
+ * intended causal context.
1290
+ */
970
1291
  export interface MindooTextPatch {
971
1292
  path: Array<string | number>;
972
1293
  baseHeads?: string[];
973
1294
  edits: MindooTextEdit[];
974
1295
  }
1296
+ /** Result returned after applying a text patch and materializing the document. */
975
1297
  export interface MindooTextPatchResult {
976
1298
  doc: MindooDoc;
977
1299
  heads: string[];
978
1300
  data: MindooDocPayload;
979
1301
  }
1302
+ /** Sets a value at `path`, creating or replacing that payload field. */
1303
+ export interface MindooJsonSetPatch {
1304
+ path: Array<string | number>;
1305
+ value: unknown;
1306
+ }
1307
+ /** Removes the value at `path` from the payload object or list. */
1308
+ export interface MindooJsonUnsetPatch {
1309
+ path: Array<string | number>;
1310
+ }
1311
+ /** Inserts one or more values into the list located at `path`. */
1312
+ export interface MindooJsonListInsertPatch {
1313
+ path: Array<string | number>;
1314
+ index: number;
1315
+ values: unknown[];
1316
+ }
1317
+ /** Deletes one or more values from the list located at `path`. */
1318
+ export interface MindooJsonListDeletePatch {
1319
+ path: Array<string | number>;
1320
+ index: number;
1321
+ deleteCount: number;
1322
+ }
1323
+ /**
1324
+ * Granular JSON mutation batch for a MindooDoc payload.
1325
+ *
1326
+ * Operations are applied in patch order by operation group. `baseHeads` has the
1327
+ * same role as in text patches: it records the document heads the caller saw
1328
+ * before constructing operations whose order matters, such as list inserts.
1329
+ */
1330
+ export interface MindooJsonPatch {
1331
+ baseHeads?: string[];
1332
+ set?: MindooJsonSetPatch[];
1333
+ unset?: MindooJsonUnsetPatch[];
1334
+ listInsert?: MindooJsonListInsertPatch[];
1335
+ listDelete?: MindooJsonListDeletePatch[];
1336
+ }
1337
+ /** Result returned after applying a JSON patch and materializing the document. */
1338
+ export interface MindooJsonPatchResult {
1339
+ doc: MindooDoc;
1340
+ heads: string[];
1341
+ data: MindooDocPayload;
1342
+ }
980
1343
  export interface DirectoryUserDetails {
981
1344
  [key: string]: string;
982
1345
  }
@@ -1281,6 +1644,13 @@ export interface OpenDBOptions extends OpenStoreOptions {
1281
1644
  * This is used for the directory database to prevent recursion and ensure security.
1282
1645
  */
1283
1646
  adminOnlyDb?: boolean;
1647
+ /**
1648
+ * Optional read-only historical cutoff for opening the database.
1649
+ *
1650
+ * When set, entries with createdAt >= timeTravelDate are ignored. The
1651
+ * normalized value maps to store-level creationDateUntil semantics.
1652
+ */
1653
+ timeTravelDate?: number | Date | string;
1284
1654
  /**
1285
1655
  * Configuration for attachment handling (chunk size, etc.)
1286
1656
  * If not provided, defaults are used.
@@ -1341,7 +1711,7 @@ export interface PerformanceCallback {
1341
1711
  * Called for bounded history/time-travel workflows.
1342
1712
  */
1343
1713
  onHistoryOperation?: (metrics: {
1344
- operation: 'getDocumentAtTimestamp' | 'getDocumentHistoryPage' | 'analyzeDocumentDagAtTimestamp' | 'materializeDocumentBranchAtEntry' | 'materializeDocumentBranchAtTimestamp' | 'describeDocumentDagEntry';
1714
+ operation: 'getDocumentAtTimestamp' | 'getDocumentHistoryPage' | 'analyzeDocumentDagAtTimestamp' | 'materializeDocumentBranchAtEntry' | 'materializeDocumentBranchAtTimestamp' | 'describeDocumentDagEntry' | 'analyzeDocumentConflicts' | 'getDocumentConflictReport' | 'getDocumentConflictBaseValues';
1345
1715
  docId: string;
1346
1716
  time: number;
1347
1717
  scannedEntries: number;
@@ -1472,6 +1842,177 @@ export interface DocumentDagEntryDetails {
1472
1842
  automergeHash: string | null;
1473
1843
  decodedChange: DocumentDagDecodedChangeSummary | null;
1474
1844
  }
1845
+ /**
1846
+ * Conflict analysis mode controls how much history the analyzer traverses.
1847
+ */
1848
+ export type DocumentConflictAnalysisMode = "quick" | "full";
1849
+ /**
1850
+ * Conflict detail level controls whether conflicting values are summarized.
1851
+ */
1852
+ export type DocumentConflictDetailLevel = "paths-only" | "values";
1853
+ /**
1854
+ * JSON-safe path segment returned by conflict analysis.
1855
+ */
1856
+ export type DocumentConflictPathSegment = string | number;
1857
+ /**
1858
+ * JSON-safe value summary for one conflicting value.
1859
+ *
1860
+ * `value` is included only when it can be represented safely without exposing
1861
+ * CRDT-specific objects. `preview` is always intended for compact UI display.
1862
+ */
1863
+ export interface DocumentConflictValueSummary {
1864
+ conflictId: string;
1865
+ preview: string | null;
1866
+ value?: unknown;
1867
+ isWinner: boolean;
1868
+ }
1869
+ /**
1870
+ * One conflicted document path detected during analysis.
1871
+ */
1872
+ export interface DocumentConflictPath {
1873
+ path: DocumentConflictPathSegment[];
1874
+ pathString: string;
1875
+ values?: DocumentConflictValueSummary[];
1876
+ }
1877
+ /**
1878
+ * Describes where in the document DAG a conflict observation was made.
1879
+ */
1880
+ export interface DocumentConflictLocation {
1881
+ kind: "entry-after" | "active-heads" | "merge-deps";
1882
+ entryId?: string;
1883
+ createdAt?: number;
1884
+ receiptOrder?: number;
1885
+ createdByPublicKey?: string;
1886
+ headEntryIds: string[];
1887
+ automergeHeads: string[];
1888
+ }
1889
+ /**
1890
+ * Conflict observation or resolution event for one document.
1891
+ */
1892
+ export interface DocumentConflictSummary {
1893
+ docId: string;
1894
+ location: DocumentConflictLocation;
1895
+ paths: DocumentConflictPath[];
1896
+ }
1897
+ /**
1898
+ * Durable cursor for incremental conflict scans.
1899
+ *
1900
+ * `changeSeqAsOf` is used for cheap document-level pre-filtering through the
1901
+ * document index. `storeReceiptOrderAsOf` is used for per-event filtering so
1902
+ * late-arriving sync entries are surfaced even if their author timestamp is old.
1903
+ */
1904
+ export interface ConflictScanCheckpoint {
1905
+ changeSeqAsOf: number;
1906
+ storeReceiptOrderAsOf?: number;
1907
+ takenAt: number;
1908
+ }
1909
+ /**
1910
+ * Options accepted by conflict analysis APIs.
1911
+ */
1912
+ export interface DocumentConflictAnalysisOptions {
1913
+ mode?: DocumentConflictAnalysisMode;
1914
+ detail?: DocumentConflictDetailLevel;
1915
+ pageSize?: number;
1916
+ yieldEveryMs?: number;
1917
+ maxConflictsPerDoc?: number;
1918
+ since?: ConflictScanCheckpoint;
1919
+ includeUnresolvedFromBefore?: boolean;
1920
+ signal?: AbortSignal;
1921
+ }
1922
+ /**
1923
+ * Options accepted by the convenience report API.
1924
+ */
1925
+ export interface DocumentConflictReportOptions extends Omit<DocumentConflictAnalysisOptions, "mode" | "maxConflictsPerDoc"> {
1926
+ }
1927
+ /**
1928
+ * Streaming event emitted by `analyzeDocumentConflicts()`.
1929
+ */
1930
+ export type DocumentConflictAnalysisEvent = {
1931
+ type: "progress";
1932
+ scannedDocs: number;
1933
+ totalDocs: number;
1934
+ docId?: string;
1935
+ scannedEntries?: number;
1936
+ message: string;
1937
+ } | {
1938
+ type: "docStart";
1939
+ docId: string;
1940
+ } | {
1941
+ type: "conflictDetected";
1942
+ conflict: DocumentConflictSummary;
1943
+ quick: boolean;
1944
+ } | {
1945
+ type: "conflictResolved";
1946
+ docId: string;
1947
+ entryId: string;
1948
+ createdAt: number;
1949
+ receiptOrder?: number;
1950
+ createdByPublicKey: string;
1951
+ path: DocumentConflictPath;
1952
+ automergeHash: string | null;
1953
+ } | {
1954
+ type: "docDone";
1955
+ docId: string;
1956
+ hadConflicts: boolean;
1957
+ conflictsFound: number;
1958
+ entriesScanned: number;
1959
+ } | {
1960
+ type: "error";
1961
+ docId?: string;
1962
+ entryId?: string;
1963
+ error: unknown;
1964
+ } | {
1965
+ type: "scanCheckpoint";
1966
+ checkpoint: ConflictScanCheckpoint;
1967
+ };
1968
+ /**
1969
+ * Full conflict report for one document.
1970
+ */
1971
+ export interface DocumentConflictReport {
1972
+ docId: string;
1973
+ hadConflicts: boolean;
1974
+ conflictsFound: number;
1975
+ conflicts: DocumentConflictSummary[];
1976
+ resolutions: Array<Extract<DocumentConflictAnalysisEvent, {
1977
+ type: "conflictResolved";
1978
+ }>>;
1979
+ entriesScanned: number;
1980
+ errors: Array<Extract<DocumentConflictAnalysisEvent, {
1981
+ type: "error";
1982
+ }>>;
1983
+ scanCheckpoint: ConflictScanCheckpoint | null;
1984
+ }
1985
+ /**
1986
+ * Request to look up the value at a conflicted path as it stood at the merge
1987
+ * base of the contributing branches (the "old" value before the conflict).
1988
+ */
1989
+ export interface DocumentConflictBaseValueQuery {
1990
+ /** Conflict location as returned by `getDocumentConflictReport`. */
1991
+ location: DocumentConflictLocation;
1992
+ /** Structured path segments (matches `DocumentConflictPath.path`). */
1993
+ path: DocumentConflictPathSegment[];
1994
+ /** Stable string key (matches `DocumentConflictPath.pathString`). */
1995
+ pathString: string;
1996
+ }
1997
+ /**
1998
+ * Result for one base-value lookup.
1999
+ *
2000
+ * - `available`: a previous value existed at the merge base; `preview` and
2001
+ * `value` are populated.
2002
+ * - `no-prior-value`: a merge base was found but the path did not exist there
2003
+ * (e.g. both branches independently created the field).
2004
+ * - `no-base`: no shared ancestor could be determined for the contributing
2005
+ * heads (e.g. the conflict has only one head, or the parent set is empty).
2006
+ * - `missing-entry`: the conflict's `entryId` is not (or no longer) in the
2007
+ * local store; the base value cannot be resolved.
2008
+ */
2009
+ export interface DocumentConflictBaseValue {
2010
+ pathString: string;
2011
+ baseEntryId: string | null;
2012
+ status: "available" | "no-prior-value" | "no-base" | "missing-entry";
2013
+ preview: string | null;
2014
+ value?: unknown;
2015
+ }
1475
2016
  /**
1476
2017
  * Result of reconstructing one branch-local document state for a selected DAG head.
1477
2018
  */
@@ -1568,6 +2109,18 @@ export interface MindooDB {
1568
2109
  * @return True if admin-only mode is enabled
1569
2110
  */
1570
2111
  isAdminOnlyDb(): boolean;
2112
+ /**
2113
+ * True when this database was opened with a timeTravelDate cutoff.
2114
+ */
2115
+ isTimeTravelMode(): boolean;
2116
+ /**
2117
+ * True when mutating APIs are disabled for this database instance.
2118
+ */
2119
+ isReadOnly(): boolean;
2120
+ /**
2121
+ * Normalized time travel cutoff in epoch milliseconds, or null for live mode.
2122
+ */
2123
+ getTimeTravelDate(): number | null;
1571
2124
  /**
1572
2125
  * Get the content-addressed store that is used to store document changes for this database.
1573
2126
  *
@@ -1580,6 +2133,9 @@ export interface MindooDB {
1580
2133
  * @return The content-addressed store for attachments
1581
2134
  */
1582
2135
  getAttachmentStore(): ContentAddressedStore;
2136
+ reclaimIncompleteAttachmentUploads?(options?: {
2137
+ minAgeMs?: number;
2138
+ }): Promise<IncompleteAttachmentUploadReclaimResult>;
1583
2139
  /**
1584
2140
  * Create a new document.
1585
2141
  *
@@ -1696,6 +2252,36 @@ export interface MindooDB {
1696
2252
  * Decode and summarize one DAG entry for hover/tooling use cases.
1697
2253
  */
1698
2254
  describeDocumentDagEntry(docId: string, entryId: string): Promise<DocumentDagEntryDetails | null>;
2255
+ /**
2256
+ * Stream conflict analysis events for one or more documents.
2257
+ *
2258
+ * The API intentionally returns MindooDB DTOs only. It does not expose
2259
+ * Automerge documents, patches, or operation objects so the storage backend
2260
+ * can evolve without changing callers.
2261
+ */
2262
+ analyzeDocumentConflicts(docIds: string[], options?: DocumentConflictAnalysisOptions): AsyncGenerator<DocumentConflictAnalysisEvent, void, unknown>;
2263
+ /**
2264
+ * Build a full conflict report for one document by consuming the streaming
2265
+ * analyzer internally.
2266
+ */
2267
+ getDocumentConflictReport(docId: string, options?: DocumentConflictReportOptions): Promise<DocumentConflictReport>;
2268
+ /**
2269
+ * Resolve the value each conflicted path had at its merge base (the most
2270
+ * recent common ancestor of the branches that contributed to the conflict).
2271
+ *
2272
+ * Each query independently maps to a base value so the caller can request
2273
+ * lookups lazily — for example, only for the paths it currently displays.
2274
+ * Queries that share a merge base are coalesced internally so the
2275
+ * underlying document state is materialized at most once per base entry.
2276
+ */
2277
+ getDocumentConflictBaseValues(docId: string, queries: DocumentConflictBaseValueQuery[]): Promise<DocumentConflictBaseValue[]>;
2278
+ /**
2279
+ * Return an incremental conflict-scan checkpoint for the current local view.
2280
+ *
2281
+ * Callers can persist this value after presenting scan results and pass it
2282
+ * back as `DocumentConflictAnalysisOptions.since` on the next run.
2283
+ */
2284
+ getConflictScanCheckpoint(): Promise<ConflictScanCheckpoint>;
1699
2285
  /**
1700
2286
  * Get all non-deleted document IDs in this database.
1701
2287
  *
@@ -1789,6 +2375,15 @@ export interface MindooDB {
1789
2375
  * overwriting concurrent edits that arrived meanwhile.
1790
2376
  */
1791
2377
  applyTextPatch(doc: MindooDoc, patch: MindooTextPatch): Promise<MindooTextPatchResult>;
2378
+ /**
2379
+ * Apply granular JSON edits at document paths.
2380
+ *
2381
+ * If `baseHeads` is provided, edits are applied at that historical Automerge
2382
+ * version using `changeAt`, then merged into the current document. This lets
2383
+ * apps flush buffered object/list operations based on a stale local copy
2384
+ * without replacing concurrent changes that arrived meanwhile.
2385
+ */
2386
+ applyJsonPatch(doc: MindooDoc, patch: MindooJsonPatch): Promise<MindooJsonPatchResult>;
1792
2387
  /**
1793
2388
  * Change a document using a specific signing key.
1794
2389
  * This is like changeDoc but allows signing with a different key than the current user's.
@@ -1850,9 +2445,87 @@ export interface MindooDB {
1850
2445
  * then processes them to update the cached database documents and index.
1851
2446
  * After processing, the new change hashes are appended to the stored list.
1852
2447
  *
1853
- * @return A promise that resolves when the sync is complete
2448
+ * Sync deliberately does NOT trigger the L2 background warmer. Callers
2449
+ * that want to warm the L2 cache after a sync (e.g. the Haven sync
2450
+ * page once a sync settles) should call {@link startBackgroundWarmer}
2451
+ * explicitly. This keeps casual `syncStoreChanges()` calls cheap and
2452
+ * leaves warmer scheduling to the caller, which knows whether the
2453
+ * cost of a full L2 walk is worth paying for the current workload.
2454
+ *
2455
+ * @return A promise that resolves when the sync is complete.
1854
2456
  */
1855
2457
  syncStoreChanges(): Promise<void>;
2458
+ /**
2459
+ * Reconcile local document visibility with the current tenant KeyBag.
2460
+ *
2461
+ * Reveals documents whose decryption keys are now available, hides
2462
+ * documents whose keys have been removed, and purges any plaintext
2463
+ * caches for inaccessible documents. The method only touches in-memory
2464
+ * and L2 caches owned by this database instance - it does not write
2465
+ * synthetic store entries and is safe to call repeatedly.
2466
+ *
2467
+ * Optional so alternative database implementations (e.g. read-only
2468
+ * views, time-travel snapshots) can omit it.
2469
+ */
2470
+ reconcileKeyVisibility?(): Promise<void>;
2471
+ /**
2472
+ * Start the L2 background warmer.
2473
+ *
2474
+ * Walks the in-memory document index and ensures every document has
2475
+ * an up-to-date L2 cache record (using the same internal load-from-L2
2476
+ * code path foreground reads use). Runs in batches with cooperative
2477
+ * yields so foreground operations are not starved.
2478
+ *
2479
+ * Intended to be called explicitly by long-lived sessions that just
2480
+ * finished a sync (e.g. the Haven sync page after pulling/pushing
2481
+ * completes) so subsequent reads - especially virtual view rebuilds
2482
+ * - hit the fast L2 path. Casual database opens should NOT call this;
2483
+ * pay the warmer cost only when the workload is about to benefit
2484
+ * from a fully warm L2.
2485
+ *
2486
+ * Single-flight: a second call while the warmer is already running
2487
+ * returns the same in-flight promise rather than starting a second
2488
+ * pass. The returned promise resolves when the warmer has finished
2489
+ * (either by visiting every document or by being cancelled via
2490
+ * {@link stopBackgroundWarmer} or via the optional `signal`).
2491
+ *
2492
+ * @param options See {@link StartBackgroundWarmerOptions}: optional
2493
+ * `signal` for per-call cancellation and `onProgress` for driving
2494
+ * a progress UI.
2495
+ */
2496
+ startBackgroundWarmer?(options?: StartBackgroundWarmerOptions): Promise<void>;
2497
+ /**
2498
+ * Cancel the running background warmer (if any). Returns a promise
2499
+ * that resolves once the in-flight warmer pass has observed the
2500
+ * cancellation and returned (so callers can `await` clean shutdown).
2501
+ * Safe to call when no warmer is running - resolves immediately in
2502
+ * that case.
2503
+ *
2504
+ * Useful before initiating destructive operations (key rotation, data
2505
+ * wipe, etc.) so the warmer cannot race against the mutation, and at
2506
+ * the end of tests to ensure no background work outlives the test.
2507
+ */
2508
+ stopBackgroundWarmer?(): Promise<void>;
2509
+ /**
2510
+ * Returns `true` while the L2 background warmer is actively
2511
+ * processing documents. Useful for "Warming…" UI indicators and
2512
+ * tests that need to coordinate with warmer progress.
2513
+ */
2514
+ isWarmerRunning?(): boolean;
2515
+ /**
2516
+ * Snapshot of the current (or most recent) background warmer pass's
2517
+ * progress, or `null` if no warmer has ever run on this database
2518
+ * instance. Designed for UIs that mount AFTER the warmer has already
2519
+ * started (e.g. the user navigates back to the Sync page mid-warm)
2520
+ * and need to "catch up" on state without missing onProgress events.
2521
+ *
2522
+ * After a warmer settles, the returned snapshot persists with its
2523
+ * final `phase` (`"done"` or `"cancelled"`) so the UI can decide
2524
+ * whether to keep showing a "Optimized" message or fade away. The
2525
+ * snapshot is replaced on the next {@link startBackgroundWarmer}
2526
+ * call.
2527
+ */
2528
+ getBackgroundWarmerProgress?(): BackgroundWarmerProgress | null;
1856
2529
  /**
1857
2530
  * Pull changes from a remote content-addressed store or another MindooDB instance.
1858
2531
  *