mindoodb 0.0.9 → 0.0.11

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 (77) hide show
  1. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -1
  2. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +3 -5
  3. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +7 -2
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  6. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +23 -3
  7. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  8. package/dist/appendonlystores/network/HttpTransport.d.ts +5 -0
  9. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  10. package/dist/appendonlystores/network/HttpTransport.js +66 -36
  11. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  12. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +4 -1
  13. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
  14. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +15 -3
  15. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
  16. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts.map +1 -1
  17. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js +7 -3
  18. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js.map +1 -1
  19. package/dist/browser/index.mjs +1 -0
  20. package/dist/core/BaseMindooDB.d.ts +44 -5
  21. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  22. package/dist/core/BaseMindooDB.js +244 -53
  23. package/dist/core/BaseMindooDB.js.map +1 -1
  24. package/dist/core/BaseMindooTenant.d.ts +2 -3
  25. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  26. package/dist/core/BaseMindooTenant.js +9 -18
  27. package/dist/core/BaseMindooTenant.js.map +1 -1
  28. package/dist/core/BaseMindooTenantDirectory.d.ts +1 -0
  29. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  30. package/dist/core/BaseMindooTenantDirectory.js +24 -8
  31. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  32. package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts.map +1 -1
  33. package/dist/core/appendonlystores/AttachmentReadPlanner.js +1 -1
  34. package/dist/core/appendonlystores/AttachmentReadPlanner.js.map +1 -1
  35. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +4 -2
  36. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  37. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +13 -8
  38. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  39. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +5 -1
  40. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  41. package/dist/core/appendonlystores/types.d.ts +14 -10
  42. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  43. package/dist/core/appendonlystores/types.js +6 -0
  44. package/dist/core/appendonlystores/types.js.map +1 -1
  45. package/dist/core/types.d.ts +24 -12
  46. package/dist/core/types.d.ts.map +1 -1
  47. package/dist/core/types.js +3 -1
  48. package/dist/core/types.js.map +1 -1
  49. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +5 -2
  50. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  51. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +10 -4
  52. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  53. package/dist/node/server/MindooDBServer.d.ts +105 -0
  54. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  55. package/dist/node/server/MindooDBServer.js +240 -66
  56. package/dist/node/server/MindooDBServer.js.map +1 -1
  57. package/dist/node/server/ServerSync.d.ts +1 -1
  58. package/dist/node/server/ServerSync.d.ts.map +1 -1
  59. package/dist/node/server/ServerSync.js +2 -1
  60. package/dist/node/server/ServerSync.js.map +1 -1
  61. package/dist/node/server/StoreFactory.d.ts +3 -3
  62. package/dist/node/server/StoreFactory.d.ts.map +1 -1
  63. package/dist/node/server/StoreFactory.js +8 -8
  64. package/dist/node/server/StoreFactory.js.map +1 -1
  65. package/dist/node/server/TenantManager.d.ts +5 -3
  66. package/dist/node/server/TenantManager.d.ts.map +1 -1
  67. package/dist/node/server/TenantManager.js +33 -20
  68. package/dist/node/server/TenantManager.js.map +1 -1
  69. package/dist/node/server/config.d.ts.map +1 -1
  70. package/dist/node/server/config.js +4 -0
  71. package/dist/node/server/config.js.map +1 -1
  72. package/dist/node/server/server.js +6 -5
  73. package/dist/node/server/server.js.map +1 -1
  74. package/dist/node/server/types.d.ts +1 -0
  75. package/dist/node/server/types.d.ts.map +1 -1
  76. package/dist/node/server/types.js.map +1 -1
  77. package/package.json +1 -1
@@ -6,6 +6,7 @@ exports.BaseMindooDB = void 0;
6
6
  // Browser/Node.js: WASM (@automerge/automerge/slim)
7
7
  const automerge_adapter_1 = require("./automerge-adapter");
8
8
  const uuid_1 = require("uuid");
9
+ const types_1 = require("./appendonlystores/types");
9
10
  const bloom_1 = require("./appendonlystores/bloom");
10
11
  const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
11
12
  const DocumentDagAnalysis_1 = require("./DocumentDagAnalysis");
