mindoodb 0.0.4 → 0.0.5
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.
- package/README-server.md +34 -5
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +9 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +226 -130
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +12 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +192 -10
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +3 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +26 -4
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +6 -3
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +79 -10
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
- package/dist/core/BaseMindooDB.d.ts +5 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +223 -108
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +1 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +69 -21
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/MindooDBServerAdmin.d.ts +3 -0
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
- package/dist/core/MindooDBServerAdmin.js +57 -1
- package/dist/core/MindooDBServerAdmin.js.map +1 -1
- package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts +22 -0
- package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts.map +1 -0
- package/dist/core/appendonlystores/AttachmentReadPlanner.js +124 -0
- package/dist/core/appendonlystores/AttachmentReadPlanner.js.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +8 -4
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +22 -9
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/MaterializationPlanner.d.ts +35 -0
- package/dist/core/appendonlystores/MaterializationPlanner.d.ts.map +1 -1
- package/dist/core/appendonlystores/MaterializationPlanner.js +1 -0
- package/dist/core/appendonlystores/MaterializationPlanner.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +19 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +33 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.js +2 -0
- package/dist/core/appendonlystores/network/types.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +91 -3
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/types.d.ts +37 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +28 -7
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +115 -17
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts +7 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +232 -16
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +17 -2
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +142 -20
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/types.d.ts +9 -4
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js.map +1 -1
- package/package.json +2 -1
|
@@ -8,6 +8,7 @@ const automerge_adapter_1 = require("./automerge-adapter");
|
|
|
8
8
|
const uuid_1 = require("uuid");
|
|
9
9
|
const bloom_1 = require("./appendonlystores/bloom");
|
|
10
10
|
const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
|
|
11
|
+
const AttachmentReadPlanner_1 = require("./appendonlystores/AttachmentReadPlanner");
|
|
11
12
|
const idGeneration_1 = require("./utils/idGeneration");
|
|
12
13
|
const errors_1 = require("./errors");
|
|
13
14
|
const logging_1 = require("./logging");
|
|
@@ -71,6 +72,7 @@ class BaseMindooDB {
|
|
|
71
72
|
this.iteratePrefetchWindowDocs = Math.max(0, Math.floor(documentCacheConfig?.iteratePrefetchWindowDocs ??
|
|
72
73
|
DEFAULT_ITERATE_PREFETCH_WINDOW_DOCS));
|
|
73
74
|
this.cacheRestoreLimit = Math.max(1, Math.floor(documentCacheConfig?.restoreLimit ?? this.maxCachedDocs));
|
|
75
|
+
this.reconcileRestoredIndexOnInit = documentCacheConfig?.reconcileRestoredIndexOnInit ?? false;
|
|
74
76
|
this.snapshotMinChanges = Math.max(1, Math.floor(snapshotConfig?.minChanges ?? DEFAULT_SNAPSHOT_MIN_CHANGES));
|
|
75
77
|
this.snapshotCooldownMs = Math.max(0, Math.floor(snapshotConfig?.cooldownMs ?? DEFAULT_SNAPSHOT_COOLDOWN_MS));
|
|
76
78
|
// Create logger if not provided (for backward compatibility)
|
|
@@ -184,7 +186,7 @@ class BaseMindooDB {
|
|
|
184
186
|
}
|
|
185
187
|
exportMetadataCheckpoint() {
|
|
186
188
|
const checkpoint = {
|
|
187
|
-
version:
|
|
189
|
+
version: 2,
|
|
188
190
|
processedEntryCursor: this.processedEntryCursor,
|
|
189
191
|
index: this.index,
|
|
190
192
|
nextChangeSeq: this.nextChangeSeq,
|
|
@@ -214,7 +216,7 @@ class BaseMindooDB {
|
|
|
214
216
|
return false;
|
|
215
217
|
}
|
|
216
218
|
const checkpoint = JSON.parse(new TextDecoder().decode(metaBytes));
|
|
217
|
-
if (checkpoint.version !==
|
|
219
|
+
if (checkpoint.version !== 2) {
|
|
218
220
|
this.logger.warn(`Unknown cache version ${checkpoint.version}, ignoring cache`);
|
|
219
221
|
return false;
|
|
220
222
|
}
|
|
@@ -266,6 +268,7 @@ class BaseMindooDB {
|
|
|
266
268
|
this.logger.warn(`Failed to restore cached doc ${docId}, will reload from store: ${e}`);
|
|
267
269
|
}
|
|
268
270
|
}
|
|
271
|
+
this.logger.info(`Restored ${restoredDocs} cached documents for ${this.store.getId()}`);
|
|
269
272
|
this.logger.info(`Restored ${restoredDocs} documents from cache for ${prefix}`);
|
|
270
273
|
return true;
|
|
271
274
|
}
|
|
@@ -460,11 +463,42 @@ class BaseMindooDB {
|
|
|
460
463
|
if (restored) {
|
|
461
464
|
this.logger.info("Restored from cache, processing delta only");
|
|
462
465
|
await this.syncStoreChanges();
|
|
466
|
+
if (this.reconcileRestoredIndexOnInit) {
|
|
467
|
+
await this.reconcileRestoredIndexWithStore();
|
|
468
|
+
}
|
|
463
469
|
return;
|
|
464
470
|
}
|
|
465
471
|
}
|
|
466
472
|
await this.syncStoreChanges();
|
|
467
473
|
}
|
|
474
|
+
async reconcileRestoredIndexWithStore() {
|
|
475
|
+
const lifecycleMetadata = await this.scanAllMetadata(this.store);
|
|
476
|
+
const lifecycleDocIds = Array.from(new Set(lifecycleMetadata
|
|
477
|
+
.filter((entry) => entry.entryType === "doc_create"
|
|
478
|
+
|| entry.entryType === "doc_change"
|
|
479
|
+
|| entry.entryType === "doc_delete"
|
|
480
|
+
|| entry.entryType === "doc_snapshot")
|
|
481
|
+
.map((entry) => entry.docId))).sort((left, right) => left.localeCompare(right));
|
|
482
|
+
const missingDocIds = lifecycleDocIds.filter((docId) => !this.indexLookup.has(docId));
|
|
483
|
+
if (missingDocIds.length === 0) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.logger.warn(`Detected stale cache checkpoint for ${this.store.getId()} - ` +
|
|
487
|
+
`index is missing ${missingDocIds.length} document(s): ${missingDocIds.join(", ")}. ` +
|
|
488
|
+
`Rebuilding metadata from local store.`);
|
|
489
|
+
this.index = [];
|
|
490
|
+
this.indexLookup.clear();
|
|
491
|
+
this.docCache.clear();
|
|
492
|
+
this.automergeHashToEntryId.clear();
|
|
493
|
+
this.processedEntryIds = [];
|
|
494
|
+
this.processedEntryCursor = null;
|
|
495
|
+
this.nextChangeSeq = 1;
|
|
496
|
+
this.cacheMetaDirty = true;
|
|
497
|
+
this.cacheManager?.markDirty();
|
|
498
|
+
await this.syncStoreChanges();
|
|
499
|
+
this.logger.info(`Metadata rebuild complete for ${this.store.getId()} - ` +
|
|
500
|
+
`index now has ${this.index.length} document(s).`);
|
|
501
|
+
}
|
|
468
502
|
/**
|
|
469
503
|
* Sync changes from the content-addressed store by finding new entries and processing them.
|
|
470
504
|
* This method can be called multiple times to incrementally sync new entries.
|
|
@@ -475,9 +509,10 @@ class BaseMindooDB {
|
|
|
475
509
|
this.logger.debug(`Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
476
510
|
this.logger.debug(`Already processed ${this.processedEntryIds.length} entry IDs`);
|
|
477
511
|
// Find new entries that we haven't processed yet
|
|
478
|
-
const newEntryMetadata = await this.getNewEntryMetadataForSync();
|
|
512
|
+
const { entries: newEntryMetadata, nextCursor } = await this.getNewEntryMetadataForSync();
|
|
479
513
|
this.logger.debug(`Found ${newEntryMetadata.length} new entries`);
|
|
480
514
|
if (newEntryMetadata.length === 0) {
|
|
515
|
+
this.processedEntryCursor = nextCursor;
|
|
481
516
|
this.logger.debug(`No new entries to process`);
|
|
482
517
|
this.performanceCallback?.onSyncOperation?.({
|
|
483
518
|
operation: "findNewEntries",
|
|
@@ -608,6 +643,7 @@ class BaseMindooDB {
|
|
|
608
643
|
}
|
|
609
644
|
// Append new entry IDs to our processed list
|
|
610
645
|
this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
|
|
646
|
+
this.processedEntryCursor = nextCursor;
|
|
611
647
|
this.cacheMetaDirty = true;
|
|
612
648
|
this.cacheManager?.markDirty();
|
|
613
649
|
this.logger.debug(`Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
|
|
@@ -656,7 +692,10 @@ class BaseMindooDB {
|
|
|
656
692
|
processedEntryCount: this.processedEntryIds.length,
|
|
657
693
|
},
|
|
658
694
|
});
|
|
659
|
-
return
|
|
695
|
+
return {
|
|
696
|
+
entries: result,
|
|
697
|
+
nextCursor: this.processedEntryCursor,
|
|
698
|
+
};
|
|
660
699
|
}
|
|
661
700
|
const allNew = [];
|
|
662
701
|
let cursor = this.processedEntryCursor;
|
|
@@ -668,7 +707,6 @@ class BaseMindooDB {
|
|
|
668
707
|
break;
|
|
669
708
|
}
|
|
670
709
|
}
|
|
671
|
-
this.processedEntryCursor = cursor;
|
|
672
710
|
this.performanceCallback?.onSyncOperation?.({
|
|
673
711
|
operation: "findNewEntries",
|
|
674
712
|
time: Date.now() - startedAt,
|
|
@@ -678,7 +716,10 @@ class BaseMindooDB {
|
|
|
678
716
|
cursor,
|
|
679
717
|
},
|
|
680
718
|
});
|
|
681
|
-
return
|
|
719
|
+
return {
|
|
720
|
+
entries: allNew,
|
|
721
|
+
nextCursor: cursor,
|
|
722
|
+
};
|
|
682
723
|
}
|
|
683
724
|
/**
|
|
684
725
|
* Fetch the target store's bloom filter summary, returning null if
|
|
@@ -746,7 +787,7 @@ class BaseMindooDB {
|
|
|
746
787
|
let currentPage = 0;
|
|
747
788
|
while (true) {
|
|
748
789
|
if (options?.signal?.aborted) {
|
|
749
|
-
return { transferred, cancelled: true };
|
|
790
|
+
return { transferred, scanned, cancelled: true };
|
|
750
791
|
}
|
|
751
792
|
const page = await sourceStore.scanEntriesSince(cursor, pageSize);
|
|
752
793
|
currentPage++;
|
|
@@ -773,7 +814,7 @@ class BaseMindooDB {
|
|
|
773
814
|
break;
|
|
774
815
|
}
|
|
775
816
|
}
|
|
776
|
-
return { transferred, cancelled: false };
|
|
817
|
+
return { transferred, scanned, cancelled: false };
|
|
777
818
|
}
|
|
778
819
|
onProgress?.({
|
|
779
820
|
phase: 'preparing',
|
|
@@ -784,10 +825,10 @@ class BaseMindooDB {
|
|
|
784
825
|
const targetIds = await targetStore.getAllIds();
|
|
785
826
|
const sourceNewMetadata = await sourceStore.findNewEntries(targetIds);
|
|
786
827
|
if (sourceNewMetadata.length === 0) {
|
|
787
|
-
return { transferred: 0, cancelled: false };
|
|
828
|
+
return { transferred: 0, scanned: 0, cancelled: false };
|
|
788
829
|
}
|
|
789
830
|
if (options?.signal?.aborted) {
|
|
790
|
-
return { transferred: 0, cancelled: true };
|
|
831
|
+
return { transferred: 0, scanned: 0, cancelled: true };
|
|
791
832
|
}
|
|
792
833
|
onProgress?.({
|
|
793
834
|
phase: 'transferring',
|
|
@@ -805,7 +846,11 @@ class BaseMindooDB {
|
|
|
805
846
|
scannedEntries: sourceNewMetadata.length,
|
|
806
847
|
totalSourceEntries: sourceNewMetadata.length,
|
|
807
848
|
});
|
|
808
|
-
return {
|
|
849
|
+
return {
|
|
850
|
+
transferred: sourceEntries.length,
|
|
851
|
+
scanned: sourceNewMetadata.length,
|
|
852
|
+
cancelled: false,
|
|
853
|
+
};
|
|
809
854
|
}
|
|
810
855
|
/**
|
|
811
856
|
* Dense sync: transfer only the entries required to reconstruct the latest
|
|
@@ -832,7 +877,7 @@ class BaseMindooDB {
|
|
|
832
877
|
scannedEntries: 0,
|
|
833
878
|
});
|
|
834
879
|
if (options?.signal?.aborted) {
|
|
835
|
-
return { transferred: 0, cancelled: true };
|
|
880
|
+
return { transferred: 0, scanned: 0, cancelled: true };
|
|
836
881
|
}
|
|
837
882
|
// ── Phase 1: discover documents ──────────────────────────────────
|
|
838
883
|
const docCreateEntries = await sourceStore.findEntries("doc_create", null, null);
|
|
@@ -840,10 +885,10 @@ class BaseMindooDB {
|
|
|
840
885
|
const docIds = [...new Set(docCreateEntries.map((e) => e.docId))];
|
|
841
886
|
this.logger.info(`Dense sync: found ${docIds.length} documents on source`);
|
|
842
887
|
if (docIds.length === 0) {
|
|
843
|
-
return { transferred: 0, cancelled: false };
|
|
888
|
+
return { transferred: 0, scanned: 0, cancelled: false };
|
|
844
889
|
}
|
|
845
890
|
if (options?.signal?.aborted) {
|
|
846
|
-
return { transferred: 0, cancelled: true };
|
|
891
|
+
return { transferred: 0, scanned: 0, cancelled: true };
|
|
847
892
|
}
|
|
848
893
|
// ── Phase 2: batch plan on source ────────────────────────────────
|
|
849
894
|
onProgress?.({
|
|
@@ -872,7 +917,7 @@ class BaseMindooDB {
|
|
|
872
917
|
this.logger.info(`Dense sync: planner identified ${neededIds.size} required entries ` +
|
|
873
918
|
`(from ${docIds.length} documents)`);
|
|
874
919
|
if (options?.signal?.aborted) {
|
|
875
|
-
return { transferred: 0, cancelled: true };
|
|
920
|
+
return { transferred: 0, scanned: neededIds.size, cancelled: true };
|
|
876
921
|
}
|
|
877
922
|
// ── Phase 4: filter out entries the target already has ───────────
|
|
878
923
|
const allNeededArray = Array.from(neededIds);
|
|
@@ -881,17 +926,17 @@ class BaseMindooDB {
|
|
|
881
926
|
this.logger.info(`Dense sync: ${missingIds.length} entries to transfer ` +
|
|
882
927
|
`(${allNeededArray.length - missingIds.length} already present)`);
|
|
883
928
|
if (missingIds.length === 0) {
|
|
884
|
-
return { transferred: 0, cancelled: false };
|
|
929
|
+
return { transferred: 0, scanned: allNeededArray.length, cancelled: false };
|
|
885
930
|
}
|
|
886
931
|
if (options?.signal?.aborted) {
|
|
887
|
-
return { transferred: 0, cancelled: true };
|
|
932
|
+
return { transferred: 0, scanned: allNeededArray.length, cancelled: true };
|
|
888
933
|
}
|
|
889
934
|
// ── Phase 5: transfer missing entries in pages ───────────────────
|
|
890
935
|
const pageSize = options?.pageSize ?? 500;
|
|
891
936
|
let transferred = 0;
|
|
892
937
|
for (let offset = 0; offset < missingIds.length; offset += pageSize) {
|
|
893
938
|
if (options?.signal?.aborted) {
|
|
894
|
-
return { transferred, cancelled: true };
|
|
939
|
+
return { transferred, scanned: allNeededArray.length, cancelled: true };
|
|
895
940
|
}
|
|
896
941
|
const batch = missingIds.slice(offset, offset + pageSize);
|
|
897
942
|
const entries = await sourceStore.getEntries(batch);
|
|
@@ -906,7 +951,7 @@ class BaseMindooDB {
|
|
|
906
951
|
});
|
|
907
952
|
}
|
|
908
953
|
this.logger.info(`Dense sync complete: transferred ${transferred} entries`);
|
|
909
|
-
return { transferred, cancelled: false };
|
|
954
|
+
return { transferred, scanned: allNeededArray.length, cancelled: false };
|
|
910
955
|
}
|
|
911
956
|
getStore() {
|
|
912
957
|
return this.store;
|
|
@@ -1067,9 +1112,15 @@ class BaseMindooDB {
|
|
|
1067
1112
|
this.logger.debug(`Getting document ${docId} at timestamp ${timestamp}`);
|
|
1068
1113
|
// Get all entry metadata for this document
|
|
1069
1114
|
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
1070
|
-
// Filter
|
|
1115
|
+
// Filter to document replay metadata plus snapshots up to the timestamp.
|
|
1116
|
+
// Attachment chunks are not part of the Automerge replay DAG and must not
|
|
1117
|
+
// participate in historical materialization.
|
|
1071
1118
|
const relevantEntries = allEntryMetadata
|
|
1072
|
-
.filter(em => em.createdAt <= timestamp
|
|
1119
|
+
.filter((em) => em.createdAt <= timestamp
|
|
1120
|
+
&& (em.entryType === "doc_create"
|
|
1121
|
+
|| em.entryType === "doc_change"
|
|
1122
|
+
|| em.entryType === "doc_delete"
|
|
1123
|
+
|| em.entryType === "doc_snapshot"))
|
|
1073
1124
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
1074
1125
|
if (relevantEntries.length === 0) {
|
|
1075
1126
|
this.performanceCallback?.onHistoryOperation?.({
|
|
@@ -1081,43 +1132,85 @@ class BaseMindooDB {
|
|
|
1081
1132
|
});
|
|
1082
1133
|
return null; // Document didn't exist at that time
|
|
1083
1134
|
}
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1135
|
+
const metadataById = new Map(relevantEntries.map((meta) => [meta.id, meta]));
|
|
1136
|
+
const materializationPlan = (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, relevantEntries);
|
|
1137
|
+
let startFromSnapshot = materializationPlan.snapshotEntryId !== null;
|
|
1138
|
+
const snapshotMeta = materializationPlan.snapshotEntryId
|
|
1139
|
+
? (metadataById.get(materializationPlan.snapshotEntryId) || null)
|
|
1140
|
+
: null;
|
|
1141
|
+
if (startFromSnapshot && !snapshotMeta) {
|
|
1142
|
+
this.logger.warn(`Planner referenced snapshot ${materializationPlan.snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
|
|
1143
|
+
startFromSnapshot = false;
|
|
1144
|
+
}
|
|
1145
|
+
let doc = undefined;
|
|
1146
|
+
if (startFromSnapshot && snapshotMeta) {
|
|
1147
|
+
const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
|
|
1148
|
+
if (snapshotEntries.length > 0) {
|
|
1149
|
+
const snapshotData = snapshotEntries[0];
|
|
1150
|
+
let isValid = false;
|
|
1151
|
+
if (this._isAdminOnlyDb && snapshotData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1152
|
+
this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
|
|
1156
|
+
}
|
|
1157
|
+
if (!isValid) {
|
|
1158
|
+
this.logger.warn(`Invalid signature for snapshot ${snapshotData.id}, falling back to replay without snapshot`);
|
|
1159
|
+
startFromSnapshot = false;
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
|
|
1163
|
+
doc = automerge_adapter_1.Automerge.load(decryptedSnapshot);
|
|
1164
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
|
|
1165
|
+
if (parsed) {
|
|
1166
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, snapshotData.id);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (!doc) {
|
|
1172
|
+
doc = automerge_adapter_1.Automerge.init();
|
|
1173
|
+
}
|
|
1174
|
+
const entriesToApply = materializationPlan.entryIdsToApply
|
|
1175
|
+
.map((id) => metadataById.get(id))
|
|
1176
|
+
.filter((entry) => entry !== undefined);
|
|
1177
|
+
const loadedEntries = entriesToApply.length > 0
|
|
1178
|
+
? await this.store.getEntries(entriesToApply.map((entry) => entry.id))
|
|
1179
|
+
: [];
|
|
1180
|
+
const entryById = new Map(loadedEntries.map((entry) => [entry.id, entry]));
|
|
1181
|
+
for (const entryMeta of entriesToApply) {
|
|
1182
|
+
const entryData = entryById.get(entryMeta.id);
|
|
1183
|
+
if (!entryData) {
|
|
1184
|
+
this.logger.warn(`Entry ${entryMeta.id} not found in store, skipping`);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1090
1187
|
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1091
1188
|
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
1092
1189
|
continue;
|
|
1093
1190
|
}
|
|
1094
|
-
// Verify signature against the encrypted payload (no decryption needed)
|
|
1095
|
-
// We sign the encrypted payload, so anyone can verify signatures without decryption keys
|
|
1096
1191
|
const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
|
|
1097
1192
|
if (!isValid) {
|
|
1098
1193
|
this.logger.warn(`Invalid signature for entry ${entryData.id}, skipping`);
|
|
1099
1194
|
continue;
|
|
1100
1195
|
}
|
|
1101
|
-
// Decrypt payload (only after signature verification passes)
|
|
1102
1196
|
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
1103
|
-
// Apply change using loadIncremental - this is the recommended way for binary change data
|
|
1104
1197
|
doc = automerge_adapter_1.Automerge.loadIncremental(doc, decryptedPayload);
|
|
1105
|
-
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
1106
1198
|
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
1107
1199
|
if (parsed) {
|
|
1108
1200
|
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
1109
1201
|
}
|
|
1110
1202
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1203
|
+
const replayEntries = relevantEntries.filter((entry) => entry.entryType === "doc_create"
|
|
1204
|
+
|| entry.entryType === "doc_change"
|
|
1205
|
+
|| entry.entryType === "doc_delete");
|
|
1206
|
+
const firstReplayEntry = replayEntries.length > 0 ? replayEntries[0] : null;
|
|
1207
|
+
const lastReplayEntry = replayEntries.length > 0 ? replayEntries[replayEntries.length - 1] : null;
|
|
1208
|
+
const createdAt = firstReplayEntry?.createdAt ?? snapshotMeta?.createdAt ?? timestamp;
|
|
1209
|
+
const decryptionKeyId = firstReplayEntry?.decryptionKeyId
|
|
1210
|
+
?? snapshotMeta?.decryptionKeyId
|
|
1211
|
+
?? "default";
|
|
1212
|
+
const isDeleted = lastReplayEntry?.entryType === "doc_delete";
|
|
1213
|
+
const lastModified = lastReplayEntry?.createdAt ?? snapshotMeta?.createdAt ?? timestamp;
|
|
1121
1214
|
const internalDoc = {
|
|
1122
1215
|
id: docId,
|
|
1123
1216
|
doc,
|
|
@@ -1287,6 +1380,15 @@ class BaseMindooDB {
|
|
|
1287
1380
|
}
|
|
1288
1381
|
return docIds;
|
|
1289
1382
|
}
|
|
1383
|
+
async getDeletedDocumentIds() {
|
|
1384
|
+
const docIds = [];
|
|
1385
|
+
for (const entry of this.index) {
|
|
1386
|
+
if (entry.isDeleted) {
|
|
1387
|
+
docIds.push(entry.docId);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return docIds;
|
|
1391
|
+
}
|
|
1290
1392
|
async getAllDocumentIdsAtTimestamp(timestamp) {
|
|
1291
1393
|
// findEntries() uses an exclusive upper bound (createdAt < creationDateUntil).
|
|
1292
1394
|
// Query up to timestamp + 1 so entries created exactly at `timestamp` are included
|
|
@@ -2012,10 +2114,10 @@ class BaseMindooDB {
|
|
|
2012
2114
|
this.logger.debug(`No change entries to apply for document ${docId}`);
|
|
2013
2115
|
return null; // No changes
|
|
2014
2116
|
}
|
|
2015
|
-
|
|
2016
|
-
entriesToApply.
|
|
2117
|
+
const entriesById = new Map(entriesToApply.map((entry) => [entry.id, entry]));
|
|
2118
|
+
const orderedEntryIds = (0, MaterializationPlanner_1.topologicalByDependencies)(new Set(entriesToApply.map((entry) => entry.id)), entriesById);
|
|
2017
2119
|
// Load entries from store
|
|
2018
|
-
const entries = await this.store.getEntries(
|
|
2120
|
+
const entries = await this.store.getEntries(orderedEntryIds);
|
|
2019
2121
|
// Filter entries for admin-only mode first
|
|
2020
2122
|
const validEntries = entries.filter(entryData => {
|
|
2021
2123
|
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
@@ -2103,9 +2205,9 @@ class BaseMindooDB {
|
|
|
2103
2205
|
this.logger.debug(`Document ${docId} changed: heads before=${JSON.stringify(headsBefore)}, after=${JSON.stringify(headsAfter)}`);
|
|
2104
2206
|
// Update metadata
|
|
2105
2207
|
const payload = updatedDoc;
|
|
2106
|
-
const
|
|
2208
|
+
const lastEntryCreatedAt = entries.reduce((maxCreatedAt, entry) => Math.max(maxCreatedAt, entry.createdAt), cachedDoc.lastModified);
|
|
2107
2209
|
const lastModified = payload._lastModified ||
|
|
2108
|
-
|
|
2210
|
+
lastEntryCreatedAt;
|
|
2109
2211
|
// Check if document was deleted
|
|
2110
2212
|
const hasDeleteEntry = newEntryMetadata.some(em => em.entryType === "doc_delete");
|
|
2111
2213
|
const isDeleted = hasDeleteEntry || cachedDoc.isDeleted;
|
|
@@ -2556,6 +2658,39 @@ class BaseMindooDB {
|
|
|
2556
2658
|
}
|
|
2557
2659
|
return ref;
|
|
2558
2660
|
}
|
|
2661
|
+
async planAttachmentRead(store, ref, startByte, endByteExclusive) {
|
|
2662
|
+
if (store.planAttachmentReadByWalkingMetadata) {
|
|
2663
|
+
try {
|
|
2664
|
+
// if the store has a planAttachmentReadByWalkingMetadata method, use it for less overhead through the network
|
|
2665
|
+
return await store.planAttachmentReadByWalkingMetadata(ref.lastChunkId, ref.size, {
|
|
2666
|
+
startByte,
|
|
2667
|
+
endByteExclusive,
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
catch (error) {
|
|
2671
|
+
this.logger.debug("Store-level attachment read planner failed, falling back to local metadata walk", { attachmentId: ref.attachmentId, error });
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
// fall back to local metadata walk
|
|
2675
|
+
return (0, AttachmentReadPlanner_1.planAttachmentReadByWalkingMetadata)(store, ref.lastChunkId, ref.size, {
|
|
2676
|
+
startByte,
|
|
2677
|
+
endByteExclusive,
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
async decryptAttachmentChunk(chunk) {
|
|
2681
|
+
if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
2682
|
+
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
2683
|
+
}
|
|
2684
|
+
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
2685
|
+
if (!isValid) {
|
|
2686
|
+
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
2687
|
+
}
|
|
2688
|
+
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
2689
|
+
if (plaintext.length !== chunk.originalSize) {
|
|
2690
|
+
throw new Error(`Attachment chunk ${chunk.id} decrypted to ${plaintext.length} bytes, expected ${chunk.originalSize}`);
|
|
2691
|
+
}
|
|
2692
|
+
return plaintext;
|
|
2693
|
+
}
|
|
2559
2694
|
/**
|
|
2560
2695
|
* Internal method to fetch and concatenate all chunks for an attachment.
|
|
2561
2696
|
*/
|
|
@@ -2572,17 +2707,7 @@ class BaseMindooDB {
|
|
|
2572
2707
|
const plaintextChunks = [];
|
|
2573
2708
|
let totalSize = 0;
|
|
2574
2709
|
for (const chunk of chunks) {
|
|
2575
|
-
|
|
2576
|
-
if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
2577
|
-
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
2578
|
-
}
|
|
2579
|
-
// Verify signature
|
|
2580
|
-
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
2581
|
-
if (!isValid) {
|
|
2582
|
-
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
2583
|
-
}
|
|
2584
|
-
// Decrypt
|
|
2585
|
-
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
2710
|
+
const plaintext = await this.decryptAttachmentChunk(chunk);
|
|
2586
2711
|
plaintextChunks.push(plaintext);
|
|
2587
2712
|
totalSize += plaintext.length;
|
|
2588
2713
|
}
|
|
@@ -2605,37 +2730,16 @@ class BaseMindooDB {
|
|
|
2605
2730
|
throw new Error(`Invalid byte range: [${startByte}, ${endByte})`);
|
|
2606
2731
|
}
|
|
2607
2732
|
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
2608
|
-
if (endByte > ref.size) {
|
|
2609
|
-
throw new Error(`End byte ${endByte} exceeds attachment size ${ref.size}`);
|
|
2610
|
-
}
|
|
2611
2733
|
const store = this.getEffectiveAttachmentStore();
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2614
|
-
const startChunkIndex = Math.floor(startByte / chunkSize);
|
|
2615
|
-
const endChunkIndex = Math.floor((endByte - 1) / chunkSize);
|
|
2616
|
-
// Resolve dependency chain to get all chunk IDs
|
|
2617
|
-
const allChunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
|
|
2618
|
-
// Get only the chunks we need
|
|
2619
|
-
const neededChunkIds = allChunkIds.slice(startChunkIndex, endChunkIndex + 1);
|
|
2734
|
+
const readPlan = await this.planAttachmentRead(store, ref, startByte, endByte);
|
|
2735
|
+
const neededChunkIds = readPlan.chunkPlans.map((chunkPlan) => chunkPlan.id);
|
|
2620
2736
|
const chunks = await store.getEntries(neededChunkIds);
|
|
2621
2737
|
// Decrypt needed chunks
|
|
2622
2738
|
const plaintextChunks = [];
|
|
2623
2739
|
for (const chunk of chunks) {
|
|
2624
|
-
|
|
2625
|
-
if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
2626
|
-
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
2627
|
-
}
|
|
2628
|
-
// Verify signature
|
|
2629
|
-
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
2630
|
-
if (!isValid) {
|
|
2631
|
-
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
2632
|
-
}
|
|
2633
|
-
// Decrypt
|
|
2634
|
-
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
2740
|
+
const plaintext = await this.decryptAttachmentChunk(chunk);
|
|
2635
2741
|
plaintextChunks.push(plaintext);
|
|
2636
2742
|
}
|
|
2637
|
-
// Calculate offsets within the fetched chunks
|
|
2638
|
-
const offsetInFirstChunk = startByte - (startChunkIndex * chunkSize);
|
|
2639
2743
|
const totalNeededBytes = endByte - startByte;
|
|
2640
2744
|
// Extract the requested range
|
|
2641
2745
|
const result = new Uint8Array(totalNeededBytes);
|
|
@@ -2643,7 +2747,7 @@ class BaseMindooDB {
|
|
|
2643
2747
|
let bytesRemaining = totalNeededBytes;
|
|
2644
2748
|
for (let i = 0; i < plaintextChunks.length && bytesRemaining > 0; i++) {
|
|
2645
2749
|
const chunk = plaintextChunks[i];
|
|
2646
|
-
const chunkStart = i === 0 ? offsetInFirstChunk : 0;
|
|
2750
|
+
const chunkStart = i === 0 ? readPlan.offsetInFirstChunk : 0;
|
|
2647
2751
|
const bytesToCopy = Math.min(chunk.length - chunkStart, bytesRemaining);
|
|
2648
2752
|
result.set(chunk.slice(chunkStart, chunkStart + bytesToCopy), resultOffset);
|
|
2649
2753
|
resultOffset += bytesToCopy;
|
|
@@ -2658,30 +2762,25 @@ class BaseMindooDB {
|
|
|
2658
2762
|
async *streamAttachmentInternal(docId, attachmentId, startOffset) {
|
|
2659
2763
|
this.logger.debug(`Streaming attachment ${attachmentId} from offset ${startOffset}`);
|
|
2660
2764
|
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
2765
|
+
if (startOffset < 0 || startOffset > ref.size) {
|
|
2766
|
+
throw new Error(`Invalid stream offset ${startOffset} for attachment size ${ref.size}`);
|
|
2767
|
+
}
|
|
2768
|
+
if (ref.size === 0 || startOffset === ref.size) {
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2661
2771
|
const store = this.getEffectiveAttachmentStore();
|
|
2662
|
-
const
|
|
2663
|
-
// Calculate starting chunk
|
|
2664
|
-
const startChunkIndex = Math.floor(startOffset / chunkSize);
|
|
2665
|
-
const offsetInStartChunk = startOffset % chunkSize;
|
|
2666
|
-
// Resolve dependency chain to get all chunk IDs
|
|
2667
|
-
const allChunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
|
|
2772
|
+
const readPlan = await this.planAttachmentRead(store, ref, startOffset, ref.size);
|
|
2668
2773
|
// Stream chunks starting from startChunkIndex
|
|
2669
|
-
for (let i =
|
|
2670
|
-
const
|
|
2671
|
-
|
|
2672
|
-
if (
|
|
2673
|
-
throw new Error(`
|
|
2674
|
-
}
|
|
2675
|
-
// Verify signature
|
|
2676
|
-
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
2677
|
-
if (!isValid) {
|
|
2678
|
-
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
2774
|
+
for (let i = 0; i < readPlan.chunkPlans.length; i++) {
|
|
2775
|
+
const chunkPlan = readPlan.chunkPlans[i];
|
|
2776
|
+
const [chunk] = await store.getEntries([chunkPlan.id]);
|
|
2777
|
+
if (!chunk) {
|
|
2778
|
+
throw new Error(`Attachment chunk ${chunkPlan.id} not found in store`);
|
|
2679
2779
|
}
|
|
2680
|
-
|
|
2681
|
-
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
2780
|
+
const plaintext = await this.decryptAttachmentChunk(chunk);
|
|
2682
2781
|
// For first chunk, skip bytes before startOffset
|
|
2683
|
-
if (i ===
|
|
2684
|
-
yield plaintext.slice(
|
|
2782
|
+
if (i === 0 && readPlan.offsetInFirstChunk > 0) {
|
|
2783
|
+
yield plaintext.slice(readPlan.offsetInFirstChunk);
|
|
2685
2784
|
}
|
|
2686
2785
|
else {
|
|
2687
2786
|
yield plaintext;
|
|
@@ -2966,11 +3065,15 @@ class BaseMindooDB {
|
|
|
2966
3065
|
this.logger.debug(`Transferred ${syncResult.transferred} entries from remote store`);
|
|
2967
3066
|
if (syncResult.cancelled) {
|
|
2968
3067
|
this.logger.info(`Pull cancelled after transferring ${syncResult.transferred} entries`);
|
|
2969
|
-
return {
|
|
3068
|
+
return {
|
|
3069
|
+
transferredEntries: syncResult.transferred,
|
|
3070
|
+
scannedEntries: syncResult.scanned,
|
|
3071
|
+
cancelled: true,
|
|
3072
|
+
};
|
|
2970
3073
|
}
|
|
2971
3074
|
if (syncResult.transferred === 0) {
|
|
2972
3075
|
this.logger.debug(`No new entries to pull`);
|
|
2973
|
-
return { transferredEntries: 0, cancelled: false };
|
|
3076
|
+
return { transferredEntries: 0, scannedEntries: syncResult.scanned, cancelled: false };
|
|
2974
3077
|
}
|
|
2975
3078
|
options?.onProgress?.({
|
|
2976
3079
|
phase: 'processing',
|
|
@@ -2988,7 +3091,11 @@ class BaseMindooDB {
|
|
|
2988
3091
|
transferredEntries: syncResult.transferred,
|
|
2989
3092
|
scannedEntries: syncResult.transferred,
|
|
2990
3093
|
});
|
|
2991
|
-
return {
|
|
3094
|
+
return {
|
|
3095
|
+
transferredEntries: syncResult.transferred,
|
|
3096
|
+
scannedEntries: syncResult.scanned,
|
|
3097
|
+
cancelled: false,
|
|
3098
|
+
};
|
|
2992
3099
|
}
|
|
2993
3100
|
finally {
|
|
2994
3101
|
restoreAuthOverride();
|
|
@@ -3018,7 +3125,11 @@ class BaseMindooDB {
|
|
|
3018
3125
|
this.logger.debug(`Transferred ${syncResult.transferred} entries to remote store`);
|
|
3019
3126
|
if (syncResult.cancelled) {
|
|
3020
3127
|
this.logger.info(`Push cancelled after transferring ${syncResult.transferred} entries`);
|
|
3021
|
-
return {
|
|
3128
|
+
return {
|
|
3129
|
+
transferredEntries: syncResult.transferred,
|
|
3130
|
+
scannedEntries: syncResult.scanned,
|
|
3131
|
+
cancelled: true,
|
|
3132
|
+
};
|
|
3022
3133
|
}
|
|
3023
3134
|
if (syncResult.transferred === 0) {
|
|
3024
3135
|
this.logger.debug(`No new entries to push`);
|
|
@@ -3032,7 +3143,11 @@ class BaseMindooDB {
|
|
|
3032
3143
|
transferredEntries: syncResult.transferred,
|
|
3033
3144
|
scannedEntries: syncResult.transferred,
|
|
3034
3145
|
});
|
|
3035
|
-
return {
|
|
3146
|
+
return {
|
|
3147
|
+
transferredEntries: syncResult.transferred,
|
|
3148
|
+
scannedEntries: syncResult.scanned,
|
|
3149
|
+
cancelled: false,
|
|
3150
|
+
};
|
|
3036
3151
|
}
|
|
3037
3152
|
finally {
|
|
3038
3153
|
restoreAuthOverride();
|