mindoodb 0.0.1 → 0.0.3
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.md +97 -24
- package/dist/__mocks__/expo-standard-web-crypto.d.ts +10 -0
- package/dist/__mocks__/expo-standard-web-crypto.d.ts.map +1 -0
- package/dist/__mocks__/expo-standard-web-crypto.js +22 -0
- package/dist/__mocks__/expo-standard-web-crypto.js.map +1 -0
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.d.ts +8 -0
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.d.ts.map +1 -0
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.js +16 -0
- package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.js.map +1 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts +1 -1
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -1
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +2 -2
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +19 -2
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +133 -22
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +20 -3
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +136 -21
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +33 -3
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +148 -27
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/index.d.ts +1 -1
- package/dist/appendonlystores/network/index.d.ts.map +1 -1
- package/dist/appendonlystores/network/index.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +61 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +685 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts +25 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js +41 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js.map +1 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +13 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/core/BaseMindooDB.d.ts +38 -4
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +810 -329
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +42 -6
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +259 -93
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +3 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +50 -40
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +25 -29
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +175 -73
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +68 -0
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js +659 -0
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +50 -4
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +204 -39
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/bloom.d.ts +85 -0
- package/dist/core/appendonlystores/bloom.d.ts.map +1 -0
- package/dist/core/appendonlystores/bloom.js +228 -0
- package/dist/core/appendonlystores/bloom.js.map +1 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +3 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +21 -17
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +36 -2
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +10 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +137 -1
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/automerge-adapter.d.ts +15 -0
- package/dist/core/automerge-adapter.d.ts.map +1 -0
- package/dist/core/automerge-adapter.js +73 -0
- package/dist/core/automerge-adapter.js.map +1 -0
- package/dist/core/crypto/MindooDocSigner.d.ts +3 -1
- package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
- package/dist/core/crypto/MindooDocSigner.js +9 -5
- package/dist/core/crypto/MindooDocSigner.js.map +1 -1
- package/dist/core/crypto/RSAEncryption.d.ts +3 -1
- package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
- package/dist/core/crypto/RSAEncryption.js +13 -9
- package/dist/core/crypto/RSAEncryption.js.map +1 -1
- package/dist/core/crypto/pbkdf2Iterations.d.ts +12 -0
- package/dist/core/crypto/pbkdf2Iterations.d.ts.map +1 -0
- package/dist/core/crypto/pbkdf2Iterations.js +55 -0
- package/dist/core/crypto/pbkdf2Iterations.js.map +1 -0
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +30 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +2 -2
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
- package/dist/core/indexing/virtualviews/index.d.ts +1 -1
- package/dist/core/indexing/virtualviews/index.js +1 -1
- package/dist/core/keys/KeyBag.d.ts +36 -13
- package/dist/core/keys/KeyBag.d.ts.map +1 -1
- package/dist/core/keys/KeyBag.js +96 -31
- package/dist/core/keys/KeyBag.js.map +1 -1
- package/dist/core/keys/KeyContext.d.ts +4 -0
- package/dist/core/keys/KeyContext.d.ts.map +1 -0
- package/dist/core/keys/KeyContext.js +11 -0
- package/dist/core/keys/KeyContext.js.map +1 -0
- package/dist/core/logging/LogLevel.d.ts +21 -0
- package/dist/core/logging/LogLevel.d.ts.map +1 -0
- package/dist/core/logging/LogLevel.js +56 -0
- package/dist/core/logging/LogLevel.js.map +1 -0
- package/dist/core/logging/Logger.d.ts +37 -0
- package/dist/core/logging/Logger.d.ts.map +1 -0
- package/dist/core/logging/Logger.js +3 -0
- package/dist/core/logging/Logger.js.map +1 -0
- package/dist/core/logging/MindooLogger.d.ts +28 -0
- package/dist/core/logging/MindooLogger.d.ts.map +1 -0
- package/dist/core/logging/MindooLogger.js +217 -0
- package/dist/core/logging/MindooLogger.js.map +1 -0
- package/dist/core/logging/index.d.ts +4 -0
- package/dist/core/logging/index.d.ts.map +1 -0
- package/dist/core/logging/index.js +10 -0
- package/dist/core/logging/index.js.map +1 -0
- package/dist/core/types.d.ts +313 -77
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/MindooURI.d.ts +58 -0
- package/dist/core/uri/MindooURI.d.ts.map +1 -0
- package/dist/core/uri/MindooURI.js +169 -0
- package/dist/core/uri/MindooURI.js.map +1 -0
- package/dist/core/userid/index.d.ts +0 -10
- package/dist/core/userid/index.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +541 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +1334 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +5 -1
- package/dist/node/index.js.map +1 -1
- package/dist/reactnative/crypto/AESAdapter.d.ts +18 -0
- package/dist/reactnative/crypto/AESAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/AESAdapter.js +112 -0
- package/dist/reactnative/crypto/AESAdapter.js.map +1 -0
- package/dist/reactnative/crypto/Ed25519Adapter.d.ts +34 -0
- package/dist/reactnative/crypto/Ed25519Adapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/Ed25519Adapter.js +386 -0
- package/dist/reactnative/crypto/Ed25519Adapter.js.map +1 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.d.ts +23 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.js +51 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.js.map +1 -0
- package/dist/reactnative/crypto/HMACAdapter.d.ts +13 -0
- package/dist/reactnative/crypto/HMACAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/HMACAdapter.js +50 -0
- package/dist/reactnative/crypto/HMACAdapter.js.map +1 -0
- package/dist/reactnative/crypto/KeyFormatConverter.d.ts +21 -0
- package/dist/reactnative/crypto/KeyFormatConverter.d.ts.map +1 -0
- package/dist/reactnative/crypto/KeyFormatConverter.js +72 -0
- package/dist/reactnative/crypto/KeyFormatConverter.js.map +1 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.d.ts +19 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.js +67 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.js.map +1 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.d.ts +37 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.js +72 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.js.map +1 -0
- package/dist/reactnative/crypto/RSAAdapter.d.ts +38 -0
- package/dist/reactnative/crypto/RSAAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/RSAAdapter.js +184 -0
- package/dist/reactnative/crypto/RSAAdapter.js.map +1 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.d.ts +33 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.js +61 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.js.map +1 -0
- package/dist/reactnative/crypto/SHA256Adapter.d.ts +9 -0
- package/dist/reactnative/crypto/SHA256Adapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/SHA256Adapter.js +29 -0
- package/dist/reactnative/crypto/SHA256Adapter.js.map +1 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.d.ts +63 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.d.ts.map +1 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.js +350 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.js.map +1 -0
- package/dist/reactnative/index.d.ts +8 -0
- package/dist/reactnative/index.d.ts.map +1 -0
- package/dist/reactnative/index.js +38 -0
- package/dist/reactnative/index.js.map +1 -0
- package/package.json +51 -6
- package/patches/PATCHES.md +111 -0
- package/patches/react-native+0.76.9.patch +34 -0
- package/patches/react-native-quick-crypto+1.0.7.patch +42 -0
- package/scripts/cli.js +21 -0
- package/scripts/setup-react-native.js +228 -0
- package/templates/reactnative/App.js +252 -0
- package/templates/reactnative/app.json +29 -0
- package/templates/reactnative/babel.config.js +6 -0
- package/templates/reactnative/index.js +26 -0
- package/templates/reactnative/metro.config.snippet.js +39 -0
- package/templates/reactnative/mindoodb-polyfills.js +116 -0
|
@@ -1,43 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.BaseMindooDB = void 0;
|
|
37
|
-
|
|
4
|
+
// Import platform-appropriate Automerge implementation
|
|
5
|
+
// React Native: native Rust (react-native-automerge-generated)
|
|
6
|
+
// Browser/Node.js: WASM (@automerge/automerge/slim)
|
|
7
|
+
const automerge_adapter_1 = require("./automerge-adapter");
|
|
38
8
|
const uuid_1 = require("uuid");
|
|
9
|
+
const bloom_1 = require("./appendonlystores/bloom");
|
|
39
10
|
const idGeneration_1 = require("./utils/idGeneration");
|
|
40
11
|
const errors_1 = require("./errors");
|
|
12
|
+
const logging_1 = require("./logging");
|
|
41
13
|
/**
|
|
42
14
|
* Default chunk size for attachments: 256KB
|
|
43
15
|
*/
|
|
@@ -62,7 +34,7 @@ const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
|
|
|
62
34
|
* - Automerge.applyChanges(doc, changes) - verify method signature
|
|
63
35
|
*/
|
|
64
36
|
class BaseMindooDB {
|
|
65
|
-
constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false) {
|
|
37
|
+
constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false, logger, performanceCallback) {
|
|
66
38
|
// Internal index: sorted array of document entries, maintained in order by (lastModified, docId)
|
|
67
39
|
// This allows efficient incremental processing without sorting on each call
|
|
68
40
|
this.index = [];
|
|
@@ -72,14 +44,23 @@ class BaseMindooDB {
|
|
|
72
44
|
this.docCache = new Map();
|
|
73
45
|
// Track which entry IDs we've already processed
|
|
74
46
|
this.processedEntryIds = [];
|
|
47
|
+
this.processedEntryCursor = null;
|
|
75
48
|
// Index: automergeHash -> entryId for each document
|
|
76
49
|
// Used for resolving Automerge dependency hashes to entry IDs
|
|
77
50
|
this.automergeHashToEntryId = new Map(); // Map<docId, Map<automergeHash, entryId>>
|
|
51
|
+
// Cache for imported public keys (CryptoKey objects) to avoid re-importing the same key
|
|
52
|
+
// Map<publicKeyPEM, CryptoKey>
|
|
53
|
+
this.publicKeyCache = new Map();
|
|
78
54
|
this.tenant = tenant;
|
|
79
55
|
this.store = store;
|
|
80
56
|
this.attachmentStore = attachmentStore;
|
|
81
57
|
this.chunkSizeBytes = attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
|
|
82
58
|
this._isAdminOnlyDb = adminOnlyDb;
|
|
59
|
+
// Create logger if not provided (for backward compatibility)
|
|
60
|
+
this.logger =
|
|
61
|
+
logger ||
|
|
62
|
+
new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), "BaseMindooDB", true);
|
|
63
|
+
this.performanceCallback = performanceCallback;
|
|
83
64
|
}
|
|
84
65
|
/**
|
|
85
66
|
* Get the admin public key from the tenant.
|
|
@@ -91,6 +72,44 @@ class BaseMindooDB {
|
|
|
91
72
|
isAdminOnlyDb() {
|
|
92
73
|
return this._isAdminOnlyDb;
|
|
93
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Get or import a CryptoKey for signature verification, with caching.
|
|
77
|
+
* This avoids re-importing the same public key multiple times.
|
|
78
|
+
*/
|
|
79
|
+
async getOrImportPublicKey(publicKey) {
|
|
80
|
+
// Check cache first
|
|
81
|
+
if (this.publicKeyCache.has(publicKey)) {
|
|
82
|
+
return this.publicKeyCache.get(publicKey);
|
|
83
|
+
}
|
|
84
|
+
// Validate the public key is trusted
|
|
85
|
+
const directory = await this.tenant.openDirectory();
|
|
86
|
+
const isTrusted = await directory.validatePublicSigningKey(publicKey);
|
|
87
|
+
if (!isTrusted) {
|
|
88
|
+
this.logger.warn(`Public key not trusted: ${publicKey}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Import the key
|
|
92
|
+
const subtle = this.tenant.getCryptoAdapter().getSubtle();
|
|
93
|
+
const publicKeyBuffer = this.tenant.pemToArrayBuffer(publicKey);
|
|
94
|
+
const cryptoKey = await subtle.importKey("spki", publicKeyBuffer, {
|
|
95
|
+
name: "Ed25519",
|
|
96
|
+
}, false, // not extractable
|
|
97
|
+
["verify"]);
|
|
98
|
+
// Cache the imported key
|
|
99
|
+
this.publicKeyCache.set(publicKey, cryptoKey);
|
|
100
|
+
return cryptoKey;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Verify a signature using a pre-imported CryptoKey.
|
|
104
|
+
* This bypasses the key import step for better performance when keys are cached.
|
|
105
|
+
*/
|
|
106
|
+
async verifySignatureWithKey(cryptoKey, payload, signature) {
|
|
107
|
+
const subtle = this.tenant.getCryptoAdapter().getSubtle();
|
|
108
|
+
const isValid = await subtle.verify({
|
|
109
|
+
name: "Ed25519",
|
|
110
|
+
}, cryptoKey, signature.buffer, payload.buffer);
|
|
111
|
+
return isValid;
|
|
112
|
+
}
|
|
94
113
|
/**
|
|
95
114
|
* Compare two index entries for sorting.
|
|
96
115
|
* Returns negative if a < b, positive if a > b, 0 if equal.
|
|
@@ -107,25 +126,33 @@ class BaseMindooDB {
|
|
|
107
126
|
* When a document changes, it's removed from its current position and inserted
|
|
108
127
|
* at the correct sorted position to maintain order by (lastModified, docId).
|
|
109
128
|
*
|
|
129
|
+
* Optimized to only update lookup map for entries that actually moved.
|
|
130
|
+
*
|
|
110
131
|
* @param docId The document ID
|
|
111
132
|
* @param lastModified The new last modified timestamp
|
|
112
133
|
* @param isDeleted Whether the document is deleted
|
|
113
134
|
*/
|
|
114
135
|
updateIndex(docId, lastModified, isDeleted) {
|
|
115
|
-
|
|
136
|
+
const newEntry = { docId, lastModified, isDeleted };
|
|
116
137
|
const existingIndex = this.indexLookup.get(docId);
|
|
138
|
+
// Check if the entry already exists and hasn't changed position
|
|
117
139
|
if (existingIndex !== undefined) {
|
|
118
|
-
|
|
140
|
+
const existingEntry = this.index[existingIndex];
|
|
141
|
+
// If position hasn't changed (same lastModified and docId), no update needed
|
|
142
|
+
if (existingEntry.lastModified === lastModified && existingEntry.isDeleted === isDeleted) {
|
|
143
|
+
return; // No change needed
|
|
144
|
+
}
|
|
145
|
+
// Remove from current position
|
|
119
146
|
this.index.splice(existingIndex, 1);
|
|
120
|
-
// Update lookup map for
|
|
121
|
-
|
|
147
|
+
// Update lookup map for entries that moved (only those after the removed position)
|
|
148
|
+
// We'll update the full range after insertion to be safe
|
|
149
|
+
const minAffectedIndex = Math.min(existingIndex, this.index.length);
|
|
150
|
+
for (let i = minAffectedIndex; i < this.index.length; i++) {
|
|
122
151
|
this.indexLookup.set(this.index[i].docId, i);
|
|
123
152
|
}
|
|
124
|
-
// Remove the entry from lookup
|
|
125
153
|
this.indexLookup.delete(docId);
|
|
126
154
|
}
|
|
127
155
|
// Find insertion point using binary search to maintain sorted order
|
|
128
|
-
const newEntry = { docId, lastModified, isDeleted };
|
|
129
156
|
let insertIndex = this.index.length; // Default to end
|
|
130
157
|
// Binary search for insertion point
|
|
131
158
|
let left = 0;
|
|
@@ -144,7 +171,8 @@ class BaseMindooDB {
|
|
|
144
171
|
}
|
|
145
172
|
// Insert at the correct position
|
|
146
173
|
this.index.splice(insertIndex, 0, newEntry);
|
|
147
|
-
// Update lookup map for
|
|
174
|
+
// Update lookup map for entries from insertion point onwards
|
|
175
|
+
// Only update entries that actually moved (from insertIndex to end)
|
|
148
176
|
for (let i = insertIndex; i < this.index.length; i++) {
|
|
149
177
|
this.indexLookup.set(this.index[i].docId, i);
|
|
150
178
|
}
|
|
@@ -182,7 +210,7 @@ class BaseMindooDB {
|
|
|
182
210
|
entryIds.push(entryId);
|
|
183
211
|
}
|
|
184
212
|
else {
|
|
185
|
-
|
|
213
|
+
this.logger.warn(`Could not resolve automerge hash ${hash} to entry ID for doc ${docId}`);
|
|
186
214
|
}
|
|
187
215
|
}
|
|
188
216
|
return entryIds;
|
|
@@ -197,7 +225,7 @@ class BaseMindooDB {
|
|
|
197
225
|
* Initialize the database instance.
|
|
198
226
|
*/
|
|
199
227
|
async initialize() {
|
|
200
|
-
|
|
228
|
+
this.logger.info(`Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
201
229
|
await this.syncStoreChanges();
|
|
202
230
|
}
|
|
203
231
|
/**
|
|
@@ -206,13 +234,13 @@ class BaseMindooDB {
|
|
|
206
234
|
* On first call (when processedEntryIds is empty), it will process all entries.
|
|
207
235
|
*/
|
|
208
236
|
async syncStoreChanges() {
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
this.logger.debug(`Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
238
|
+
this.logger.debug(`Already processed ${this.processedEntryIds.length} entry IDs`);
|
|
211
239
|
// Find new entries that we haven't processed yet
|
|
212
|
-
const newEntryMetadata = await this.
|
|
213
|
-
|
|
240
|
+
const newEntryMetadata = await this.getNewEntryMetadataForSync();
|
|
241
|
+
this.logger.debug(`Found ${newEntryMetadata.length} new entries`);
|
|
214
242
|
if (newEntryMetadata.length === 0) {
|
|
215
|
-
|
|
243
|
+
this.logger.debug(`No new entries to process`);
|
|
216
244
|
return;
|
|
217
245
|
}
|
|
218
246
|
// Group new entries by document ID
|
|
@@ -224,49 +252,191 @@ class BaseMindooDB {
|
|
|
224
252
|
entriesByDoc.get(entryMeta.docId).push(entryMeta);
|
|
225
253
|
}
|
|
226
254
|
// Process each document with new entries
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
255
|
+
// Use incremental cache updates when possible
|
|
256
|
+
// Process documents in parallel with concurrency limit
|
|
257
|
+
this.logger.debug(`Processing ${entriesByDoc.size} documents with new entries`);
|
|
258
|
+
// Helper function to process a single document
|
|
259
|
+
const processDocument = async (docId, entryMetadataList) => {
|
|
230
260
|
try {
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
this.docCache.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
this.logger.debug(`===== Processing document ${docId} with ${entryMetadataList.length} new entry(s) in syncStoreChanges =====`);
|
|
262
|
+
// Check if document is cached
|
|
263
|
+
const cachedDoc = this.docCache.get(docId);
|
|
264
|
+
let updatedDoc = null;
|
|
265
|
+
if (cachedDoc) {
|
|
266
|
+
// Document is cached - try incremental update
|
|
267
|
+
this.logger.debug(`Document ${docId} found in cache, attempting incremental update`);
|
|
268
|
+
try {
|
|
269
|
+
updatedDoc = await this.applyNewEntriesToCachedDocument(cachedDoc, entryMetadataList);
|
|
270
|
+
if (updatedDoc) {
|
|
271
|
+
this.logger.debug(`Successfully updated cached document ${docId} incrementally`);
|
|
272
|
+
// Only update index if document actually changed
|
|
273
|
+
this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
|
|
274
|
+
this.logger.debug(`Updated index for document ${docId} (lastModified: ${updatedDoc.lastModified}, isDeleted: ${updatedDoc.isDeleted})`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
this.logger.debug(`Document ${docId} unchanged after applying new entries, skipping index update`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
// If incremental update fails, fall back to full reload
|
|
282
|
+
this.logger.warn(`Incremental update failed for document ${docId}, falling back to full reload:`, error);
|
|
283
|
+
this.docCache.delete(docId);
|
|
284
|
+
updatedDoc = await this.loadDocumentInternal(docId);
|
|
285
|
+
if (updatedDoc) {
|
|
286
|
+
this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
243
289
|
}
|
|
244
290
|
else {
|
|
245
|
-
|
|
291
|
+
// Document not cached - load from scratch
|
|
292
|
+
this.logger.debug(`Document ${docId} not in cache, loading from store`);
|
|
293
|
+
updatedDoc = await this.loadDocumentInternal(docId);
|
|
294
|
+
if (updatedDoc) {
|
|
295
|
+
this.logger.debug(`Successfully loaded document ${docId}, updating index`);
|
|
296
|
+
this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
|
|
297
|
+
this.logger.debug(`Updated index for document ${docId} (lastModified: ${updatedDoc.lastModified}, isDeleted: ${updatedDoc.isDeleted})`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
this.logger.warn(`Document ${docId} returned null from loadDocumentInternal`);
|
|
301
|
+
}
|
|
246
302
|
}
|
|
247
303
|
}
|
|
248
304
|
catch (error) {
|
|
249
305
|
// Handle missing symmetric key gracefully - skip documents we can't decrypt
|
|
250
306
|
if (error instanceof errors_1.SymmetricKeyNotFoundError) {
|
|
251
|
-
|
|
307
|
+
this.logger.debug(`Skipping document ${docId} - missing key: ${error.keyId}`);
|
|
252
308
|
// Mark entry IDs as processed so we don't retry them
|
|
253
309
|
this.processedEntryIds.push(...entryMetadataList.map(em => em.id));
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
console.error(`[BaseMindooDB] ===== ERROR processing document ${docId} in syncStoreChanges =====`);
|
|
257
|
-
console.error(`[BaseMindooDB] Error type: ${error instanceof Error ? error.constructor.name : typeof error}`);
|
|
258
|
-
console.error(`[BaseMindooDB] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
|
-
if (error instanceof Error && error.stack) {
|
|
260
|
-
console.error(`[BaseMindooDB] Error stack: ${error.stack}`);
|
|
310
|
+
return; // Skip this document
|
|
261
311
|
}
|
|
312
|
+
this.logger.error(`===== ERROR processing document ${docId} in syncStoreChanges =====`, error);
|
|
262
313
|
// Re-throw the error so we can see what's happening in the test
|
|
263
|
-
console.error(`[BaseMindooDB] Re-throwing error for document ${docId}`);
|
|
264
314
|
throw error;
|
|
265
315
|
}
|
|
316
|
+
};
|
|
317
|
+
// Process documents in parallel with concurrency limit (default: 10)
|
|
318
|
+
const maxConcurrency = 10;
|
|
319
|
+
const documentEntries = Array.from(entriesByDoc.entries());
|
|
320
|
+
// Process in batches to limit concurrency
|
|
321
|
+
const results = [];
|
|
322
|
+
for (let i = 0; i < documentEntries.length; i += maxConcurrency) {
|
|
323
|
+
const batch = documentEntries.slice(i, i + maxConcurrency);
|
|
324
|
+
const batchResults = await Promise.allSettled(batch.map(([docId, entryMetadataList]) => processDocument(docId, entryMetadataList)));
|
|
325
|
+
results.push(...batchResults);
|
|
326
|
+
}
|
|
327
|
+
// Check for errors (excluding SymmetricKeyNotFoundError which is handled gracefully)
|
|
328
|
+
for (let i = 0; i < results.length; i++) {
|
|
329
|
+
const result = results[i];
|
|
330
|
+
if (result.status === 'rejected') {
|
|
331
|
+
const error = result.reason;
|
|
332
|
+
if (!(error instanceof errors_1.SymmetricKeyNotFoundError)) {
|
|
333
|
+
// Re-throw non-key errors
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
266
337
|
}
|
|
267
338
|
// Append new entry IDs to our processed list
|
|
268
339
|
this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
|
|
269
|
-
|
|
340
|
+
this.logger.debug(`Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
|
|
341
|
+
}
|
|
342
|
+
supportsCursorScan(store) {
|
|
343
|
+
return typeof store.scanEntriesSince === "function";
|
|
344
|
+
}
|
|
345
|
+
async scanAllMetadata(store, filters) {
|
|
346
|
+
if (!this.supportsCursorScan(store)) {
|
|
347
|
+
if (filters?.docId) {
|
|
348
|
+
return store.findNewEntriesForDoc([], filters.docId);
|
|
349
|
+
}
|
|
350
|
+
return store.findNewEntries([]);
|
|
351
|
+
}
|
|
352
|
+
const all = [];
|
|
353
|
+
let cursor = null;
|
|
354
|
+
while (true) {
|
|
355
|
+
const page = await store.scanEntriesSince(cursor, 1000, filters);
|
|
356
|
+
all.push(...page.entries);
|
|
357
|
+
cursor = page.nextCursor;
|
|
358
|
+
if (!page.hasMore) {
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return all;
|
|
363
|
+
}
|
|
364
|
+
async getNewEntryMetadataForSync() {
|
|
365
|
+
if (!this.supportsCursorScan(this.store)) {
|
|
366
|
+
return this.store.findNewEntries(this.processedEntryIds);
|
|
367
|
+
}
|
|
368
|
+
const allNew = [];
|
|
369
|
+
let cursor = this.processedEntryCursor;
|
|
370
|
+
while (true) {
|
|
371
|
+
const page = await this.store.scanEntriesSince(cursor, 1000);
|
|
372
|
+
allNew.push(...page.entries);
|
|
373
|
+
cursor = page.nextCursor;
|
|
374
|
+
if (!page.hasMore) {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.processedEntryCursor = cursor;
|
|
379
|
+
return allNew;
|
|
380
|
+
}
|
|
381
|
+
async syncEntriesFromStore(sourceStore, targetStore) {
|
|
382
|
+
let transferred = 0;
|
|
383
|
+
let targetBloom = null;
|
|
384
|
+
if (typeof targetStore.getIdBloomSummary === "function") {
|
|
385
|
+
try {
|
|
386
|
+
targetBloom = await targetStore.getIdBloomSummary();
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
this.logger.warn("Failed to get bloom summary from target store, falling back to exact checks", error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (this.supportsCursorScan(sourceStore)) {
|
|
393
|
+
let cursor = null;
|
|
394
|
+
while (true) {
|
|
395
|
+
const page = await sourceStore.scanEntriesSince(cursor, 1000);
|
|
396
|
+
if (page.entries.length > 0) {
|
|
397
|
+
const ids = page.entries.map((m) => m.id);
|
|
398
|
+
let definitelyMissing = [];
|
|
399
|
+
let maybeExisting = ids;
|
|
400
|
+
if (targetBloom) {
|
|
401
|
+
definitelyMissing = [];
|
|
402
|
+
maybeExisting = [];
|
|
403
|
+
for (const id of ids) {
|
|
404
|
+
if ((0, bloom_1.bloomMightContainId)(targetBloom, id)) {
|
|
405
|
+
maybeExisting.push(id);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
definitelyMissing.push(id);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
let missingIds = definitelyMissing;
|
|
413
|
+
if (maybeExisting.length > 0) {
|
|
414
|
+
const existing = await targetStore.hasEntries(maybeExisting);
|
|
415
|
+
const existingSet = new Set(existing);
|
|
416
|
+
const maybeMissing = maybeExisting.filter((id) => !existingSet.has(id));
|
|
417
|
+
missingIds = missingIds.concat(maybeMissing);
|
|
418
|
+
}
|
|
419
|
+
if (missingIds.length > 0) {
|
|
420
|
+
const missingEntries = await sourceStore.getEntries(missingIds);
|
|
421
|
+
await targetStore.putEntries(missingEntries);
|
|
422
|
+
transferred += missingEntries.length;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
cursor = page.nextCursor;
|
|
426
|
+
if (!page.hasMore) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return transferred;
|
|
431
|
+
}
|
|
432
|
+
const targetIds = await targetStore.getAllIds();
|
|
433
|
+
const sourceNewMetadata = await sourceStore.findNewEntries(targetIds);
|
|
434
|
+
if (sourceNewMetadata.length === 0) {
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
const sourceEntries = await sourceStore.getEntries(sourceNewMetadata.map((m) => m.id));
|
|
438
|
+
await targetStore.putEntries(sourceEntries);
|
|
439
|
+
return sourceEntries.length;
|
|
270
440
|
}
|
|
271
441
|
getStore() {
|
|
272
442
|
return this.store;
|
|
@@ -306,72 +476,72 @@ class BaseMindooDB {
|
|
|
306
476
|
}
|
|
307
477
|
// Generate UUID7 for document ID
|
|
308
478
|
const docId = (0, uuid_1.v7)();
|
|
309
|
-
|
|
479
|
+
this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
310
480
|
// Create initial Automerge document
|
|
311
|
-
const initialDoc = Automerge.init();
|
|
481
|
+
const initialDoc = automerge_adapter_1.Automerge.init();
|
|
312
482
|
// Create the first change
|
|
313
483
|
const now = Date.now();
|
|
314
|
-
|
|
484
|
+
this.logger.debug(`Creating initial Automerge change for document ${docId}`);
|
|
315
485
|
let newDoc;
|
|
316
486
|
try {
|
|
317
|
-
newDoc = Automerge.change(initialDoc, (doc) => {
|
|
487
|
+
newDoc = automerge_adapter_1.Automerge.change(initialDoc, (doc) => {
|
|
318
488
|
// Store metadata in the document payload
|
|
319
489
|
// We need to modify the document to ensure a change is created
|
|
320
490
|
doc._attachments = [];
|
|
321
491
|
});
|
|
322
|
-
|
|
492
|
+
this.logger.debug(`Successfully created Automerge change, document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
323
493
|
}
|
|
324
494
|
catch (error) {
|
|
325
|
-
|
|
495
|
+
this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
|
|
326
496
|
throw error;
|
|
327
497
|
}
|
|
328
498
|
// Get the change bytes from the document
|
|
329
|
-
|
|
330
|
-
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
499
|
+
this.logger.debug(`Getting change bytes from document ${docId}`);
|
|
500
|
+
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
331
501
|
if (!changeBytes) {
|
|
332
502
|
throw new Error("Failed to get change bytes from Automerge document");
|
|
333
503
|
}
|
|
334
|
-
|
|
504
|
+
this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
|
|
335
505
|
// Decode the change to get hash and dependencies
|
|
336
|
-
|
|
506
|
+
this.logger.debug(`Decoding change to get hash and dependencies`);
|
|
337
507
|
let decodedChange;
|
|
338
508
|
try {
|
|
339
|
-
decodedChange = Automerge.decodeChange(changeBytes);
|
|
340
|
-
|
|
509
|
+
decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
510
|
+
this.logger.debug(`Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
|
|
341
511
|
}
|
|
342
512
|
catch (error) {
|
|
343
|
-
|
|
513
|
+
this.logger.error(`Error decoding change for document ${docId}:`, error);
|
|
344
514
|
throw error;
|
|
345
515
|
}
|
|
346
516
|
const automergeHash = decodedChange.hash;
|
|
347
517
|
const automergeDepHashes = decodedChange.deps || []; // First change has no dependencies
|
|
348
518
|
// Encrypt the change payload first
|
|
349
|
-
|
|
519
|
+
this.logger.debug(`Encrypting change payload for document ${docId}`);
|
|
350
520
|
const encryptedPayload = await this.tenant.encryptPayload(changeBytes, keyId);
|
|
351
|
-
|
|
521
|
+
this.logger.debug(`Encrypted payload: ${changeBytes.length} -> ${encryptedPayload.length} bytes`);
|
|
352
522
|
// Compute content hash from encrypted data
|
|
353
523
|
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
354
|
-
|
|
524
|
+
this.logger.debug(`Computed content hash: ${contentHash.substring(0, 16)}...`);
|
|
355
525
|
// Generate entry ID with blockchain-like chaining
|
|
356
526
|
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
|
|
357
|
-
|
|
527
|
+
this.logger.debug(`Generated entry ID: ${entryId}`);
|
|
358
528
|
// Resolve Automerge dependency hashes to entry IDs (empty for first change)
|
|
359
529
|
const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
|
|
360
530
|
// Sign the encrypted payload - either with custom key or current user's key
|
|
361
531
|
let signature;
|
|
362
532
|
let createdByPublicKey;
|
|
363
533
|
if (useCustomSigningKey) {
|
|
364
|
-
|
|
534
|
+
this.logger.debug(`Signing encrypted payload for document ${docId} with provided key`);
|
|
365
535
|
signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
|
|
366
536
|
createdByPublicKey = signingKeyPair.publicKey;
|
|
367
537
|
}
|
|
368
538
|
else {
|
|
369
|
-
|
|
539
|
+
this.logger.debug(`Signing encrypted payload for document ${docId}`);
|
|
370
540
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
371
541
|
signature = await this.tenant.signPayload(encryptedPayload);
|
|
372
542
|
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
373
543
|
}
|
|
374
|
-
|
|
544
|
+
this.logger.debug(`Signed payload, signature length: ${signature.length} bytes`);
|
|
375
545
|
// Create entry metadata
|
|
376
546
|
const entryMetadata = {
|
|
377
547
|
entryType: "doc_create",
|
|
@@ -407,8 +577,8 @@ class BaseMindooDB {
|
|
|
407
577
|
// Update cache and index
|
|
408
578
|
this.docCache.set(docId, internalDoc);
|
|
409
579
|
this.updateIndex(docId, internalDoc.lastModified, false);
|
|
410
|
-
|
|
411
|
-
|
|
580
|
+
this.logger.info(`Document ${docId} created successfully`);
|
|
581
|
+
this.logger.debug(`Document ${docId} cached and indexed (lastModified: ${internalDoc.lastModified})`);
|
|
412
582
|
return this.wrapDocument(internalDoc);
|
|
413
583
|
}
|
|
414
584
|
async getDocument(docId) {
|
|
@@ -422,9 +592,9 @@ class BaseMindooDB {
|
|
|
422
592
|
return this.wrapDocument(internalDoc);
|
|
423
593
|
}
|
|
424
594
|
async getDocumentAtTimestamp(docId, timestamp) {
|
|
425
|
-
|
|
595
|
+
this.logger.debug(`Getting document ${docId} at timestamp ${timestamp}`);
|
|
426
596
|
// Get all entry metadata for this document
|
|
427
|
-
const allEntryMetadata = await this.store
|
|
597
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
428
598
|
// Filter entries up to the timestamp
|
|
429
599
|
const relevantEntries = allEntryMetadata
|
|
430
600
|
.filter(em => em.createdAt <= timestamp)
|
|
@@ -435,24 +605,24 @@ class BaseMindooDB {
|
|
|
435
605
|
// Load entries
|
|
436
606
|
const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
|
|
437
607
|
// Apply changes in order
|
|
438
|
-
let doc = Automerge.init();
|
|
608
|
+
let doc = automerge_adapter_1.Automerge.init();
|
|
439
609
|
for (const entryData of entries) {
|
|
440
610
|
// Admin-only mode: only accept entries signed by the admin key
|
|
441
611
|
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
442
|
-
|
|
612
|
+
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
443
613
|
continue;
|
|
444
614
|
}
|
|
445
615
|
// Verify signature against the encrypted payload (no decryption needed)
|
|
446
616
|
// We sign the encrypted payload, so anyone can verify signatures without decryption keys
|
|
447
617
|
const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
|
|
448
618
|
if (!isValid) {
|
|
449
|
-
|
|
619
|
+
this.logger.warn(`Invalid signature for entry ${entryData.id}, skipping`);
|
|
450
620
|
continue;
|
|
451
621
|
}
|
|
452
622
|
// Decrypt payload (only after signature verification passes)
|
|
453
623
|
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
454
624
|
// Apply change using loadIncremental - this is the recommended way for binary change data
|
|
455
|
-
doc = Automerge.loadIncremental(doc, decryptedPayload);
|
|
625
|
+
doc = automerge_adapter_1.Automerge.loadIncremental(doc, decryptedPayload);
|
|
456
626
|
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
457
627
|
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
458
628
|
if (parsed) {
|
|
@@ -461,24 +631,125 @@ class BaseMindooDB {
|
|
|
461
631
|
}
|
|
462
632
|
// Check if document was deleted at this timestamp
|
|
463
633
|
// Look for a "doc_delete" type entry in the relevant entries
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
return null; // Document was deleted at this time
|
|
467
|
-
}
|
|
634
|
+
const deleteEntry = relevantEntries.find(em => em.entryType === "doc_delete" && em.createdAt <= timestamp);
|
|
635
|
+
const isDeleted = deleteEntry !== undefined;
|
|
468
636
|
// Find the first entry to get createdAt and decryptionKeyId
|
|
469
637
|
const firstEntry = relevantEntries.length > 0 ? relevantEntries[0] : null;
|
|
470
638
|
const createdAt = firstEntry ? firstEntry.createdAt : timestamp;
|
|
471
639
|
const decryptionKeyId = firstEntry ? firstEntry.decryptionKeyId : "default";
|
|
640
|
+
// Use delete entry timestamp as lastModified if deleted, otherwise use the requested timestamp
|
|
641
|
+
const lastModified = isDeleted && deleteEntry ? deleteEntry.createdAt : timestamp;
|
|
472
642
|
const internalDoc = {
|
|
473
643
|
id: docId,
|
|
474
644
|
doc,
|
|
475
645
|
createdAt,
|
|
476
|
-
lastModified
|
|
646
|
+
lastModified,
|
|
477
647
|
decryptionKeyId,
|
|
478
|
-
isDeleted
|
|
648
|
+
isDeleted,
|
|
479
649
|
};
|
|
480
650
|
return this.wrapDocument(internalDoc);
|
|
481
651
|
}
|
|
652
|
+
async *iterateDocumentHistory(docId) {
|
|
653
|
+
this.logger.debug(`Iterating document history for ${docId}`);
|
|
654
|
+
// Get all entry metadata for this document
|
|
655
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
656
|
+
// Filter to only doc_create, doc_change, and doc_delete entries (exclude snapshots)
|
|
657
|
+
const relevantEntries = allEntryMetadata
|
|
658
|
+
.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete")
|
|
659
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
660
|
+
if (relevantEntries.length === 0) {
|
|
661
|
+
return; // Document has no history
|
|
662
|
+
}
|
|
663
|
+
// Load all entries
|
|
664
|
+
const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
|
|
665
|
+
// Build a map for quick lookup
|
|
666
|
+
const entryMap = new Map(entries.map(e => [e.id, e]));
|
|
667
|
+
// Apply changes in order
|
|
668
|
+
let currentDoc = null;
|
|
669
|
+
let createdAt = null;
|
|
670
|
+
let decryptionKeyId = "default";
|
|
671
|
+
for (const entryMetadata of relevantEntries) {
|
|
672
|
+
const entryData = entryMap.get(entryMetadata.id);
|
|
673
|
+
if (!entryData) {
|
|
674
|
+
this.logger.warn(`Entry ${entryMetadata.id} not found in store, skipping`);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
// Admin-only mode: only accept entries signed by the admin key
|
|
678
|
+
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
679
|
+
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
// Verify signature
|
|
683
|
+
const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
|
|
684
|
+
if (!isValid) {
|
|
685
|
+
this.logger.warn(`Invalid signature for entry ${entryData.id}, skipping`);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
// Decrypt payload
|
|
689
|
+
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
690
|
+
// Initialize document if this is the first entry (doc_create)
|
|
691
|
+
const isFirstEntry = currentDoc === null;
|
|
692
|
+
if (isFirstEntry) {
|
|
693
|
+
if (entryMetadata.entryType !== "doc_create") {
|
|
694
|
+
this.logger.warn(`First entry is not doc_create, skipping`);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
currentDoc = automerge_adapter_1.Automerge.init();
|
|
698
|
+
createdAt = entryMetadata.createdAt;
|
|
699
|
+
decryptionKeyId = entryMetadata.decryptionKeyId;
|
|
700
|
+
}
|
|
701
|
+
// Check document heads before applying change (for non-first entries)
|
|
702
|
+
const headsBefore = isFirstEntry ? null : (currentDoc ? automerge_adapter_1.Automerge.getHeads(currentDoc) : null);
|
|
703
|
+
// Apply change using loadIncremental
|
|
704
|
+
if (currentDoc === null) {
|
|
705
|
+
throw new Error("currentDoc should not be null at this point");
|
|
706
|
+
}
|
|
707
|
+
currentDoc = automerge_adapter_1.Automerge.loadIncremental(currentDoc, decryptedPayload);
|
|
708
|
+
// Check if document actually changed by comparing heads
|
|
709
|
+
// For first entry (doc_create), always yield since it's the initial creation
|
|
710
|
+
// For delete entries, always yield
|
|
711
|
+
// For other entries, only yield if heads changed
|
|
712
|
+
let shouldYield = false;
|
|
713
|
+
if (isFirstEntry || entryMetadata.entryType === "doc_delete") {
|
|
714
|
+
shouldYield = true;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
const headsAfter = automerge_adapter_1.Automerge.getHeads(currentDoc);
|
|
718
|
+
const headsChanged = headsBefore !== null && JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
|
|
719
|
+
shouldYield = headsChanged;
|
|
720
|
+
}
|
|
721
|
+
// Register automerge hash mapping
|
|
722
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
723
|
+
if (parsed) {
|
|
724
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
725
|
+
}
|
|
726
|
+
// Only yield if document actually changed (or if this is create/delete entry)
|
|
727
|
+
if (shouldYield) {
|
|
728
|
+
// Clone the document to ensure independence
|
|
729
|
+
const clonedDoc = automerge_adapter_1.Automerge.clone(currentDoc);
|
|
730
|
+
// Create internal doc representation
|
|
731
|
+
const internalDoc = {
|
|
732
|
+
id: docId,
|
|
733
|
+
doc: clonedDoc,
|
|
734
|
+
createdAt: createdAt,
|
|
735
|
+
lastModified: entryMetadata.createdAt,
|
|
736
|
+
decryptionKeyId,
|
|
737
|
+
isDeleted: entryMetadata.entryType === "doc_delete",
|
|
738
|
+
};
|
|
739
|
+
// Wrap and yield (including delete entries)
|
|
740
|
+
const wrappedDoc = this.wrapDocument(internalDoc);
|
|
741
|
+
yield {
|
|
742
|
+
doc: wrappedDoc,
|
|
743
|
+
changeCreatedAt: entryMetadata.createdAt,
|
|
744
|
+
changeCreatedByPublicKey: entryMetadata.createdByPublicKey,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
// Stop after delete entry (document is deleted, no more changes)
|
|
748
|
+
if (entryMetadata.entryType === "doc_delete") {
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
482
753
|
async getAllDocumentIds() {
|
|
483
754
|
// Return all non-deleted document IDs from index
|
|
484
755
|
const docIds = [];
|
|
@@ -489,6 +760,53 @@ class BaseMindooDB {
|
|
|
489
760
|
}
|
|
490
761
|
return docIds;
|
|
491
762
|
}
|
|
763
|
+
async getAllDocumentIdsAtTimestamp(timestamp) {
|
|
764
|
+
// findEntries() uses an exclusive upper bound (createdAt < creationDateUntil).
|
|
765
|
+
// Query up to timestamp + 1 so entries created exactly at `timestamp` are included
|
|
766
|
+
// for the strict checks below (createTime < timestamp, deleteTime > timestamp).
|
|
767
|
+
const upperBoundExclusive = timestamp === Number.MAX_SAFE_INTEGER ? timestamp : timestamp + 1;
|
|
768
|
+
// Efficiently query for doc_create and doc_delete entries before the timestamp
|
|
769
|
+
const [creates, deletes] = await Promise.all([
|
|
770
|
+
this.store.findEntries("doc_create", null, upperBoundExclusive),
|
|
771
|
+
this.store.findEntries("doc_delete", null, upperBoundExclusive)
|
|
772
|
+
]);
|
|
773
|
+
// Build sets of docIds for efficient lookup
|
|
774
|
+
const createdDocIds = new Set();
|
|
775
|
+
const deletedDocIds = new Set();
|
|
776
|
+
// Track earliest create time for each doc
|
|
777
|
+
const createTimes = new Map();
|
|
778
|
+
for (const entry of creates) {
|
|
779
|
+
const existingTime = createTimes.get(entry.docId);
|
|
780
|
+
if (!existingTime || entry.createdAt < existingTime) {
|
|
781
|
+
createTimes.set(entry.docId, entry.createdAt);
|
|
782
|
+
createdDocIds.add(entry.docId);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Track earliest delete time for each doc
|
|
786
|
+
const deleteTimes = new Map();
|
|
787
|
+
for (const entry of deletes) {
|
|
788
|
+
const existingTime = deleteTimes.get(entry.docId);
|
|
789
|
+
if (!existingTime || entry.createdAt < existingTime) {
|
|
790
|
+
deleteTimes.set(entry.docId, entry.createdAt);
|
|
791
|
+
deletedDocIds.add(entry.docId);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Find documents that existed at the timestamp
|
|
795
|
+
const docIds = [];
|
|
796
|
+
for (const docId of createdDocIds) {
|
|
797
|
+
const createTime = createTimes.get(docId);
|
|
798
|
+
const deleteTime = deleteTimes.get(docId);
|
|
799
|
+
// Document exists at timestamp if:
|
|
800
|
+
// 1. It was created at or before the timestamp
|
|
801
|
+
// 2. Either it was never deleted, or it was deleted after the timestamp
|
|
802
|
+
if (createTime <= timestamp) {
|
|
803
|
+
if (!deleteTime || deleteTime > timestamp) {
|
|
804
|
+
docIds.push(docId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return docIds;
|
|
809
|
+
}
|
|
492
810
|
async deleteDocument(docId) {
|
|
493
811
|
return this.deleteDocInternal(docId);
|
|
494
812
|
}
|
|
@@ -502,7 +820,7 @@ class BaseMindooDB {
|
|
|
502
820
|
*/
|
|
503
821
|
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword) {
|
|
504
822
|
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
505
|
-
|
|
823
|
+
this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
506
824
|
// Admin-only validation: only admin key can modify data in admin-only databases
|
|
507
825
|
if (this._isAdminOnlyDb) {
|
|
508
826
|
const adminPublicKey = this.getAdminPublicKey();
|
|
@@ -521,7 +839,7 @@ class BaseMindooDB {
|
|
|
521
839
|
// Create deletion change by clearing all fields from the document
|
|
522
840
|
// This ensures Automerge produces actual change bytes
|
|
523
841
|
// The deletion is also tracked via the "doc_delete" type entry in the append-only store
|
|
524
|
-
const newDoc = Automerge.change(internalDoc.doc, (doc) => {
|
|
842
|
+
const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (doc) => {
|
|
525
843
|
// Remove all fields from the document
|
|
526
844
|
for (const key of Object.keys(doc)) {
|
|
527
845
|
delete doc[key];
|
|
@@ -530,12 +848,12 @@ class BaseMindooDB {
|
|
|
530
848
|
doc._deleted = true;
|
|
531
849
|
});
|
|
532
850
|
// Get the change bytes from the document
|
|
533
|
-
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
851
|
+
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
534
852
|
if (!changeBytes) {
|
|
535
853
|
throw new Error("Failed to get change bytes from Automerge document");
|
|
536
854
|
}
|
|
537
855
|
// Decode the change to get hash and dependencies
|
|
538
|
-
const decodedChange = Automerge.decodeChange(changeBytes);
|
|
856
|
+
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
539
857
|
const automergeHash = decodedChange.hash;
|
|
540
858
|
const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
|
|
541
859
|
// Encrypt the change payload first
|
|
@@ -550,12 +868,12 @@ class BaseMindooDB {
|
|
|
550
868
|
let signature;
|
|
551
869
|
let createdByPublicKey;
|
|
552
870
|
if (useCustomSigningKey) {
|
|
553
|
-
|
|
871
|
+
this.logger.debug(`Signing deletion for document ${docId} with provided key`);
|
|
554
872
|
signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
|
|
555
873
|
createdByPublicKey = signingKeyPair.publicKey;
|
|
556
874
|
}
|
|
557
875
|
else {
|
|
558
|
-
|
|
876
|
+
this.logger.debug(`Signing deletion for document ${docId} with current user's key`);
|
|
559
877
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
560
878
|
signature = await this.tenant.signPayload(encryptedPayload);
|
|
561
879
|
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
@@ -588,7 +906,7 @@ class BaseMindooDB {
|
|
|
588
906
|
internalDoc.lastModified = Date.now();
|
|
589
907
|
this.docCache.set(docId, internalDoc);
|
|
590
908
|
this.updateIndex(docId, internalDoc.lastModified, true);
|
|
591
|
-
|
|
909
|
+
this.logger.info(`Document ${docId} deleted successfully`);
|
|
592
910
|
}
|
|
593
911
|
async changeDoc(doc, changeFunc) {
|
|
594
912
|
return this.changeDocInternal(doc, changeFunc);
|
|
@@ -599,7 +917,7 @@ class BaseMindooDB {
|
|
|
599
917
|
async changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword) {
|
|
600
918
|
const docId = doc.getId();
|
|
601
919
|
const useCustomKey = signingKeyPair && signingKeyPassword;
|
|
602
|
-
|
|
920
|
+
this.logger.debug(`===== ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'} called for document ${docId} =====`);
|
|
603
921
|
// Admin-only validation: only admin key can modify data in admin-only databases
|
|
604
922
|
if (this._isAdminOnlyDb) {
|
|
605
923
|
const adminPublicKey = this.getAdminPublicKey();
|
|
@@ -613,24 +931,24 @@ class BaseMindooDB {
|
|
|
613
931
|
// Get internal document from cache or load it
|
|
614
932
|
let internalDoc = this.docCache.get(docId);
|
|
615
933
|
if (!internalDoc) {
|
|
616
|
-
|
|
934
|
+
this.logger.debug(`Document ${docId} not in cache, loading from store`);
|
|
617
935
|
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
618
936
|
if (!loadedDoc) {
|
|
619
937
|
throw new Error(`Document ${docId} not found`);
|
|
620
938
|
}
|
|
621
939
|
internalDoc = loadedDoc;
|
|
622
|
-
|
|
940
|
+
this.logger.debug(`Successfully loaded document ${docId} from store for ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'}`);
|
|
623
941
|
}
|
|
624
942
|
else {
|
|
625
|
-
|
|
943
|
+
this.logger.debug(`Document ${docId} found in cache`);
|
|
626
944
|
}
|
|
627
945
|
if (internalDoc.isDeleted) {
|
|
628
946
|
throw new Error(`Document ${docId} has been deleted`);
|
|
629
947
|
}
|
|
630
948
|
// Apply the change function
|
|
631
949
|
const now = Date.now();
|
|
632
|
-
|
|
633
|
-
|
|
950
|
+
this.logger.debug(`Applying change function to document ${docId}`);
|
|
951
|
+
this.logger.debug(`Document state before change: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(internalDoc.doc))}`);
|
|
634
952
|
// For async callbacks, we need to handle document modifications carefully.
|
|
635
953
|
// Automerge.change() requires synchronous modifications, so we'll:
|
|
636
954
|
// 1. Execute the async callback to do any async work (like signing)
|
|
@@ -673,8 +991,8 @@ class BaseMindooDB {
|
|
|
673
991
|
pendingDeletions.delete(prop);
|
|
674
992
|
// Track the change
|
|
675
993
|
pendingChanges.set(prop, value);
|
|
676
|
-
//
|
|
677
|
-
|
|
994
|
+
// NOTE: Don't set on target immediately - Automerge requires changes
|
|
995
|
+
// to be made inside Automerge.change(). Pending changes are applied later.
|
|
678
996
|
}
|
|
679
997
|
return true;
|
|
680
998
|
},
|
|
@@ -685,8 +1003,8 @@ class BaseMindooDB {
|
|
|
685
1003
|
pendingDeletions.add(prop);
|
|
686
1004
|
// Remove from pending changes if it was there
|
|
687
1005
|
pendingChanges.delete(prop);
|
|
688
|
-
//
|
|
689
|
-
|
|
1006
|
+
// NOTE: Don't delete from target immediately - Automerge requires changes
|
|
1007
|
+
// to be made inside Automerge.change(). Pending deletions are applied later.
|
|
690
1008
|
}
|
|
691
1009
|
return true;
|
|
692
1010
|
},
|
|
@@ -805,7 +1123,7 @@ class BaseMindooDB {
|
|
|
805
1123
|
// Now apply the collected changes synchronously in Automerge.change()
|
|
806
1124
|
let newDoc;
|
|
807
1125
|
try {
|
|
808
|
-
newDoc = Automerge.change(internalDoc.doc, (automergeDoc) => {
|
|
1126
|
+
newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (automergeDoc) => {
|
|
809
1127
|
// Apply all pending changes (sets/updates)
|
|
810
1128
|
for (const [key, value] of pendingChanges) {
|
|
811
1129
|
automergeDoc[key] = value;
|
|
@@ -844,22 +1162,22 @@ class BaseMindooDB {
|
|
|
844
1162
|
// Update lastModified timestamp
|
|
845
1163
|
automergeDoc._lastModified = now;
|
|
846
1164
|
});
|
|
847
|
-
|
|
1165
|
+
this.logger.debug(`Successfully applied change function, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
848
1166
|
}
|
|
849
1167
|
catch (error) {
|
|
850
|
-
|
|
1168
|
+
this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
|
|
851
1169
|
throw error;
|
|
852
1170
|
}
|
|
853
1171
|
// Get the change bytes from the document
|
|
854
|
-
|
|
855
|
-
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
1172
|
+
this.logger.debug(`Getting change bytes from document ${docId}`);
|
|
1173
|
+
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
856
1174
|
if (!changeBytes) {
|
|
857
1175
|
//TODO decide if we just exit here or throw an error
|
|
858
1176
|
throw new Error("Failed to get change bytes from Automerge document");
|
|
859
1177
|
}
|
|
860
|
-
|
|
1178
|
+
this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
|
|
861
1179
|
// Decode the change to get hash and dependencies
|
|
862
|
-
const decodedChange = Automerge.decodeChange(changeBytes);
|
|
1180
|
+
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
863
1181
|
const automergeHash = decodedChange.hash;
|
|
864
1182
|
const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
|
|
865
1183
|
// Encrypt the change payload first
|
|
@@ -868,7 +1186,7 @@ class BaseMindooDB {
|
|
|
868
1186
|
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
869
1187
|
// Generate entry ID with blockchain-like chaining
|
|
870
1188
|
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
|
|
871
|
-
|
|
1189
|
+
this.logger.debug(`Generated entry ID for change: ${entryId}`);
|
|
872
1190
|
// Resolve Automerge dependency hashes to entry IDs
|
|
873
1191
|
const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
|
|
874
1192
|
// Sign the encrypted payload - use custom key if provided, otherwise current user's key
|
|
@@ -911,12 +1229,12 @@ class BaseMindooDB {
|
|
|
911
1229
|
internalDoc.lastModified = Date.now();
|
|
912
1230
|
this.docCache.set(docId, internalDoc);
|
|
913
1231
|
this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted);
|
|
914
|
-
|
|
1232
|
+
this.logger.info(`Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
|
|
915
1233
|
}
|
|
916
|
-
async
|
|
1234
|
+
async *iterateChangesSince(cursor) {
|
|
917
1235
|
// Default to initial cursor if null is provided
|
|
918
1236
|
const actualCursor = cursor ?? { lastModified: 0, docId: "" };
|
|
919
|
-
|
|
1237
|
+
this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
|
|
920
1238
|
// Find starting position using binary search
|
|
921
1239
|
// We want to find the first entry that is greater than the cursor
|
|
922
1240
|
let startIndex = 0;
|
|
@@ -944,77 +1262,190 @@ class BaseMindooDB {
|
|
|
944
1262
|
}
|
|
945
1263
|
}
|
|
946
1264
|
}
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
let
|
|
950
|
-
for (let i = startIndex; i < this.index.length && processedCount < limit; i++) {
|
|
1265
|
+
// Pre-check which documents are in cache to optimize iteration
|
|
1266
|
+
const uncachedDocIds = [];
|
|
1267
|
+
for (let i = startIndex; i < this.index.length; i++) {
|
|
951
1268
|
const entry = this.index[i];
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
continue;
|
|
1269
|
+
if (!this.docCache.has(entry.docId)) {
|
|
1270
|
+
uncachedDocIds.push(entry.docId);
|
|
955
1271
|
}
|
|
1272
|
+
}
|
|
1273
|
+
// Prefetch uncached documents in batches (if any)
|
|
1274
|
+
if (uncachedDocIds.length > 0) {
|
|
1275
|
+
this.logger.debug(`Prefetching ${uncachedDocIds.length} uncached documents for iteration`);
|
|
1276
|
+
const prefetchBatchSize = 10;
|
|
1277
|
+
for (let i = 0; i < uncachedDocIds.length; i += prefetchBatchSize) {
|
|
1278
|
+
const batch = uncachedDocIds.slice(i, i + prefetchBatchSize);
|
|
1279
|
+
await Promise.all(batch.map(docId => this.loadDocumentInternal(docId).catch(err => {
|
|
1280
|
+
this.logger.warn(`Failed to prefetch document ${docId}:`, err);
|
|
1281
|
+
return null;
|
|
1282
|
+
})));
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
// Iterate through the index and yield documents one at a time
|
|
1286
|
+
// Documents should now be in cache from prefetching
|
|
1287
|
+
for (let i = startIndex; i < this.index.length; i++) {
|
|
1288
|
+
const entry = this.index[i];
|
|
956
1289
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1290
|
+
this.logger.debug(`Yielding document ${entry.docId} from index (lastModified: ${entry.lastModified}, isDeleted: ${entry.isDeleted})`);
|
|
1291
|
+
// Check cache first (should be there after prefetching)
|
|
1292
|
+
let internalDoc = this.docCache.get(entry.docId) || null;
|
|
1293
|
+
if (!internalDoc) {
|
|
1294
|
+
// Fallback to loading if not in cache (shouldn't happen after prefetching)
|
|
1295
|
+
this.logger.debug(`Document ${entry.docId} not in cache, loading from store`);
|
|
1296
|
+
internalDoc = await this.loadDocumentInternal(entry.docId);
|
|
1297
|
+
}
|
|
1298
|
+
if (!internalDoc) {
|
|
1299
|
+
this.logger.warn(`Document ${entry.docId} not found, skipping`);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
// Wrap the document (works for both deleted and non-deleted documents)
|
|
1303
|
+
const doc = this.wrapDocument(internalDoc);
|
|
1304
|
+
this.logger.debug(`Successfully loaded document ${entry.docId} (isDeleted: ${doc.isDeleted()})`);
|
|
960
1305
|
// Create cursor for current document
|
|
961
1306
|
const currentCursor = {
|
|
962
1307
|
lastModified: entry.lastModified,
|
|
963
1308
|
docId: entry.docId,
|
|
964
1309
|
};
|
|
965
|
-
|
|
966
|
-
//
|
|
967
|
-
|
|
968
|
-
processedCount++;
|
|
969
|
-
// Stop if callback returns false
|
|
970
|
-
if (shouldContinue === false) {
|
|
971
|
-
console.log(`[BaseMindooDB] Callback requested to stop processing`);
|
|
972
|
-
break;
|
|
973
|
-
}
|
|
1310
|
+
// Yield immediately - this allows the caller to break early after each document
|
|
1311
|
+
// Deleted documents are included so external indexes can handle deletions
|
|
1312
|
+
yield { doc, cursor: currentCursor };
|
|
974
1313
|
}
|
|
975
1314
|
catch (error) {
|
|
976
|
-
|
|
1315
|
+
this.logger.error(`Error processing document ${entry.docId}:`, error);
|
|
977
1316
|
// Stop processing on error
|
|
978
1317
|
throw error;
|
|
979
1318
|
}
|
|
980
1319
|
}
|
|
981
|
-
|
|
982
|
-
return lastCursor;
|
|
1320
|
+
this.logger.debug(`Iteration completed`);
|
|
983
1321
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1322
|
+
/**
|
|
1323
|
+
* Incrementally update a cached document with new entries.
|
|
1324
|
+
* Returns the updated document, or null if document wasn't actually changed.
|
|
1325
|
+
*/
|
|
1326
|
+
async applyNewEntriesToCachedDocument(cachedDoc, newEntryMetadata) {
|
|
1327
|
+
const docId = cachedDoc.id;
|
|
1328
|
+
if (newEntryMetadata.length === 0) {
|
|
1329
|
+
this.logger.debug(`No new entries for cached document ${docId}`);
|
|
1330
|
+
return null; // No changes
|
|
1331
|
+
}
|
|
1332
|
+
this.logger.debug(`Applying ${newEntryMetadata.length} new entries to cached document ${docId}`);
|
|
1333
|
+
// Get current document heads to check if it changes
|
|
1334
|
+
const headsBefore = automerge_adapter_1.Automerge.getHeads(cachedDoc.doc);
|
|
1335
|
+
// Filter entries to only include change entries (exclude snapshots)
|
|
1336
|
+
const entriesToApply = newEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
|
|
1337
|
+
if (entriesToApply.length === 0) {
|
|
1338
|
+
this.logger.debug(`No change entries to apply for document ${docId}`);
|
|
1339
|
+
return null; // No changes
|
|
1340
|
+
}
|
|
1341
|
+
// Sort entries by timestamp
|
|
1342
|
+
entriesToApply.sort((a, b) => a.createdAt - b.createdAt);
|
|
1343
|
+
// Load entries from store
|
|
1344
|
+
const entries = await this.store.getEntries(entriesToApply.map(em => em.id));
|
|
1345
|
+
// Filter entries for admin-only mode first
|
|
1346
|
+
const validEntries = entries.filter(entryData => {
|
|
1347
|
+
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1348
|
+
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
1349
|
+
return false;
|
|
1001
1350
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1351
|
+
return true;
|
|
1352
|
+
});
|
|
1353
|
+
if (validEntries.length === 0) {
|
|
1354
|
+
this.logger.debug(`No valid entries to process for cached document ${docId}`);
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
// Batch signature verification with key caching
|
|
1358
|
+
// Group entries by public key to import each key only once
|
|
1359
|
+
const entriesByPublicKey = new Map();
|
|
1360
|
+
for (const entryData of validEntries) {
|
|
1361
|
+
if (!entriesByPublicKey.has(entryData.createdByPublicKey)) {
|
|
1362
|
+
entriesByPublicKey.set(entryData.createdByPublicKey, []);
|
|
1006
1363
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1364
|
+
entriesByPublicKey.get(entryData.createdByPublicKey).push(entryData);
|
|
1365
|
+
}
|
|
1366
|
+
// Import all unique public keys in parallel (with caching)
|
|
1367
|
+
const keyImportResults = await Promise.all(Array.from(entriesByPublicKey.keys()).map(async (publicKey) => {
|
|
1368
|
+
const cryptoKey = await this.getOrImportPublicKey(publicKey);
|
|
1369
|
+
return { publicKey, cryptoKey };
|
|
1370
|
+
}));
|
|
1371
|
+
// Create a map of public key -> CryptoKey for quick lookup
|
|
1372
|
+
const keyMap = new Map();
|
|
1373
|
+
for (const { publicKey, cryptoKey } of keyImportResults) {
|
|
1374
|
+
if (cryptoKey) {
|
|
1375
|
+
keyMap.set(publicKey, cryptoKey);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
// Verify all signatures in parallel using cached keys
|
|
1379
|
+
const signatureVerificationResults = await Promise.all(validEntries.map(async (entryData) => {
|
|
1380
|
+
const cryptoKey = keyMap.get(entryData.createdByPublicKey);
|
|
1381
|
+
if (!cryptoKey) {
|
|
1382
|
+
// Key was not trusted or failed to import
|
|
1383
|
+
return { entryData, isValid: false };
|
|
1384
|
+
}
|
|
1385
|
+
const isValid = await this.verifySignatureWithKey(cryptoKey, entryData.encryptedData, entryData.signature);
|
|
1386
|
+
return { entryData, isValid };
|
|
1387
|
+
}));
|
|
1388
|
+
// Filter out entries with invalid signatures
|
|
1389
|
+
const verifiedEntries = signatureVerificationResults
|
|
1390
|
+
.filter(({ isValid }) => isValid)
|
|
1391
|
+
.map(({ entryData }) => entryData);
|
|
1392
|
+
if (verifiedEntries.length === 0) {
|
|
1393
|
+
this.logger.debug(`No entries with valid signatures for cached document ${docId}`);
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
// Parallel decryption - decrypt all entries concurrently
|
|
1397
|
+
const decryptionResults = await Promise.all(verifiedEntries.map(async (entryData) => {
|
|
1398
|
+
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
1399
|
+
return { entryData, decryptedPayload };
|
|
1400
|
+
}));
|
|
1401
|
+
// Collect change bytes and register automerge hash mappings
|
|
1402
|
+
const changeBytes = [];
|
|
1403
|
+
for (const { entryData, decryptedPayload } of decryptionResults) {
|
|
1404
|
+
changeBytes.push(decryptedPayload);
|
|
1405
|
+
// Register automerge hash -> entry ID mapping
|
|
1406
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
1407
|
+
if (parsed) {
|
|
1408
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
1013
1409
|
}
|
|
1014
|
-
// Continue with the next page using the returned cursor
|
|
1015
|
-
currentCursor = returnedCursor;
|
|
1016
1410
|
}
|
|
1017
|
-
|
|
1411
|
+
if (changeBytes.length === 0) {
|
|
1412
|
+
this.logger.debug(`No valid change bytes to apply for document ${docId}`);
|
|
1413
|
+
return null; // No changes
|
|
1414
|
+
}
|
|
1415
|
+
// Clone the cached document to avoid "outdated document" error
|
|
1416
|
+
// This happens when the document has been wrapped and returned to the user
|
|
1417
|
+
// Automerge marks documents as outdated when they're accessed, preventing direct mutation
|
|
1418
|
+
const clonedDoc = automerge_adapter_1.Automerge.clone(cachedDoc.doc);
|
|
1419
|
+
// Apply all changes at once (Automerge handles dependency ordering)
|
|
1420
|
+
const result = automerge_adapter_1.Automerge.applyChanges(clonedDoc, changeBytes);
|
|
1421
|
+
const updatedDoc = result[0];
|
|
1422
|
+
// Check if document actually changed
|
|
1423
|
+
const headsAfter = automerge_adapter_1.Automerge.getHeads(updatedDoc);
|
|
1424
|
+
const headsChanged = JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
|
|
1425
|
+
if (!headsChanged) {
|
|
1426
|
+
this.logger.debug(`Document ${docId} heads unchanged after applying new entries`);
|
|
1427
|
+
return null; // Document didn't actually change
|
|
1428
|
+
}
|
|
1429
|
+
this.logger.debug(`Document ${docId} changed: heads before=${JSON.stringify(headsBefore)}, after=${JSON.stringify(headsAfter)}`);
|
|
1430
|
+
// Update metadata
|
|
1431
|
+
const payload = updatedDoc;
|
|
1432
|
+
const lastEntry = entries[entries.length - 1];
|
|
1433
|
+
const lastModified = payload._lastModified ||
|
|
1434
|
+
(lastEntry ? lastEntry.createdAt : cachedDoc.lastModified);
|
|
1435
|
+
// Check if document was deleted
|
|
1436
|
+
const hasDeleteEntry = newEntryMetadata.some(em => em.entryType === "doc_delete");
|
|
1437
|
+
const isDeleted = hasDeleteEntry || cachedDoc.isDeleted;
|
|
1438
|
+
const updatedInternalDoc = {
|
|
1439
|
+
id: docId,
|
|
1440
|
+
doc: updatedDoc,
|
|
1441
|
+
createdAt: cachedDoc.createdAt,
|
|
1442
|
+
lastModified,
|
|
1443
|
+
decryptionKeyId: cachedDoc.decryptionKeyId,
|
|
1444
|
+
isDeleted,
|
|
1445
|
+
};
|
|
1446
|
+
// Update cache
|
|
1447
|
+
this.docCache.set(docId, updatedInternalDoc);
|
|
1448
|
+
return updatedInternalDoc;
|
|
1018
1449
|
}
|
|
1019
1450
|
/**
|
|
1020
1451
|
* Internal method to load a document from the content-addressed store
|
|
@@ -1022,25 +1453,25 @@ class BaseMindooDB {
|
|
|
1022
1453
|
async loadDocumentInternal(docId) {
|
|
1023
1454
|
// Check cache first
|
|
1024
1455
|
if (this.docCache.has(docId)) {
|
|
1025
|
-
|
|
1456
|
+
this.logger.debug(`Document ${docId} found in cache, returning cached version`);
|
|
1026
1457
|
return this.docCache.get(docId);
|
|
1027
1458
|
}
|
|
1028
|
-
|
|
1459
|
+
this.logger.debug(`===== Starting to load document ${docId} from store =====`);
|
|
1029
1460
|
// Get all entry metadata for this document
|
|
1030
1461
|
// TODO: Implement loading from last snapshot if available
|
|
1031
|
-
|
|
1032
|
-
const allEntryMetadata = await this.store
|
|
1033
|
-
|
|
1462
|
+
this.logger.debug(`Getting all entry hashes for document ${docId}`);
|
|
1463
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
1464
|
+
this.logger.debug(`Found ${allEntryMetadata.length} total entry hashes for document ${docId}`);
|
|
1034
1465
|
if (allEntryMetadata.length === 0) {
|
|
1035
|
-
|
|
1466
|
+
this.logger.debug(`No entry hashes found for document ${docId}, returning null`);
|
|
1036
1467
|
return null;
|
|
1037
1468
|
}
|
|
1038
1469
|
// Log all entry types
|
|
1039
1470
|
const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
|
|
1040
|
-
|
|
1471
|
+
this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
|
|
1041
1472
|
// Find the most recent snapshot (if any)
|
|
1042
1473
|
const snapshots = allEntryMetadata.filter(em => em.entryType === "doc_snapshot");
|
|
1043
|
-
|
|
1474
|
+
this.logger.debug(`Found ${snapshots.length} snapshot(s) for document ${docId}`);
|
|
1044
1475
|
let startFromSnapshot = false;
|
|
1045
1476
|
let snapshotMeta = null;
|
|
1046
1477
|
if (snapshots.length > 0) {
|
|
@@ -1048,29 +1479,29 @@ class BaseMindooDB {
|
|
|
1048
1479
|
snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
1049
1480
|
snapshotMeta = snapshots[0];
|
|
1050
1481
|
startFromSnapshot = true;
|
|
1051
|
-
|
|
1482
|
+
this.logger.debug(`Will start from snapshot ${snapshotMeta.id} created at ${snapshotMeta.createdAt}`);
|
|
1052
1483
|
}
|
|
1053
1484
|
else {
|
|
1054
|
-
|
|
1485
|
+
this.logger.debug(`No snapshot found, will start from scratch`);
|
|
1055
1486
|
}
|
|
1056
1487
|
// Get all entries (excluding snapshot entries - we'll handle delete separately)
|
|
1057
1488
|
// Include "doc_create", "doc_change", and "doc_delete" types as they all contain Automerge changes to apply
|
|
1058
1489
|
const entriesToLoad = startFromSnapshot
|
|
1059
1490
|
? allEntryMetadata.filter(em => (em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete") && em.createdAt > snapshotMeta.createdAt)
|
|
1060
1491
|
: allEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
|
|
1061
|
-
|
|
1492
|
+
this.logger.debug(`Will load ${entriesToLoad.length} entries for document ${docId} (after snapshot filter)`);
|
|
1062
1493
|
// Load the snapshot first if we have one
|
|
1063
1494
|
let doc = undefined;
|
|
1064
1495
|
if (startFromSnapshot && snapshotMeta) {
|
|
1065
|
-
|
|
1496
|
+
this.logger.debug(`Loading snapshot for document ${docId}`);
|
|
1066
1497
|
const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
|
|
1067
|
-
|
|
1498
|
+
this.logger.debug(`Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
|
|
1068
1499
|
if (snapshotEntries.length > 0) {
|
|
1069
1500
|
const snapshotData = snapshotEntries[0];
|
|
1070
1501
|
// Admin-only mode: only accept snapshots signed by admin
|
|
1071
1502
|
let isValid = false;
|
|
1072
1503
|
if (this._isAdminOnlyDb && snapshotData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1073
|
-
|
|
1504
|
+
this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
1074
1505
|
}
|
|
1075
1506
|
else {
|
|
1076
1507
|
// Verify signature against the encrypted snapshot (no decryption needed)
|
|
@@ -1078,21 +1509,21 @@ class BaseMindooDB {
|
|
|
1078
1509
|
isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
|
|
1079
1510
|
}
|
|
1080
1511
|
if (!isValid) {
|
|
1081
|
-
|
|
1512
|
+
this.logger.warn(`Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
|
|
1082
1513
|
// Fall back to loading from scratch
|
|
1083
1514
|
startFromSnapshot = false;
|
|
1084
1515
|
}
|
|
1085
1516
|
else {
|
|
1086
|
-
|
|
1517
|
+
this.logger.debug(`Snapshot signature valid, decrypting snapshot`);
|
|
1087
1518
|
// Decrypt snapshot (only after signature verification passes)
|
|
1088
1519
|
const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
|
|
1089
|
-
|
|
1520
|
+
this.logger.debug(`Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
|
|
1090
1521
|
// Load snapshot using Automerge.load()
|
|
1091
1522
|
// This deserializes a full document snapshot from binary data
|
|
1092
1523
|
// According to Automerge docs: load() is equivalent to init() followed by loadIncremental()
|
|
1093
|
-
|
|
1094
|
-
doc = Automerge.load(decryptedSnapshot);
|
|
1095
|
-
|
|
1524
|
+
this.logger.debug(`Loading snapshot into Automerge document`);
|
|
1525
|
+
doc = automerge_adapter_1.Automerge.load(decryptedSnapshot);
|
|
1526
|
+
this.logger.debug(`Successfully loaded snapshot, document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1096
1527
|
// Register the snapshot's automerge hash -> entry ID mapping
|
|
1097
1528
|
const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
|
|
1098
1529
|
if (parsed) {
|
|
@@ -1103,96 +1534,119 @@ class BaseMindooDB {
|
|
|
1103
1534
|
}
|
|
1104
1535
|
// If we don't have a snapshot, start from scratch
|
|
1105
1536
|
if (!doc) {
|
|
1106
|
-
|
|
1107
|
-
doc = Automerge.init();
|
|
1108
|
-
|
|
1537
|
+
this.logger.debug(`Initializing new Automerge document for ${docId}`);
|
|
1538
|
+
doc = automerge_adapter_1.Automerge.init();
|
|
1539
|
+
this.logger.debug(`Initialized empty document, heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1109
1540
|
}
|
|
1110
1541
|
// Sort entries by timestamp
|
|
1111
1542
|
entriesToLoad.sort((a, b) => a.createdAt - b.createdAt);
|
|
1112
|
-
|
|
1543
|
+
this.logger.debug(`Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
|
|
1113
1544
|
// Load and apply all entries
|
|
1114
|
-
|
|
1545
|
+
this.logger.debug(`Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
|
|
1115
1546
|
const entries = await this.store.getEntries(entriesToLoad.map(em => em.id));
|
|
1116
|
-
|
|
1117
|
-
|
|
1547
|
+
this.logger.debug(`Retrieved ${entries.length} entries from store for document ${docId}`);
|
|
1548
|
+
this.logger.debug(`Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? 'starting from snapshot' : 'starting from scratch'})`);
|
|
1118
1549
|
// Log current document state before applying entries
|
|
1119
|
-
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1123
|
-
const entryData = entries[i];
|
|
1124
|
-
console.log(`[BaseMindooDB] ===== Processing entry ${i + 1}/${entries.length} for document ${docId} =====`);
|
|
1125
|
-
console.log(`[BaseMindooDB] Entry id: ${entryData.id}`);
|
|
1126
|
-
console.log(`[BaseMindooDB] Entry type: ${entryData.entryType}`);
|
|
1127
|
-
console.log(`[BaseMindooDB] Entry createdAt: ${entryData.createdAt}`);
|
|
1128
|
-
console.log(`[BaseMindooDB] Entry dependencies: ${JSON.stringify(entryData.dependencyIds || [])}`);
|
|
1129
|
-
console.log(`[BaseMindooDB] Entry payload size: ${entryData.encryptedData.length} bytes`);
|
|
1130
|
-
// Admin-only mode: only accept entries signed by the admin key
|
|
1131
|
-
// This prevents recursion and ensures security for the directory database
|
|
1550
|
+
this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1551
|
+
// Filter entries for admin-only mode first
|
|
1552
|
+
const validEntries = entries.filter(entryData => {
|
|
1132
1553
|
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1554
|
+
this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
1555
|
+
return false;
|
|
1135
1556
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1557
|
+
return true;
|
|
1558
|
+
});
|
|
1559
|
+
if (validEntries.length === 0) {
|
|
1560
|
+
this.logger.debug(`No valid entries to process for document ${docId}`);
|
|
1561
|
+
}
|
|
1562
|
+
else {
|
|
1563
|
+
// Batch signature verification with key caching
|
|
1564
|
+
// Group entries by public key to import each key only once
|
|
1565
|
+
this.logger.debug(`Verifying ${validEntries.length} signatures in parallel with key caching`);
|
|
1566
|
+
const entriesByPublicKey = new Map();
|
|
1567
|
+
for (const entryData of validEntries) {
|
|
1568
|
+
if (!entriesByPublicKey.has(entryData.createdByPublicKey)) {
|
|
1569
|
+
entriesByPublicKey.set(entryData.createdByPublicKey, []);
|
|
1570
|
+
}
|
|
1571
|
+
entriesByPublicKey.get(entryData.createdByPublicKey).push(entryData);
|
|
1143
1572
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
console.log(`[BaseMindooDB] Decrypted payload length: ${decryptedPayload.length} bytes`);
|
|
1155
|
-
const currentDoc = doc;
|
|
1156
|
-
const result = Automerge.applyChanges(currentDoc, [decryptedPayload]);
|
|
1157
|
-
doc = result[0];
|
|
1158
|
-
console.log(`[BaseMindooDB] Successfully applied entry ${i + 1}/${entries.length}`);
|
|
1159
|
-
console.log(`[BaseMindooDB] Document state after applying entry ${i + 1}: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1160
|
-
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
1161
|
-
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
1162
|
-
if (parsed) {
|
|
1163
|
-
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
1573
|
+
// Import all unique public keys in parallel (with caching)
|
|
1574
|
+
const keyImportResults = await Promise.all(Array.from(entriesByPublicKey.keys()).map(async (publicKey) => {
|
|
1575
|
+
const cryptoKey = await this.getOrImportPublicKey(publicKey);
|
|
1576
|
+
return { publicKey, cryptoKey };
|
|
1577
|
+
}));
|
|
1578
|
+
// Create a map of public key -> CryptoKey for quick lookup
|
|
1579
|
+
const keyMap = new Map();
|
|
1580
|
+
for (const { publicKey, cryptoKey } of keyImportResults) {
|
|
1581
|
+
if (cryptoKey) {
|
|
1582
|
+
keyMap.set(publicKey, cryptoKey);
|
|
1164
1583
|
}
|
|
1165
1584
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1585
|
+
// Verify all signatures in parallel using cached keys
|
|
1586
|
+
const signatureVerificationResults = await Promise.all(validEntries.map(async (entryData) => {
|
|
1587
|
+
const cryptoKey = keyMap.get(entryData.createdByPublicKey);
|
|
1588
|
+
if (!cryptoKey) {
|
|
1589
|
+
// Key was not trusted or failed to import
|
|
1590
|
+
return { entryData, isValid: false };
|
|
1591
|
+
}
|
|
1592
|
+
const isValid = await this.verifySignatureWithKey(cryptoKey, entryData.encryptedData, entryData.signature);
|
|
1593
|
+
return { entryData, isValid };
|
|
1594
|
+
}));
|
|
1595
|
+
// Filter out entries with invalid signatures
|
|
1596
|
+
const verifiedEntries = signatureVerificationResults
|
|
1597
|
+
.filter(({ isValid }) => {
|
|
1598
|
+
if (!isValid) {
|
|
1599
|
+
this.logger.warn(`Invalid signature for entry, skipping`);
|
|
1600
|
+
}
|
|
1601
|
+
return isValid;
|
|
1602
|
+
})
|
|
1603
|
+
.map(({ entryData }) => entryData);
|
|
1604
|
+
if (verifiedEntries.length === 0) {
|
|
1605
|
+
this.logger.debug(`No entries with valid signatures for document ${docId}`);
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
// Parallel decryption - decrypt all entries concurrently
|
|
1609
|
+
// Automerge handles dependency buffering internally, so we can decrypt all in parallel
|
|
1610
|
+
this.logger.debug(`Decrypting ${verifiedEntries.length} entries in parallel`);
|
|
1611
|
+
const decryptionResults = await Promise.all(verifiedEntries.map(async (entryData) => {
|
|
1612
|
+
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
1613
|
+
return { entryData, decryptedPayload };
|
|
1614
|
+
}));
|
|
1615
|
+
// Collect change bytes and register automerge hash mappings
|
|
1616
|
+
const changeBytes = [];
|
|
1617
|
+
for (const { entryData, decryptedPayload } of decryptionResults) {
|
|
1618
|
+
changeBytes.push(decryptedPayload);
|
|
1619
|
+
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
1620
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
1621
|
+
if (parsed) {
|
|
1622
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
// Batch apply all changes at once - Automerge handles dependency ordering
|
|
1626
|
+
if (changeBytes.length > 0) {
|
|
1627
|
+
this.logger.debug(`Applying ${changeBytes.length} changes to document ${docId} using batch applyChanges`);
|
|
1628
|
+
try {
|
|
1629
|
+
const result = automerge_adapter_1.Automerge.applyChanges(doc, changeBytes);
|
|
1630
|
+
doc = result[0];
|
|
1631
|
+
this.logger.debug(`Successfully applied ${changeBytes.length} changes to document ${docId}`);
|
|
1632
|
+
this.logger.debug(`Document state after applying changes: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1633
|
+
}
|
|
1634
|
+
catch (error) {
|
|
1635
|
+
this.logger.error(`Error applying changes to document ${docId}:`, error);
|
|
1636
|
+
this.logger.error(`Number of changes: ${changeBytes.length}`);
|
|
1637
|
+
throw error;
|
|
1638
|
+
}
|
|
1184
1639
|
}
|
|
1185
|
-
throw error;
|
|
1186
1640
|
}
|
|
1187
1641
|
}
|
|
1188
1642
|
// Extract metadata from document (doc is guaranteed to be defined at this point)
|
|
1189
|
-
|
|
1190
|
-
|
|
1643
|
+
this.logger.debug(`All entries applied successfully for document ${docId}`);
|
|
1644
|
+
this.logger.debug(`Final document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1191
1645
|
const payload = doc;
|
|
1192
1646
|
// Check if document was deleted by looking for a "doc_delete" type entry
|
|
1193
1647
|
const hasDeleteEntry = allEntryMetadata.some(em => em.entryType === "doc_delete");
|
|
1194
1648
|
const isDeleted = hasDeleteEntry;
|
|
1195
|
-
|
|
1649
|
+
this.logger.debug(`Document ${docId} isDeleted: ${isDeleted}`);
|
|
1196
1650
|
const decryptionKeyId = payload._decryptionKeyId || "default";
|
|
1197
1651
|
// Get lastModified from payload, or use the timestamp of the last entry
|
|
1198
1652
|
const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
@@ -1201,7 +1655,7 @@ class BaseMindooDB {
|
|
|
1201
1655
|
// Get createdAt from the first entry
|
|
1202
1656
|
const firstEntry = allEntryMetadata.length > 0 ? allEntryMetadata[0] : null;
|
|
1203
1657
|
const createdAt = firstEntry ? firstEntry.createdAt : lastModified;
|
|
1204
|
-
|
|
1658
|
+
this.logger.debug(`Document ${docId} metadata: createdAt=${createdAt}, lastModified=${lastModified}, decryptionKeyId=${decryptionKeyId}`);
|
|
1205
1659
|
const internalDoc = {
|
|
1206
1660
|
id: docId,
|
|
1207
1661
|
doc: doc, // doc is guaranteed to be defined at this point
|
|
@@ -1212,9 +1666,53 @@ class BaseMindooDB {
|
|
|
1212
1666
|
};
|
|
1213
1667
|
// Update cache
|
|
1214
1668
|
this.docCache.set(docId, internalDoc);
|
|
1215
|
-
|
|
1669
|
+
this.logger.debug(`===== Successfully loaded document ${docId} and cached it =====`);
|
|
1216
1670
|
return internalDoc;
|
|
1217
1671
|
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Convert an Automerge document to a plain JS object, converting Text objects to strings.
|
|
1674
|
+
* If using native Automerge (react-native-automerge-generated), this uses the native
|
|
1675
|
+
* materialize() method which properly converts Text objects to strings.
|
|
1676
|
+
* Falls back to direct access if native backend is not available.
|
|
1677
|
+
*/
|
|
1678
|
+
convertAutomergeToJS(doc) {
|
|
1679
|
+
// Check if this document has a native Automerge handle attached
|
|
1680
|
+
// The native implementation attaches metadata with Symbol.for('_am_meta')
|
|
1681
|
+
const STATE = Symbol.for('_am_meta');
|
|
1682
|
+
const meta = doc[STATE];
|
|
1683
|
+
if (meta && meta.handle && typeof meta.handle.materialize === 'function') {
|
|
1684
|
+
// Use native materialize() which properly converts Text objects to strings
|
|
1685
|
+
try {
|
|
1686
|
+
const materialized = meta.handle.materialize('/');
|
|
1687
|
+
return materialized;
|
|
1688
|
+
}
|
|
1689
|
+
catch (error) {
|
|
1690
|
+
console.error('[MindooDB] Failed to materialize document:', error);
|
|
1691
|
+
// Fall through to direct access
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
// Fallback: direct access (for WASM or if native fails)
|
|
1695
|
+
const result = {};
|
|
1696
|
+
const keys = Object.keys(doc);
|
|
1697
|
+
for (const key of keys) {
|
|
1698
|
+
const value = doc[key];
|
|
1699
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Uint8Array)) {
|
|
1700
|
+
result[key] = this.convertAutomergeToJS(value);
|
|
1701
|
+
}
|
|
1702
|
+
else if (Array.isArray(value)) {
|
|
1703
|
+
result[key] = value.map(item => {
|
|
1704
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Uint8Array)) {
|
|
1705
|
+
return this.convertAutomergeToJS(item);
|
|
1706
|
+
}
|
|
1707
|
+
return item;
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
else {
|
|
1711
|
+
result[key] = value;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return result;
|
|
1715
|
+
}
|
|
1218
1716
|
/**
|
|
1219
1717
|
* Wrap an internal document in the MindooDoc interface.
|
|
1220
1718
|
* This is the read-only wrapper - write methods throw errors.
|
|
@@ -1247,7 +1745,11 @@ class BaseMindooDB {
|
|
|
1247
1745
|
getCreatedAt: () => internalDoc.createdAt,
|
|
1248
1746
|
getLastModified: () => internalDoc.lastModified,
|
|
1249
1747
|
isDeleted: () => internalDoc.isDeleted,
|
|
1250
|
-
getData: () =>
|
|
1748
|
+
getData: () => {
|
|
1749
|
+
// Convert Automerge document to plain JS object, converting Text objects to strings
|
|
1750
|
+
const jsDoc = this.convertAutomergeToJS(internalDoc.doc);
|
|
1751
|
+
return createReadOnlyProxy(jsDoc);
|
|
1752
|
+
},
|
|
1251
1753
|
// ========== Attachment Write Methods ==========
|
|
1252
1754
|
// These throw errors in the read-only wrapper
|
|
1253
1755
|
addAttachment: async () => {
|
|
@@ -1299,12 +1801,12 @@ class BaseMindooDB {
|
|
|
1299
1801
|
* Internal method to fetch and concatenate all chunks for an attachment.
|
|
1300
1802
|
*/
|
|
1301
1803
|
async getAttachmentInternal(docId, attachmentId) {
|
|
1302
|
-
|
|
1804
|
+
this.logger.debug(`Getting attachment ${attachmentId} from document ${docId}`);
|
|
1303
1805
|
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
1304
1806
|
const store = this.getEffectiveAttachmentStore();
|
|
1305
1807
|
// Resolve dependency chain to get all chunk IDs in order (oldest first)
|
|
1306
1808
|
const chunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
|
|
1307
|
-
|
|
1809
|
+
this.logger.debug(`Resolved ${chunkIds.length} chunks for attachment ${attachmentId}`);
|
|
1308
1810
|
// Fetch all chunks
|
|
1309
1811
|
const chunks = await store.getEntries(chunkIds);
|
|
1310
1812
|
// Verify signatures, decrypt, and collect plaintext chunks
|
|
@@ -1332,14 +1834,14 @@ class BaseMindooDB {
|
|
|
1332
1834
|
result.set(chunk, offset);
|
|
1333
1835
|
offset += chunk.length;
|
|
1334
1836
|
}
|
|
1335
|
-
|
|
1837
|
+
this.logger.debug(`Retrieved attachment ${attachmentId}: ${result.length} bytes`);
|
|
1336
1838
|
return result;
|
|
1337
1839
|
}
|
|
1338
1840
|
/**
|
|
1339
1841
|
* Internal method to get a byte range from an attachment.
|
|
1340
1842
|
*/
|
|
1341
1843
|
async getAttachmentRangeInternal(docId, attachmentId, startByte, endByte) {
|
|
1342
|
-
|
|
1844
|
+
this.logger.debug(`Getting attachment ${attachmentId} range [${startByte}, ${endByte}) from document ${docId}`);
|
|
1343
1845
|
if (startByte < 0 || endByte <= startByte) {
|
|
1344
1846
|
throw new Error(`Invalid byte range: [${startByte}, ${endByte})`);
|
|
1345
1847
|
}
|
|
@@ -1388,14 +1890,14 @@ class BaseMindooDB {
|
|
|
1388
1890
|
resultOffset += bytesToCopy;
|
|
1389
1891
|
bytesRemaining -= bytesToCopy;
|
|
1390
1892
|
}
|
|
1391
|
-
|
|
1893
|
+
this.logger.debug(`Retrieved attachment ${attachmentId} range: ${result.length} bytes`);
|
|
1392
1894
|
return result;
|
|
1393
1895
|
}
|
|
1394
1896
|
/**
|
|
1395
1897
|
* Internal async generator to stream attachment data.
|
|
1396
1898
|
*/
|
|
1397
1899
|
async *streamAttachmentInternal(docId, attachmentId, startOffset) {
|
|
1398
|
-
|
|
1900
|
+
this.logger.debug(`Streaming attachment ${attachmentId} from offset ${startOffset}`);
|
|
1399
1901
|
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
1400
1902
|
const store = this.getEffectiveAttachmentStore();
|
|
1401
1903
|
const chunkSize = this.chunkSizeBytes;
|
|
@@ -1426,7 +1928,7 @@ class BaseMindooDB {
|
|
|
1426
1928
|
yield plaintext;
|
|
1427
1929
|
}
|
|
1428
1930
|
}
|
|
1429
|
-
|
|
1931
|
+
this.logger.debug(`Finished streaming attachment ${attachmentId}`);
|
|
1430
1932
|
}
|
|
1431
1933
|
/**
|
|
1432
1934
|
* Get the effective attachment store (attachment store if configured, otherwise doc store).
|
|
@@ -1438,7 +1940,7 @@ class BaseMindooDB {
|
|
|
1438
1940
|
* Internal method to add an attachment by chunking the file and storing chunks.
|
|
1439
1941
|
*/
|
|
1440
1942
|
async addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, createdAt) {
|
|
1441
|
-
|
|
1943
|
+
this.logger.debug(`Adding attachment to document ${docId}: ${fileName} (${fileData.length} bytes)`);
|
|
1442
1944
|
const store = this.getEffectiveAttachmentStore();
|
|
1443
1945
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
1444
1946
|
const attachmentId = (0, idGeneration_1.generateFileUuid7)();
|
|
@@ -1477,7 +1979,7 @@ class BaseMindooDB {
|
|
|
1477
1979
|
}
|
|
1478
1980
|
// Store all chunks
|
|
1479
1981
|
await store.putEntries(chunks);
|
|
1480
|
-
|
|
1982
|
+
this.logger.debug(`Stored ${chunks.length} chunks for attachment ${attachmentId}`);
|
|
1481
1983
|
// Create attachment reference
|
|
1482
1984
|
const ref = {
|
|
1483
1985
|
attachmentId,
|
|
@@ -1496,7 +1998,7 @@ class BaseMindooDB {
|
|
|
1496
1998
|
* Memory efficient - processes data chunk by chunk without loading entire file into memory.
|
|
1497
1999
|
*/
|
|
1498
2000
|
async addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, createdAt) {
|
|
1499
|
-
|
|
2001
|
+
this.logger.debug(`Adding streaming attachment to document ${docId}: ${fileName}`);
|
|
1500
2002
|
const store = this.getEffectiveAttachmentStore();
|
|
1501
2003
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
1502
2004
|
const attachmentId = (0, idGeneration_1.generateFileUuid7)();
|
|
@@ -1561,7 +2063,7 @@ class BaseMindooDB {
|
|
|
1561
2063
|
lastChunkId = await storeChunk(buffer);
|
|
1562
2064
|
totalSize += buffer.length;
|
|
1563
2065
|
}
|
|
1564
|
-
|
|
2066
|
+
this.logger.debug(`Stored ${chunkCount} chunks for streaming attachment ${attachmentId} (${totalSize} bytes)`);
|
|
1565
2067
|
// Create attachment reference
|
|
1566
2068
|
const ref = {
|
|
1567
2069
|
attachmentId,
|
|
@@ -1579,7 +2081,7 @@ class BaseMindooDB {
|
|
|
1579
2081
|
* Internal method to append data to an existing attachment.
|
|
1580
2082
|
*/
|
|
1581
2083
|
async appendToAttachmentInternal(docId, attachmentId, decryptionKeyId, prevLastChunkId, data, createdAt) {
|
|
1582
|
-
|
|
2084
|
+
this.logger.debug(`Appending ${data.length} bytes to attachment ${attachmentId}`);
|
|
1583
2085
|
const store = this.getEffectiveAttachmentStore();
|
|
1584
2086
|
const currentUser = await this.tenant.getCurrentUserId();
|
|
1585
2087
|
// Chunk the data
|
|
@@ -1617,7 +2119,7 @@ class BaseMindooDB {
|
|
|
1617
2119
|
}
|
|
1618
2120
|
// Store all chunks
|
|
1619
2121
|
await store.putEntries(chunks);
|
|
1620
|
-
|
|
2122
|
+
this.logger.debug(`Appended ${chunks.length} chunks to attachment ${attachmentId}`);
|
|
1621
2123
|
return {
|
|
1622
2124
|
lastChunkId,
|
|
1623
2125
|
sizeIncrease: data.length,
|
|
@@ -1639,28 +2141,17 @@ class BaseMindooDB {
|
|
|
1639
2141
|
if (this.store.getId() !== remoteStore.getId()) {
|
|
1640
2142
|
throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
|
|
1641
2143
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
const newEntryMetadata = await remoteStore.findNewEntries(localEntryIds);
|
|
1648
|
-
console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries in remote store`);
|
|
1649
|
-
if (newEntryMetadata.length === 0) {
|
|
1650
|
-
console.log(`[BaseMindooDB] No new entries to pull`);
|
|
2144
|
+
this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}`);
|
|
2145
|
+
const transferred = await this.syncEntriesFromStore(remoteStore, this.store);
|
|
2146
|
+
this.logger.debug(`Transferred ${transferred} entries from remote store`);
|
|
2147
|
+
if (transferred === 0) {
|
|
2148
|
+
this.logger.debug(`No new entries to pull`);
|
|
1651
2149
|
return;
|
|
1652
2150
|
}
|
|
1653
|
-
// Get the full entries from the remote store
|
|
1654
|
-
const newEntries = await remoteStore.getEntries(newEntryMetadata.map(em => em.id));
|
|
1655
|
-
console.log(`[BaseMindooDB] Retrieved ${newEntries.length} entries from remote store`);
|
|
1656
|
-
// Store all entries in our local store
|
|
1657
|
-
// The putEntries method handles deduplication (no-op if entry already exists)
|
|
1658
|
-
await this.store.putEntries(newEntries);
|
|
1659
|
-
console.log(`[BaseMindooDB] Stored ${newEntries.length} entries in local store`);
|
|
1660
2151
|
// Sync the local store to process the new entries
|
|
1661
2152
|
// This will update the index, cache, and processedEntryIds
|
|
1662
2153
|
await this.syncStoreChanges();
|
|
1663
|
-
|
|
2154
|
+
this.logger.info(`Pull complete, synced ${transferred} entries`);
|
|
1664
2155
|
}
|
|
1665
2156
|
/**
|
|
1666
2157
|
* Push changes to a remote content-addressed store.
|
|
@@ -1677,24 +2168,14 @@ class BaseMindooDB {
|
|
|
1677
2168
|
if (this.store.getId() !== remoteStore.getId()) {
|
|
1678
2169
|
throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
|
|
1679
2170
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const newEntryMetadata = await this.store.findNewEntries(remoteEntryIds);
|
|
1686
|
-
console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries to push`);
|
|
1687
|
-
if (newEntryMetadata.length === 0) {
|
|
1688
|
-
console.log(`[BaseMindooDB] No new entries to push`);
|
|
2171
|
+
this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}`);
|
|
2172
|
+
const transferred = await this.syncEntriesFromStore(this.store, remoteStore);
|
|
2173
|
+
this.logger.debug(`Transferred ${transferred} entries to remote store`);
|
|
2174
|
+
if (transferred === 0) {
|
|
2175
|
+
this.logger.debug(`No new entries to push`);
|
|
1689
2176
|
return;
|
|
1690
2177
|
}
|
|
1691
|
-
|
|
1692
|
-
const newEntries = await this.store.getEntries(newEntryMetadata.map(em => em.id));
|
|
1693
|
-
console.log(`[BaseMindooDB] Retrieved ${newEntries.length} entries from local store`);
|
|
1694
|
-
// Store all entries in the remote store
|
|
1695
|
-
// The putEntries method handles deduplication (no-op if entry already exists)
|
|
1696
|
-
await remoteStore.putEntries(newEntries);
|
|
1697
|
-
console.log(`[BaseMindooDB] Pushed ${newEntries.length} entries to remote store`);
|
|
2178
|
+
this.logger.info(`Pushed ${transferred} entries to remote store`);
|
|
1698
2179
|
}
|
|
1699
2180
|
}
|
|
1700
2181
|
exports.BaseMindooDB = BaseMindooDB;
|