@@ -17,6 +18,7 @@ const logging_1 = require("./logging");
17
18
  * Default chunk size for attachments: 256KB
18
19
  */
19
20
  const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
21
+ const DEFAULT_ATTACHMENT_STREAM_BATCH_SIZE = 4;
20
22
  const DEFAULT_SNAPSHOT_MIN_CHANGES = 100;
21
23
  const DEFAULT_SNAPSHOT_COOLDOWN_MS = 10 * 60 * 1000;
22
24
  const DEFAULT_MAX_CACHED_DOCS = 128;
@@ -64,6 +66,12 @@ class BaseMindooDB {
64
66
  this.cachePrefix = null;
65
67
  this.dirtyDocIds = new Set();
66
68
  this.cacheMetaDirty = false;
69
+ if (store.getStoreKind() !== types_1.StoreKind.docs) {
70
+ throw new Error(`[BaseMindooDB] Expected primary store kind ${types_1.StoreKind.docs} but received ${store.getStoreKind()}`);
71
+ }
72
+ if (attachmentStore.getStoreKind() !== types_1.StoreKind.attachments) {
73
+ throw new Error(`[BaseMindooDB] Expected attachment store kind ${types_1.StoreKind.attachments} but received ${attachmentStore.getStoreKind()}`);
74
+ }
67
75
  this.tenant = tenant;
68
76
  this.store = store;
69
77
  this.attachmentStore = attachmentStore;
@@ -738,6 +746,11 @@ class BaseMindooDB {
738
746
  return null;
739
747
  }
740
748
  }
749
+ static setSyncAbortSignalOnStore(store, signal) {
750
+ if ('setSyncAbortSignal' in store && typeof store.setSyncAbortSignal === 'function') {
751
+ store.setSyncAbortSignal(signal);
752
+ }
753
+ }
741
754
  /**
742
755
  * From a list of candidate IDs, return only those the target store is
743
756
  * missing. Uses bloom-filter pre-screening when available, then falls
@@ -766,14 +779,126 @@ class BaseMindooDB {
766
779
  }
767
780
  return missingIds;
768
781
  }
782
+ /**
783
+ * Determine how many entry IDs to fetch per `getEntries` call during sync.
784
+ *
785
+ * This is intentionally separate from the metadata scan `pageSize` so that
786
+ * scanning can page through large ID lists quickly while the heavier
787
+ * payload downloads use a smaller batch to keep progress responsive and
788
+ * cancellation timely.
789
+ *
790
+ * Priority: explicit option > attachment default (100) > pageSize fallback.
791
+ */
792
+ resolveTransferBatchSize(options) {
793
+ if (options?.transferBatchSize && options.transferBatchSize > 0) {
794
+ return options.transferBatchSize;
795
+ }
796
+ if (options?.storeKind === types_1.StoreKind.attachments) {
797
+ return 100;
798
+ }
799
+ return options?.pageSize ?? 1000;
800
+ }
801
+ /**
802
+ * Transfer a set of entry IDs from source to target in fixed-size batches,
803
+ * emitting progress and checking for cancellation between each batch.
804
+ *
805
+ * Callers (cursor-scan path and legacy path) collect the IDs that need
806
+ * transferring, then delegate to this method instead of issuing one
807
+ * monolithic `getEntries`. This gives three benefits:
808
+ *
809
+ * 1. The UI receives frequent progress updates with batch metadata so long
810
+ * transfers no longer look frozen.
811
+ * 2. Cancellation can interrupt work between batches rather than waiting
812
+ * for one large HTTP response to complete.
813
+ * 3. Each server-side `getEntries` + RSA encryption unit is smaller,
814
+ * reducing the risk of socket timeouts on large payloads.
815
+ *
816
+ * Returns partial progress on cancellation so callers can report how much
817
+ * was actually transferred before the abort.
818
+ */
819
+ async transferEntriesInBatches(sourceStore, targetStore, entryIds, options, state) {
820
+ if (entryIds.length === 0) {
821
+ return { transferred: state.transferred, cancelled: false };
822
+ }
823
+ const onProgress = options?.onProgress;
824
+ const signal = options?.signal;
825
+ const transferBatchSize = this.resolveTransferBatchSize(options);
826
+ const totalTransferBatches = Math.max(1, Math.ceil(entryIds.length / transferBatchSize));
827
+ let transferred = state.transferred;
828
+ for (let offset = 0; offset < entryIds.length; offset += transferBatchSize) {
829
+ if (signal?.aborted) {
830
+ return { transferred, cancelled: true };
831
+ }
832
+ const currentTransferBatch = Math.floor(offset / transferBatchSize) + 1;
833
+ const batchIds = entryIds.slice(offset, offset + transferBatchSize);
834
+ const pageSummary = state.currentPage ? `page ${state.currentPage}, ` : "";
835
+ onProgress?.({
836
+ phase: "transferring",
837
+ message: `Transferring batch ${currentTransferBatch}/${totalTransferBatches} (${batchIds.length} entries, ${pageSummary}scanned ${state.scanned})...`,
838
+ transferredEntries: transferred,
839
+ scannedEntries: state.scanned,
840
+ totalSourceEntries: state.totalSourceEntries,
841
+ currentPage: state.currentPage,
842
+ currentTransferBatch,
843
+ totalTransferBatches,
844
+ transferBatchSize,
845
+ });
846
+ try {
847
+ const batchEntries = await sourceStore.getEntries(batchIds);
848
+ if (signal?.aborted) {
849
+ return { transferred, cancelled: true };
850
+ }
851
+ await targetStore.putEntries(batchEntries);
852
+ transferred += batchEntries.length;
853
+ }
854
+ catch (error) {
855
+ if (signal?.aborted) {
856
+ return { transferred, cancelled: true };
857
+ }
858
+ throw error;
859
+ }
860
+ onProgress?.({
861
+ phase: "transferring",
862
+ message: `Transferred ${transferred} entries after batch ${currentTransferBatch}/${totalTransferBatches}`,
863
+ transferredEntries: transferred,
864
+ scannedEntries: state.scanned,
865
+ totalSourceEntries: state.totalSourceEntries,
866
+ currentPage: state.currentPage,
867
+ currentTransferBatch,
868
+ totalTransferBatches,
869
+ transferBatchSize,
870
+ });
871
+ }
872
+ return { transferred, cancelled: false };
873
+ }
769
874
  async syncEntriesFromStore(sourceStore, targetStore, options) {
770
- if (options?.mode === "dense") {
771
- return this.syncEntriesFromStoreDense(sourceStore, targetStore, options);
875
+ const signal = options?.signal;
876
+ BaseMindooDB.setSyncAbortSignalOnStore(sourceStore, signal);
877
+ BaseMindooDB.setSyncAbortSignalOnStore(targetStore, signal);
878
+ try {
879
+ if (options?.mode === "dense") {
880
+ return await this.syncEntriesFromStoreDense(sourceStore, targetStore, options);
881
+ }
882
+ return await this.syncEntriesFromStoreImpl(sourceStore, targetStore, options);
883
+ }
884
+ catch (error) {
885
+ if (signal?.aborted) {
886
+ this.logger.info("Sync cancelled by abort signal");
887
+ return { transferred: 0, scanned: 0, cancelled: true };
888
+ }
889
+ throw error;
890
+ }
891
+ finally {
892
+ BaseMindooDB.setSyncAbortSignalOnStore(sourceStore, undefined);
893
+ BaseMindooDB.setSyncAbortSignalOnStore(targetStore, undefined);
772
894
  }
895
+ }
896
+ async syncEntriesFromStoreImpl(sourceStore, targetStore, options) {
773
897
  let transferred = 0;
774
898
  let scanned = 0;
775
899
  const onProgress = options?.onProgress;
776
900
  const pageSize = options?.pageSize ?? 1000;
901
+ const signal = options?.signal;
777
902
  const targetBloom = await this.getTargetBloomSummary(targetStore);
778
903
  const totalSourceEstimate = targetBloom?.totalIds;
779
904
  if (this.supportsCursorScan(sourceStore)) {
@@ -787,19 +912,40 @@ class BaseMindooDB {
787
912
  let cursor = null;
788
913
  let currentPage = 0;
789
914
  while (true) {
790
- if (options?.signal?.aborted) {
915
+ if (signal?.aborted) {
791
916
  return { transferred, scanned, cancelled: true };
792
917
  }
793
918
  const page = await sourceStore.scanEntriesSince(cursor, pageSize);
794
919
  currentPage++;
795
920
  scanned += page.entries.length;
921
+ if (signal?.aborted) {
922
+ return { transferred, scanned, cancelled: true };
923
+ }
796
924
  if (page.entries.length > 0) {
925
+ onProgress?.({
926
+ phase: 'transferring',
927
+ message: `Scanned ${scanned} entries, checking for changes (page ${currentPage})...`,
928
+ transferredEntries: transferred,
929
+ scannedEntries: scanned,
930
+ totalSourceEntries: totalSourceEstimate,
931
+ currentPage,
932
+ });
797
933
  const ids = page.entries.map((m) => m.id);
798
934
  const missingIds = await this.filterMissingIds(targetStore, ids, targetBloom);
935
+ if (signal?.aborted) {
936
+ return { transferred, scanned, cancelled: true };
937
+ }
799
938
  if (missingIds.length > 0) {
800
- const missingEntries = await sourceStore.getEntries(missingIds);
801
- await targetStore.putEntries(missingEntries);
802
- transferred += missingEntries.length;
939
+ const transferResult = await this.transferEntriesInBatches(sourceStore, targetStore, missingIds, options, {
940
+ transferred,
941
+ scanned,
942
+ totalSourceEntries: totalSourceEstimate,
943
+ currentPage,
944
+ });
945
+ transferred = transferResult.transferred;
946
+ if (transferResult.cancelled) {
947
+ return { transferred, scanned, cancelled: true };
948
+ }
803
949
  }
804
950
  }
805
951
  onProgress?.({
@@ -834,21 +980,28 @@ class BaseMindooDB {
834
980
  onProgress?.({
835
981
  phase: 'transferring',
836
982
  message: `Transferring ${sourceNewMetadata.length} entries...`,
837
- transferredEntries: 0,
983
+ transferredEntries: transferred,
838
984
  scannedEntries: sourceNewMetadata.length,
839
985
  totalSourceEntries: sourceNewMetadata.length,
840
986
  });
841
- const sourceEntries = await sourceStore.getEntries(sourceNewMetadata.map((m) => m.id));
842
- await targetStore.putEntries(sourceEntries);
987
+ const transferResult = await this.transferEntriesInBatches(sourceStore, targetStore, sourceNewMetadata.map((m) => m.id), options, {
988
+ transferred,
989
+ scanned: sourceNewMetadata.length,
990
+ totalSourceEntries: sourceNewMetadata.length,
991
+ });
992
+ transferred = transferResult.transferred;
993
+ if (transferResult.cancelled) {
994
+ return { transferred, scanned: sourceNewMetadata.length, cancelled: true };
995
+ }
843
996
  onProgress?.({
844
997
  phase: 'transferring',
845
- message: `Transferred ${sourceEntries.length} entries`,
846
- transferredEntries: sourceEntries.length,
998
+ message: `Transferred ${transferred} entries`,
999
+ transferredEntries: transferred,
847
1000
  scannedEntries: sourceNewMetadata.length,
848
1001
  totalSourceEntries: sourceNewMetadata.length,
849
1002
  });
850
1003
  return {
851
- transferred: sourceEntries.length,
1004
+ transferred,
852
1005
  scanned: sourceNewMetadata.length,
853
1006
  cancelled: false,
854
1007
  };
@@ -3142,10 +3295,10 @@ class BaseMindooDB {
3142
3295
  /**
3143
3296
  * Get an attachment reference by ID from a document's _attachments array.
3144
3297
  */
3145
- getAttachmentRefInternal(docId, attachmentId) {
3146
- const internalDoc = this.getCachedDocument(docId);
3298
+ async getAttachmentRefInternal(docId, attachmentId) {
3299
+ const internalDoc = this.getCachedDocument(docId) ?? await this.loadDocumentInternal(docId);
3147
3300
  if (!internalDoc) {
3148
- throw new Error(`Document ${docId} not found in cache`);
3301
+ throw new Error(`Document ${docId} not found while reading attachment ${attachmentId}`);
3149
3302
  }
3150
3303
  const payload = internalDoc.doc;
3151
3304
  const attachments = payload._attachments || [];
@@ -3193,7 +3346,7 @@ class BaseMindooDB {
3193
3346
  */
3194
3347
  async getAttachmentInternal(docId, attachmentId) {
3195
3348
  this.logger.debug(`Getting attachment ${attachmentId} from document ${docId}`);
3196
- const ref = this.getAttachmentRefInternal(docId, attachmentId);
3349
+ const ref = await this.getAttachmentRefInternal(docId, attachmentId);
3197
3350
  const store = this.getEffectiveAttachmentStore();
3198
3351
  // Resolve dependency chain to get all chunk IDs in order (oldest first)
3199
3352
  const chunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
@@ -3226,7 +3379,7 @@ class BaseMindooDB {
3226
3379
  if (startByte < 0 || endByte <= startByte) {
3227
3380
  throw new Error(`Invalid byte range: [${startByte}, ${endByte})`);
3228
3381
  }
3229
- const ref = this.getAttachmentRefInternal(docId, attachmentId);
3382
+ const ref = await this.getAttachmentRefInternal(docId, attachmentId);
3230
3383
  const store = this.getEffectiveAttachmentStore();
3231
3384
  const readPlan = await this.planAttachmentRead(store, ref, startByte, endByte);
3232
3385
  const neededChunkIds = readPlan.chunkPlans.map((chunkPlan) => chunkPlan.id);
@@ -3258,7 +3411,7 @@ class BaseMindooDB {
3258
3411
  */
3259
3412
  async *streamAttachmentInternal(docId, attachmentId, startOffset) {
3260
3413
  this.logger.debug(`Streaming attachment ${attachmentId} from offset ${startOffset}`);
3261
- const ref = this.getAttachmentRefInternal(docId, attachmentId);
3414
+ const ref = await this.getAttachmentRefInternal(docId, attachmentId);
3262
3415
  if (startOffset < 0 || startOffset > ref.size) {
3263
3416
  throw new Error(`Invalid stream offset ${startOffset} for attachment size ${ref.size}`);
3264
3417
  }
@@ -3267,29 +3420,34 @@ class BaseMindooDB {
3267
3420
  }
3268
3421
  const store = this.getEffectiveAttachmentStore();
3269
3422
  const readPlan = await this.planAttachmentRead(store, ref, startOffset, ref.size);
3270
- // Stream chunks starting from startChunkIndex
3271
- for (let i = 0; i < readPlan.chunkPlans.length; i++) {
3272
- const chunkPlan = readPlan.chunkPlans[i];
3273
- const [chunk] = await store.getEntries([chunkPlan.id]);
3274
- if (!chunk) {
3275
- throw new Error(`Attachment chunk ${chunkPlan.id} not found in store`);
3276
- }
3277
- const plaintext = await this.decryptAttachmentChunk(chunk);
3278
- // For first chunk, skip bytes before startOffset
3279
- if (i === 0 && readPlan.offsetInFirstChunk > 0) {
3280
- yield plaintext.slice(readPlan.offsetInFirstChunk);
3281
- }
3282
- else {
3283
- yield plaintext;
3423
+ for (let i = 0; i < readPlan.chunkPlans.length; i += DEFAULT_ATTACHMENT_STREAM_BATCH_SIZE) {
3424
+ const chunkPlansBatch = readPlan.chunkPlans.slice(i, i + DEFAULT_ATTACHMENT_STREAM_BATCH_SIZE);
3425
+ const batchIds = chunkPlansBatch.map((chunkPlan) => chunkPlan.id);
3426
+ const chunks = await store.getEntries(batchIds);
3427
+ const chunkById = new Map(chunks.map((chunk) => [chunk.id, chunk]));
3428
+ for (let batchIndex = 0; batchIndex < chunkPlansBatch.length; batchIndex++) {
3429
+ const chunkPlan = chunkPlansBatch[batchIndex];
3430
+ const chunk = chunkById.get(chunkPlan.id);
3431
+ if (!chunk) {
3432
+ throw new Error(`Attachment chunk ${chunkPlan.id} not found in store. The document metadata exists, but the attachment payload may not be synced locally yet.`);
3433
+ }
3434
+ const plaintext = await this.decryptAttachmentChunk(chunk);
3435
+ // For first chunk, skip bytes before startOffset
3436
+ if (i === 0 && batchIndex === 0 && readPlan.offsetInFirstChunk > 0) {
3437
+ yield plaintext.slice(readPlan.offsetInFirstChunk);
3438
+ }
3439
+ else {
3440
+ yield plaintext;
3441
+ }
3284
3442
  }
3285
3443
  }
3286
3444
  this.logger.debug(`Finished streaming attachment ${attachmentId}`);
3287
3445
  }
3288
3446
  /**
3289
- * Get the effective attachment store (attachment store if configured, otherwise doc store).
3447
+ * Get the attachment store for this database.
3290
3448
  */
3291
3449
  getEffectiveAttachmentStore() {
3292
- return this.attachmentStore || this.store;
3450
+ return this.attachmentStore;
3293
3451
  }
3294
3452
  /**
3295
3453
  * Internal method to add an attachment by chunking the file and storing chunks.
@@ -3484,9 +3642,14 @@ class BaseMindooDB {
3484
3642
  * Resolve a sync target to a ContentAddressedStore.
3485
3643
  * Accepts either a raw store or a MindooDB instance (calls getStore()).
3486
3644
  */
3487
- resolveStore(remote) {
3645
+ getStoreForKind(storeKind = types_1.StoreKind.docs) {
3646
+ return storeKind === types_1.StoreKind.attachments ? this.attachmentStore : this.store;
3647
+ }
3648
+ resolveStore(remote, storeKind = types_1.StoreKind.docs) {
3488
3649
  if ('getStore' in remote && typeof remote.getStore === 'function') {
3489
- return remote.getStore();
3650
+ return storeKind === types_1.StoreKind.attachments
3651
+ ? remote.getAttachmentStore()
3652
+ : remote.getStore();
3490
3653
  }
3491
3654
  return remote;
3492
3655
  }
@@ -3546,19 +3709,24 @@ class BaseMindooDB {
3546
3709
  * 3. Stores them in our local store
3547
3710
  * 4. Syncs the local store to process the new entries
3548
3711
  *
3712
+ * The optional `storeKind` sync option selects which store is synced.
3713
+ * By default, this method syncs the docs store only.
3714
+ *
3549
3715
  * @param remote The remote store or MindooDB instance to pull entries from
3550
- * @param options Optional sync options for progress tracking, paging, and cancellation
3716
+ * @param options Optional sync options for progress tracking, paging, cancellation, and store selection
3551
3717
  * @return A promise that resolves with the sync result
3552
3718
  */
3553
3719
  async pullChangesFrom(remote, options) {
3554
- const remoteStore = this.resolveStore(remote);
3555
- if (this.store.getId() !== remoteStore.getId()) {
3556
- throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
3720
+ const storeKind = options?.storeKind ?? types_1.StoreKind.docs;
3721
+ const localStore = this.getStoreForKind(storeKind);
3722
+ const remoteStore = this.resolveStore(remote, storeKind);
3723
+ if (localStore.getId() !== remoteStore.getId() || localStore.getStoreKind() !== remoteStore.getStoreKind()) {
3724
+ throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
3557
3725
  }
3558
- this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}`);
3726
+ this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}/${remoteStore.getStoreKind()}`);
3559
3727
  const restoreAuthOverride = await this.applyNetworkAuthOverrideForSync(remoteStore, options);
3560
3728
  try {
3561
- const syncResult = await this.syncEntriesFromStore(remoteStore, this.store, options);
3729
+ const syncResult = await this.syncEntriesFromStore(remoteStore, localStore, options);
3562
3730
  this.logger.debug(`Transferred ${syncResult.transferred} entries from remote store`);
3563
3731
  if (syncResult.cancelled) {
3564
3732
  this.logger.info(`Pull cancelled after transferring ${syncResult.transferred} entries`);
@@ -3576,17 +3744,35 @@ class BaseMindooDB {
3576
3744
  phase: 'processing',
3577
3745
  message: `Processing ${syncResult.transferred} new entries...`,
3578
3746
  transferredEntries: syncResult.transferred,
3579
- scannedEntries: syncResult.transferred,
3747
+ scannedEntries: syncResult.scanned,
3580
3748
  });
3581
3749
  // Sync the local store to process the new entries
3582
3750
  // This will update the index, cache, and processedEntryIds
3583
- await this.syncStoreChanges();
3751
+ if (options?.signal?.aborted) {
3752
+ this.logger.info(`Pull cancelled before local processing after transferring ${syncResult.transferred} entries`);
3753
+ return {
3754
+ transferredEntries: syncResult.transferred,
3755
+ scannedEntries: syncResult.scanned,
3756
+ cancelled: true,
3757
+ };
3758
+ }
3759
+ if (storeKind === types_1.StoreKind.docs) {
3760
+ await this.syncStoreChanges();
3761
+ }
3762
+ if (options?.signal?.aborted) {
3763
+ this.logger.info(`Pull cancelled after local processing for ${storeKind} entries`);
3764
+ return {
3765
+ transferredEntries: syncResult.transferred,
3766
+ scannedEntries: syncResult.scanned,
3767
+ cancelled: true,
3768
+ };
3769
+ }
3584
3770
  this.logger.info(`Pull complete, synced ${syncResult.transferred} entries`);
3585
3771
  options?.onProgress?.({
3586
3772
  phase: 'complete',
3587
- message: `Pull complete: ${syncResult.transferred} entries synced`,
3773
+ message: `Pull complete: ${syncResult.transferred} ${storeKind} entries synced`,
3588
3774
  transferredEntries: syncResult.transferred,
3589
- scannedEntries: syncResult.transferred,
3775
+ scannedEntries: syncResult.scanned,
3590
3776
  });
3591
3777
  return {
3592
3778
  transferredEntries: syncResult.transferred,
@@ -3606,19 +3792,24 @@ class BaseMindooDB {
3606
3792
  * 2. Retrieves those entries from our local store
3607
3793
  * 3. Stores them in the remote store
3608
3794
  *
3795
+ * The optional `storeKind` sync option selects which store is synced.
3796
+ * By default, this method syncs the docs store only.
3797
+ *
3609
3798
  * @param remote The remote store or MindooDB instance to push entries to
3610
- * @param options Optional sync options for progress tracking, paging, and cancellation
3799
+ * @param options Optional sync options for progress tracking, paging, cancellation, and store selection
3611
3800
  * @return A promise that resolves with the sync result
3612
3801
  */
3613
3802
  async pushChangesTo(remote, options) {
3614
- const remoteStore = this.resolveStore(remote);
3615
- if (this.store.getId() !== remoteStore.getId()) {
3616
- throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
3803
+ const storeKind = options?.storeKind ?? types_1.StoreKind.docs;
3804
+ const localStore = this.getStoreForKind(storeKind);
3805
+ const remoteStore = this.resolveStore(remote, storeKind);
3806
+ if (localStore.getId() !== remoteStore.getId() || localStore.getStoreKind() !== remoteStore.getStoreKind()) {
3807
+ throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${localStore.getId()}/${localStore.getStoreKind()}`);
3617
3808
  }
3618
- this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}`);
3809
+ this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}/${remoteStore.getStoreKind()}`);
3619
3810
  const restoreAuthOverride = await this.applyNetworkAuthOverrideForSync(remoteStore, options);
3620
3811
  try {
3621
- const syncResult = await this.syncEntriesFromStore(this.store, remoteStore, options);
3812
+ const syncResult = await this.syncEntriesFromStore(localStore, remoteStore, options);
3622
3813
  this.logger.debug(`Transferred ${syncResult.transferred} entries to remote store`);
3623
3814
  if (syncResult.cancelled) {
3624
3815
  this.logger.info(`Push cancelled after transferring ${syncResult.transferred} entries`);
@@ -3638,7 +3829,7 @@ class BaseMindooDB {
3638
3829
  phase: 'complete',
3639
3830
  message: `Push complete: ${syncResult.transferred} entries transferred`,
3640
3831
  transferredEntries: syncResult.transferred,
3641
- scannedEntries: syncResult.transferred,
3832
+ scannedEntries: syncResult.scanned,
3642
3833
  });
3643
3834
  return {
3644
3835
  transferredEntries: syncResult.transferred,