mindoodb 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-server.md +1001 -0
- package/README.md +89 -28
- 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 +30 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +152 -5
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +17 -2
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +129 -0
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +22 -2
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +110 -0
- 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 +65 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +741 -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/cache/IndexedDBLocalCacheStore.d.ts +21 -0
- package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -0
- package/dist/browser/cache/IndexedDBLocalCacheStore.js +106 -0
- package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -0
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +21 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/core/BaseMindooDB.d.ts +115 -13
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +1232 -269
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +69 -6
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +302 -109
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +13 -10
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +144 -27
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +27 -29
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +185 -112
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/MindooDBServerAdmin.d.ts +238 -0
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -0
- package/dist/core/MindooDBServerAdmin.js +345 -0
- package/dist/core/MindooDBServerAdmin.js.map +1 -0
- 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 +45 -3
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +184 -23
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/MaterializationPlanner.d.ts +49 -0
- package/dist/core/appendonlystores/MaterializationPlanner.d.ts.map +1 -0
- package/dist/core/appendonlystores/MaterializationPlanner.js +325 -0
- package/dist/core/appendonlystores/MaterializationPlanner.js.map +1 -0
- 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/NetworkTransport.d.ts +37 -2
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +12 -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 +206 -0
- 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/cache/CacheManager.d.ts +75 -0
- package/dist/core/cache/CacheManager.d.ts.map +1 -0
- package/dist/core/cache/CacheManager.js +101 -0
- package/dist/core/cache/CacheManager.js.map +1 -0
- package/dist/core/cache/EncryptedLocalCacheStore.d.ts +41 -0
- package/dist/core/cache/EncryptedLocalCacheStore.d.ts.map +1 -0
- package/dist/core/cache/EncryptedLocalCacheStore.js +120 -0
- package/dist/core/cache/EncryptedLocalCacheStore.js.map +1 -0
- package/dist/core/cache/LocalCacheStore.d.ts +50 -0
- package/dist/core/cache/LocalCacheStore.d.ts.map +1 -0
- package/dist/core/cache/LocalCacheStore.js +39 -0
- package/dist/core/cache/LocalCacheStore.js.map +1 -0
- 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/crypto/privateKeyEncryption.d.ts +5 -0
- package/dist/core/crypto/privateKeyEncryption.d.ts.map +1 -0
- package/dist/core/crypto/privateKeyEncryption.js +87 -0
- package/dist/core/crypto/privateKeyEncryption.js.map +1 -0
- package/dist/core/index.d.ts +18 -6
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +42 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +4 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +39 -11
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +3 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualView.d.ts +43 -0
- package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualView.js +173 -0
- package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +7 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +28 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
- package/dist/core/keys/KeyBag.d.ts +56 -12
- package/dist/core/keys/KeyBag.d.ts.map +1 -1
- package/dist/core/keys/KeyBag.js +156 -70
- 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 +17 -0
- package/dist/core/keys/KeyContext.js.map +1 -0
- package/dist/core/types.d.ts +512 -60
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +9 -1
- 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/core/utils/idGeneration.d.ts.map +1 -1
- package/dist/core/utils/idGeneration.js +37 -3
- package/dist/core/utils/idGeneration.js.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +544 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +1358 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
- package/dist/node/attachments/writeAttachmentToFile.d.ts +15 -0
- package/dist/node/attachments/writeAttachmentToFile.d.ts.map +1 -0
- package/dist/node/attachments/writeAttachmentToFile.js +73 -0
- package/dist/node/attachments/writeAttachmentToFile.js.map +1 -0
- package/dist/node/cache/FileSystemLocalCacheStore.d.ts +17 -0
- package/dist/node/cache/FileSystemLocalCacheStore.d.ts.map +1 -0
- package/dist/node/cache/FileSystemLocalCacheStore.js +128 -0
- package/dist/node/cache/FileSystemLocalCacheStore.js.map +1 -0
- package/dist/node/cli/change-identity-password.d.ts +7 -0
- package/dist/node/cli/change-identity-password.d.ts.map +1 -0
- package/dist/node/cli/change-identity-password.js +67 -0
- package/dist/node/cli/change-identity-password.js.map +1 -0
- package/dist/node/cli/cli-utils.d.ts +16 -0
- package/dist/node/cli/cli-utils.d.ts.map +1 -0
- package/dist/node/cli/cli-utils.js +113 -0
- package/dist/node/cli/cli-utils.js.map +1 -0
- package/dist/node/cli/identity-export-public.d.ts +4 -0
- package/dist/node/cli/identity-export-public.d.ts.map +1 -0
- package/dist/node/cli/identity-export-public.js +55 -0
- package/dist/node/cli/identity-export-public.js.map +1 -0
- package/dist/node/cli/identity-info.d.ts +4 -0
- package/dist/node/cli/identity-info.d.ts.map +1 -0
- package/dist/node/cli/identity-info.js +57 -0
- package/dist/node/cli/identity-info.js.map +1 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +8 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/server/CapabilityMatcher.d.ts +45 -0
- package/dist/node/server/CapabilityMatcher.d.ts.map +1 -0
- package/dist/node/server/CapabilityMatcher.js +117 -0
- package/dist/node/server/CapabilityMatcher.js.map +1 -0
- package/dist/node/server/MindooDBServer.d.ts +87 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -0
- package/dist/node/server/MindooDBServer.js +1040 -0
- package/dist/node/server/MindooDBServer.js.map +1 -0
- package/dist/node/server/ServerSync.d.ts +41 -0
- package/dist/node/server/ServerSync.d.ts.map +1 -0
- package/dist/node/server/ServerSync.js +187 -0
- package/dist/node/server/ServerSync.js.map +1 -0
- package/dist/node/server/StoreFactory.d.ts +45 -0
- package/dist/node/server/StoreFactory.d.ts.map +1 -0
- package/dist/node/server/StoreFactory.js +89 -0
- package/dist/node/server/StoreFactory.js.map +1 -0
- package/dist/node/server/SystemAdminAuth.d.ts +65 -0
- package/dist/node/server/SystemAdminAuth.d.ts.map +1 -0
- package/dist/node/server/SystemAdminAuth.js +200 -0
- package/dist/node/server/SystemAdminAuth.js.map +1 -0
- package/dist/node/server/SystemIpAllowlist.d.ts +21 -0
- package/dist/node/server/SystemIpAllowlist.d.ts.map +1 -0
- package/dist/node/server/SystemIpAllowlist.js +210 -0
- package/dist/node/server/SystemIpAllowlist.js.map +1 -0
- package/dist/node/server/TenantManager.d.ts +85 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -0
- package/dist/node/server/TenantManager.js +493 -0
- package/dist/node/server/TenantManager.js.map +1 -0
- package/dist/node/server/add-to-network.d.ts +20 -0
- package/dist/node/server/add-to-network.d.ts.map +1 -0
- package/dist/node/server/add-to-network.js +275 -0
- package/dist/node/server/add-to-network.js.map +1 -0
- package/dist/node/server/config.d.ts +51 -0
- package/dist/node/server/config.d.ts.map +1 -0
- package/dist/node/server/config.js +218 -0
- package/dist/node/server/config.js.map +1 -0
- package/dist/node/server/resolveServerPassword.d.ts +7 -0
- package/dist/node/server/resolveServerPassword.d.ts.map +1 -0
- package/dist/node/server/resolveServerPassword.js +25 -0
- package/dist/node/server/resolveServerPassword.js.map +1 -0
- package/dist/node/server/server.d.ts +22 -0
- package/dist/node/server/server.d.ts.map +1 -0
- package/dist/node/server/server.js +225 -0
- package/dist/node/server/server.js.map +1 -0
- package/dist/node/server/serverinit.d.ts +21 -0
- package/dist/node/server/serverinit.d.ts.map +1 -0
- package/dist/node/server/serverinit.js +317 -0
- package/dist/node/server/serverinit.js.map +1 -0
- package/dist/node/server/types.d.ts +162 -0
- package/dist/node/server/types.d.ts.map +1 -0
- package/dist/node/server/types.js +24 -0
- package/dist/node/server/types.js.map +1 -0
- package/dist/node/server/validation.d.ts +47 -0
- package/dist/node/server/validation.d.ts.map +1 -0
- package/dist/node/server/validation.js +114 -0
- package/dist/node/server/validation.js.map +1 -0
- package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts +48 -0
- package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts.map +1 -0
- package/dist/reactnative/cache/MMKVLocalCacheStore.js +113 -0
- package/dist/reactnative/cache/MMKVLocalCacheStore.js.map +1 -0
- 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 +9 -0
- package/dist/reactnative/index.d.ts.map +1 -0
- package/dist/reactnative/index.js +41 -0
- package/dist/reactnative/index.js.map +1 -0
- package/package.json +63 -7
- 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,41 +1,13 @@
|
|
|
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");
|
|
10
|
+
const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
|
|
39
11
|
const idGeneration_1 = require("./utils/idGeneration");
|
|
40
12
|
const errors_1 = require("./errors");
|
|
41
13
|
const logging_1 = require("./logging");
|
|
@@ -43,6 +15,10 @@ const logging_1 = require("./logging");
|
|
|
43
15
|
* Default chunk size for attachments: 256KB
|
|
44
16
|
*/
|
|
45
17
|
const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
|
|
18
|
+
const DEFAULT_SNAPSHOT_MIN_CHANGES = 100;
|
|
19
|
+
const DEFAULT_SNAPSHOT_COOLDOWN_MS = 10 * 60 * 1000;
|
|
20
|
+
const DEFAULT_MAX_CACHED_DOCS = 128;
|
|
21
|
+
const DEFAULT_ITERATE_PREFETCH_WINDOW_DOCS = 0;
|
|
46
22
|
/**
|
|
47
23
|
* BaseMindooDB is a platform-agnostic implementation of MindooDB
|
|
48
24
|
* that works in both browser and server environments.
|
|
@@ -63,27 +39,40 @@ const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
|
|
|
63
39
|
* - Automerge.applyChanges(doc, changes) - verify method signature
|
|
64
40
|
*/
|
|
65
41
|
class BaseMindooDB {
|
|
66
|
-
constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false, logger, performanceCallback) {
|
|
67
|
-
// Internal index: sorted
|
|
68
|
-
//
|
|
42
|
+
constructor(tenant, store, attachmentStore, attachmentConfig, documentCacheConfig, snapshotConfig, adminOnlyDb = false, logger, performanceCallback) {
|
|
43
|
+
// Internal changefeed index: sorted by (changeSeq, docId) for deterministic iteration.
|
|
44
|
+
// lastModified remains available for UX metadata but is not the primary cursor key.
|
|
69
45
|
this.index = [];
|
|
70
46
|
// Lookup map for O(1) access to index entries by docId
|
|
71
47
|
this.indexLookup = new Map(); // Map<docId, arrayIndex>
|
|
48
|
+
this.nextChangeSeq = 1;
|
|
72
49
|
// Cache of loaded documents: Map<docId, InternalDoc>
|
|
73
50
|
this.docCache = new Map();
|
|
74
51
|
// Track which entry IDs we've already processed
|
|
75
52
|
this.processedEntryIds = [];
|
|
53
|
+
this.processedEntryCursor = null;
|
|
76
54
|
// Index: automergeHash -> entryId for each document
|
|
77
55
|
// Used for resolving Automerge dependency hashes to entry IDs
|
|
78
56
|
this.automergeHashToEntryId = new Map(); // Map<docId, Map<automergeHash, entryId>>
|
|
79
57
|
// Cache for imported public keys (CryptoKey objects) to avoid re-importing the same key
|
|
80
58
|
// Map<publicKeyPEM, CryptoKey>
|
|
81
59
|
this.publicKeyCache = new Map();
|
|
60
|
+
// Local cache support
|
|
61
|
+
this.cacheManager = null;
|
|
62
|
+
this.cachePrefix = null;
|
|
63
|
+
this.dirtyDocIds = new Set();
|
|
64
|
+
this.cacheMetaDirty = false;
|
|
82
65
|
this.tenant = tenant;
|
|
83
66
|
this.store = store;
|
|
84
67
|
this.attachmentStore = attachmentStore;
|
|
85
68
|
this.chunkSizeBytes = attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
|
|
86
69
|
this._isAdminOnlyDb = adminOnlyDb;
|
|
70
|
+
this.maxCachedDocs = Math.max(1, Math.floor(documentCacheConfig?.maxEntries ?? DEFAULT_MAX_CACHED_DOCS));
|
|
71
|
+
this.iteratePrefetchWindowDocs = Math.max(0, Math.floor(documentCacheConfig?.iteratePrefetchWindowDocs ??
|
|
72
|
+
DEFAULT_ITERATE_PREFETCH_WINDOW_DOCS));
|
|
73
|
+
this.cacheRestoreLimit = Math.max(1, Math.floor(documentCacheConfig?.restoreLimit ?? this.maxCachedDocs));
|
|
74
|
+
this.snapshotMinChanges = Math.max(1, Math.floor(snapshotConfig?.minChanges ?? DEFAULT_SNAPSHOT_MIN_CHANGES));
|
|
75
|
+
this.snapshotCooldownMs = Math.max(0, Math.floor(snapshotConfig?.cooldownMs ?? DEFAULT_SNAPSHOT_COOLDOWN_MS));
|
|
87
76
|
// Create logger if not provided (for backward compatibility)
|
|
88
77
|
this.logger =
|
|
89
78
|
logger ||
|
|
@@ -100,6 +89,207 @@ class BaseMindooDB {
|
|
|
100
89
|
isAdminOnlyDb() {
|
|
101
90
|
return this._isAdminOnlyDb;
|
|
102
91
|
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Local cache support (ICacheable)
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
/**
|
|
96
|
+
* Attach a CacheManager so this DB participates in periodic cache flushing.
|
|
97
|
+
*/
|
|
98
|
+
setCacheManager(cacheManager) {
|
|
99
|
+
this.cacheManager = cacheManager;
|
|
100
|
+
const cacheIdentity = this.store.getCacheIdentity?.() ?? this.store.getId();
|
|
101
|
+
this.cachePrefix = `${this.tenant.getId()}/${cacheIdentity}`;
|
|
102
|
+
cacheManager.register(this);
|
|
103
|
+
}
|
|
104
|
+
getCachePrefix() {
|
|
105
|
+
return this.cachePrefix ?? `${this.tenant.getId()}/${this.store.getId()}`;
|
|
106
|
+
}
|
|
107
|
+
hasDirtyState() {
|
|
108
|
+
return this.dirtyDocIds.size > 0 || this.cacheMetaDirty;
|
|
109
|
+
}
|
|
110
|
+
clearDirty() {
|
|
111
|
+
this.dirtyDocIds.clear();
|
|
112
|
+
this.cacheMetaDirty = false;
|
|
113
|
+
this.evictCachedDocsIfNeeded();
|
|
114
|
+
}
|
|
115
|
+
markDocDirty(docId) {
|
|
116
|
+
this.dirtyDocIds.add(docId);
|
|
117
|
+
this.cacheMetaDirty = true;
|
|
118
|
+
this.cacheManager?.markDirty();
|
|
119
|
+
}
|
|
120
|
+
getCachedDocument(docId) {
|
|
121
|
+
const cached = this.docCache.get(docId) ?? null;
|
|
122
|
+
if (!cached) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
this.touchCachedDocument(docId, cached);
|
|
126
|
+
return cached;
|
|
127
|
+
}
|
|
128
|
+
touchCachedDocument(docId, internalDoc) {
|
|
129
|
+
this.docCache.delete(docId);
|
|
130
|
+
this.docCache.set(docId, internalDoc);
|
|
131
|
+
}
|
|
132
|
+
storeCachedDocument(internalDoc) {
|
|
133
|
+
this.touchCachedDocument(internalDoc.id, internalDoc);
|
|
134
|
+
this.evictCachedDocsIfNeeded(new Set([internalDoc.id]));
|
|
135
|
+
}
|
|
136
|
+
evictCachedDocsIfNeeded(protectedDocIds) {
|
|
137
|
+
if (this.docCache.size <= this.maxCachedDocs) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const [docId] of this.docCache) {
|
|
141
|
+
if (this.docCache.size <= this.maxCachedDocs) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (protectedDocIds?.has(docId)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
this.docCache.delete(docId);
|
|
148
|
+
this.dirtyDocIds.delete(docId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Flush dirty documents and metadata to the cache store.
|
|
153
|
+
*/
|
|
154
|
+
async flushToCache(store) {
|
|
155
|
+
const prefix = this.getCachePrefix();
|
|
156
|
+
let written = 0;
|
|
157
|
+
for (const docId of this.dirtyDocIds) {
|
|
158
|
+
const internal = this.docCache.get(docId);
|
|
159
|
+
if (!internal)
|
|
160
|
+
continue;
|
|
161
|
+
const amBinary = automerge_adapter_1.Automerge.save(internal.doc);
|
|
162
|
+
const header = JSON.stringify({
|
|
163
|
+
id: internal.id,
|
|
164
|
+
createdAt: internal.createdAt,
|
|
165
|
+
lastModified: internal.lastModified,
|
|
166
|
+
decryptionKeyId: internal.decryptionKeyId,
|
|
167
|
+
isDeleted: internal.isDeleted,
|
|
168
|
+
});
|
|
169
|
+
const headerBytes = new TextEncoder().encode(header);
|
|
170
|
+
// Format: 4-byte header length (big-endian) + header JSON + Automerge binary
|
|
171
|
+
const value = new Uint8Array(4 + headerBytes.length + amBinary.length);
|
|
172
|
+
const view = new DataView(value.buffer);
|
|
173
|
+
view.setUint32(0, headerBytes.length, false);
|
|
174
|
+
value.set(headerBytes, 4);
|
|
175
|
+
value.set(amBinary, 4 + headerBytes.length);
|
|
176
|
+
await store.put("doc", `${prefix}/${docId}`, value);
|
|
177
|
+
written++;
|
|
178
|
+
}
|
|
179
|
+
// Write metadata checkpoint
|
|
180
|
+
const meta = this.exportMetadataCheckpoint();
|
|
181
|
+
await store.put("db-meta", prefix, meta);
|
|
182
|
+
written++;
|
|
183
|
+
return written;
|
|
184
|
+
}
|
|
185
|
+
exportMetadataCheckpoint() {
|
|
186
|
+
const checkpoint = {
|
|
187
|
+
version: 1,
|
|
188
|
+
processedEntryCursor: this.processedEntryCursor,
|
|
189
|
+
index: this.index,
|
|
190
|
+
nextChangeSeq: this.nextChangeSeq,
|
|
191
|
+
};
|
|
192
|
+
// Serialize automergeHashToEntryId: Map<string, Map<string,string>> -> nested object
|
|
193
|
+
const hashMap = {};
|
|
194
|
+
for (const [docId, inner] of this.automergeHashToEntryId) {
|
|
195
|
+
hashMap[docId] = Object.fromEntries(inner);
|
|
196
|
+
}
|
|
197
|
+
checkpoint.automergeHashToEntryId = hashMap;
|
|
198
|
+
// For stores without cursor scan, include processedEntryIds
|
|
199
|
+
if (!this.supportsCursorScan(this.store)) {
|
|
200
|
+
checkpoint.processedEntryIds = this.processedEntryIds;
|
|
201
|
+
}
|
|
202
|
+
return new TextEncoder().encode(JSON.stringify(checkpoint));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Attempt to restore state from cache. Returns true on success.
|
|
206
|
+
*/
|
|
207
|
+
async restoreFromCache(store) {
|
|
208
|
+
const prefix = this.getCachePrefix();
|
|
209
|
+
try {
|
|
210
|
+
// 1. Load metadata checkpoint
|
|
211
|
+
const metaBytes = await store.get("db-meta", prefix);
|
|
212
|
+
if (!metaBytes) {
|
|
213
|
+
this.logger.debug("No cache metadata found, will do full rebuild");
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const checkpoint = JSON.parse(new TextDecoder().decode(metaBytes));
|
|
217
|
+
if (checkpoint.version !== 1) {
|
|
218
|
+
this.logger.warn(`Unknown cache version ${checkpoint.version}, ignoring cache`);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
// 2. Restore metadata
|
|
222
|
+
this.processedEntryCursor = checkpoint.processedEntryCursor ?? null;
|
|
223
|
+
this.index = checkpoint.index ?? [];
|
|
224
|
+
this.nextChangeSeq = checkpoint.nextChangeSeq ?? 1;
|
|
225
|
+
if (checkpoint.processedEntryIds) {
|
|
226
|
+
this.processedEntryIds = checkpoint.processedEntryIds;
|
|
227
|
+
}
|
|
228
|
+
// Rebuild indexLookup from index
|
|
229
|
+
this.indexLookup.clear();
|
|
230
|
+
for (let i = 0; i < this.index.length; i++) {
|
|
231
|
+
if (typeof this.index[i].changeSeq !== "number") {
|
|
232
|
+
this.index[i].changeSeq = i + 1;
|
|
233
|
+
}
|
|
234
|
+
this.indexLookup.set(this.index[i].docId, i);
|
|
235
|
+
}
|
|
236
|
+
if (this.index.length > 0 && this.nextChangeSeq <= this.index.length) {
|
|
237
|
+
this.nextChangeSeq = Math.max(...this.index.map(e => e.changeSeq)) + 1;
|
|
238
|
+
}
|
|
239
|
+
// Restore automergeHashToEntryId
|
|
240
|
+
if (checkpoint.automergeHashToEntryId) {
|
|
241
|
+
this.automergeHashToEntryId.clear();
|
|
242
|
+
for (const [docId, inner] of Object.entries(checkpoint.automergeHashToEntryId)) {
|
|
243
|
+
this.automergeHashToEntryId.set(docId, new Map(Object.entries(inner)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 3. Load cached documents
|
|
247
|
+
const docIds = await store.list("doc");
|
|
248
|
+
const docPrefix = `${prefix}/`;
|
|
249
|
+
let restoredDocs = 0;
|
|
250
|
+
for (const id of docIds) {
|
|
251
|
+
if (restoredDocs >= this.cacheRestoreLimit) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
if (!id.startsWith(docPrefix))
|
|
255
|
+
continue;
|
|
256
|
+
const docId = id.slice(docPrefix.length);
|
|
257
|
+
const docBytes = await store.get("doc", id);
|
|
258
|
+
if (!docBytes)
|
|
259
|
+
continue;
|
|
260
|
+
try {
|
|
261
|
+
const internal = this.deserializeDoc(docBytes);
|
|
262
|
+
this.storeCachedDocument(internal);
|
|
263
|
+
restoredDocs++;
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
this.logger.warn(`Failed to restore cached doc ${docId}, will reload from store: ${e}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.logger.info(`Restored ${restoredDocs} documents from cache for ${prefix}`);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
this.logger.warn(`Cache restore failed, will do full rebuild: ${e}`);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
deserializeDoc(value) {
|
|
278
|
+
const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
279
|
+
const headerLen = view.getUint32(0, false);
|
|
280
|
+
const headerBytes = value.slice(4, 4 + headerLen);
|
|
281
|
+
const amBinary = value.slice(4 + headerLen);
|
|
282
|
+
const header = JSON.parse(new TextDecoder().decode(headerBytes));
|
|
283
|
+
const doc = automerge_adapter_1.Automerge.load(amBinary);
|
|
284
|
+
return {
|
|
285
|
+
id: header.id,
|
|
286
|
+
doc,
|
|
287
|
+
createdAt: header.createdAt,
|
|
288
|
+
lastModified: header.lastModified,
|
|
289
|
+
decryptionKeyId: header.decryptionKeyId,
|
|
290
|
+
isDeleted: header.isDeleted,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
103
293
|
/**
|
|
104
294
|
* Get or import a CryptoKey for signature verification, with caching.
|
|
105
295
|
* This avoids re-importing the same public key multiple times.
|
|
@@ -141,18 +331,18 @@ class BaseMindooDB {
|
|
|
141
331
|
/**
|
|
142
332
|
* Compare two index entries for sorting.
|
|
143
333
|
* Returns negative if a < b, positive if a > b, 0 if equal.
|
|
144
|
-
* Sorts by
|
|
334
|
+
* Sorts by deterministic changeSeq first, then by docId for uniqueness.
|
|
145
335
|
*/
|
|
146
336
|
compareIndexEntries(a, b) {
|
|
147
|
-
if (a.
|
|
148
|
-
return a.
|
|
337
|
+
if (a.changeSeq !== b.changeSeq) {
|
|
338
|
+
return a.changeSeq - b.changeSeq;
|
|
149
339
|
}
|
|
150
340
|
return a.docId.localeCompare(b.docId);
|
|
151
341
|
}
|
|
152
342
|
/**
|
|
153
343
|
* Update the index entry for a document.
|
|
154
|
-
* When a document changes, it
|
|
155
|
-
*
|
|
344
|
+
* When a document changes, it gets a new monotonic sequence and is moved to
|
|
345
|
+
* maintain order by (changeSeq, docId).
|
|
156
346
|
*
|
|
157
347
|
* Optimized to only update lookup map for entries that actually moved.
|
|
158
348
|
*
|
|
@@ -161,12 +351,14 @@ class BaseMindooDB {
|
|
|
161
351
|
* @param isDeleted Whether the document is deleted
|
|
162
352
|
*/
|
|
163
353
|
updateIndex(docId, lastModified, isDeleted) {
|
|
164
|
-
const
|
|
354
|
+
const startedAt = Date.now();
|
|
355
|
+
const assignedSeq = this.nextChangeSeq;
|
|
356
|
+
const newEntry = { docId, changeSeq: assignedSeq, lastModified, isDeleted };
|
|
165
357
|
const existingIndex = this.indexLookup.get(docId);
|
|
166
358
|
// Check if the entry already exists and hasn't changed position
|
|
167
359
|
if (existingIndex !== undefined) {
|
|
168
360
|
const existingEntry = this.index[existingIndex];
|
|
169
|
-
// If
|
|
361
|
+
// If only metadata flags stayed identical and caller replays same state, skip.
|
|
170
362
|
if (existingEntry.lastModified === lastModified && existingEntry.isDeleted === isDeleted) {
|
|
171
363
|
return; // No change needed
|
|
172
364
|
}
|
|
@@ -199,11 +391,17 @@ class BaseMindooDB {
|
|
|
199
391
|
}
|
|
200
392
|
// Insert at the correct position
|
|
201
393
|
this.index.splice(insertIndex, 0, newEntry);
|
|
394
|
+
this.nextChangeSeq = assignedSeq + 1;
|
|
202
395
|
// Update lookup map for entries from insertion point onwards
|
|
203
396
|
// Only update entries that actually moved (from insertIndex to end)
|
|
204
397
|
for (let i = insertIndex; i < this.index.length; i++) {
|
|
205
398
|
this.indexLookup.set(this.index[i].docId, i);
|
|
206
399
|
}
|
|
400
|
+
this.performanceCallback?.onIndexUpdate?.({
|
|
401
|
+
docId,
|
|
402
|
+
operation: existingIndex === undefined ? "insert" : "update",
|
|
403
|
+
time: Date.now() - startedAt,
|
|
404
|
+
});
|
|
207
405
|
}
|
|
208
406
|
/**
|
|
209
407
|
* Register an automerge hash -> entry ID mapping for a document.
|
|
@@ -251,9 +449,20 @@ class BaseMindooDB {
|
|
|
251
449
|
}
|
|
252
450
|
/**
|
|
253
451
|
* Initialize the database instance.
|
|
452
|
+
* If a cache store is available, attempts to restore from cache first,
|
|
453
|
+
* then processes only the delta. Falls back to full rebuild on cache miss.
|
|
254
454
|
*/
|
|
255
455
|
async initialize() {
|
|
256
456
|
this.logger.info(`Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
457
|
+
const cacheStore = this.cacheManager?.getStore();
|
|
458
|
+
if (cacheStore) {
|
|
459
|
+
const restored = await this.restoreFromCache(cacheStore);
|
|
460
|
+
if (restored) {
|
|
461
|
+
this.logger.info("Restored from cache, processing delta only");
|
|
462
|
+
await this.syncStoreChanges();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
257
466
|
await this.syncStoreChanges();
|
|
258
467
|
}
|
|
259
468
|
/**
|
|
@@ -262,13 +471,22 @@ class BaseMindooDB {
|
|
|
262
471
|
* On first call (when processedEntryIds is empty), it will process all entries.
|
|
263
472
|
*/
|
|
264
473
|
async syncStoreChanges() {
|
|
474
|
+
const syncStartedAt = Date.now();
|
|
265
475
|
this.logger.debug(`Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
266
476
|
this.logger.debug(`Already processed ${this.processedEntryIds.length} entry IDs`);
|
|
267
477
|
// Find new entries that we haven't processed yet
|
|
268
|
-
const newEntryMetadata = await this.
|
|
478
|
+
const newEntryMetadata = await this.getNewEntryMetadataForSync();
|
|
269
479
|
this.logger.debug(`Found ${newEntryMetadata.length} new entries`);
|
|
270
480
|
if (newEntryMetadata.length === 0) {
|
|
271
481
|
this.logger.debug(`No new entries to process`);
|
|
482
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
483
|
+
operation: "findNewEntries",
|
|
484
|
+
time: Date.now() - syncStartedAt,
|
|
485
|
+
details: {
|
|
486
|
+
newEntryCount: 0,
|
|
487
|
+
processedEntryCount: this.processedEntryIds.length,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
272
490
|
return;
|
|
273
491
|
}
|
|
274
492
|
// Group new entries by document ID
|
|
@@ -285,10 +503,11 @@ class BaseMindooDB {
|
|
|
285
503
|
this.logger.debug(`Processing ${entriesByDoc.size} documents with new entries`);
|
|
286
504
|
// Helper function to process a single document
|
|
287
505
|
const processDocument = async (docId, entryMetadataList) => {
|
|
506
|
+
const processStartedAt = Date.now();
|
|
288
507
|
try {
|
|
289
508
|
this.logger.debug(`===== Processing document ${docId} with ${entryMetadataList.length} new entry(s) in syncStoreChanges =====`);
|
|
290
509
|
// Check if document is cached
|
|
291
|
-
const cachedDoc = this.
|
|
510
|
+
const cachedDoc = this.getCachedDocument(docId);
|
|
292
511
|
let updatedDoc = null;
|
|
293
512
|
if (cachedDoc) {
|
|
294
513
|
// Document is cached - try incremental update
|
|
@@ -316,16 +535,37 @@ class BaseMindooDB {
|
|
|
316
535
|
}
|
|
317
536
|
}
|
|
318
537
|
else {
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
538
|
+
// Metadata-first startup: avoid full materialization for uncached docs.
|
|
539
|
+
// Index based on doc-lifecycle entries (create/change/delete) plus
|
|
540
|
+
// doc_snapshot which may be the sole entry after a dense sync.
|
|
541
|
+
// Attachment-only batches should not trigger document index updates.
|
|
542
|
+
const docLifecycleEntries = entryMetadataList.filter((e) => e.entryType === "doc_create" ||
|
|
543
|
+
e.entryType === "doc_change" ||
|
|
544
|
+
e.entryType === "doc_delete" ||
|
|
545
|
+
e.entryType === "doc_snapshot");
|
|
546
|
+
if (docLifecycleEntries.length === 0) {
|
|
547
|
+
this.logger.debug(`Skipping metadata-first index for doc ${docId} — no document lifecycle entries (${entryMetadataList.length} attachment/other entries only)`);
|
|
326
548
|
}
|
|
327
549
|
else {
|
|
328
|
-
|
|
550
|
+
// Before indexing, verify the user has the decryption key so that
|
|
551
|
+
// documents the user cannot access do not appear in getAllDocumentIds.
|
|
552
|
+
const representativeEntry = docLifecycleEntries[0];
|
|
553
|
+
const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
|
|
554
|
+
if (!keyAvailable) {
|
|
555
|
+
this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
const mutationEntries = docLifecycleEntries.filter((e) => e.entryType === "doc_create" ||
|
|
559
|
+
e.entryType === "doc_change" ||
|
|
560
|
+
e.entryType === "doc_delete");
|
|
561
|
+
// Snapshots compact replay history but should not change the user-visible
|
|
562
|
+
// modification time when the original create/change/delete entries still exist.
|
|
563
|
+
const entriesForLastModified = mutationEntries.length > 0 ? mutationEntries : docLifecycleEntries;
|
|
564
|
+
const lastModified = Math.max(...entriesForLastModified.map((e) => e.createdAt));
|
|
565
|
+
const isDeleted = mutationEntries.some((e) => e.entryType === "doc_delete");
|
|
566
|
+
this.updateIndex(docId, lastModified, isDeleted);
|
|
567
|
+
this.logger.debug(`Metadata-first update for uncached doc ${docId} (lastModified: ${lastModified}, isDeleted: ${isDeleted})`);
|
|
568
|
+
}
|
|
329
569
|
}
|
|
330
570
|
}
|
|
331
571
|
}
|
|
@@ -341,31 +581,332 @@ class BaseMindooDB {
|
|
|
341
581
|
// Re-throw the error so we can see what's happening in the test
|
|
342
582
|
throw error;
|
|
343
583
|
}
|
|
584
|
+
finally {
|
|
585
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
586
|
+
operation: "processDocument",
|
|
587
|
+
time: Date.now() - processStartedAt,
|
|
588
|
+
details: {
|
|
589
|
+
docId,
|
|
590
|
+
entryCount: entryMetadataList.length,
|
|
591
|
+
cacheHit: this.docCache.has(docId),
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
}
|
|
344
595
|
};
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const batch = documentEntries.slice(i, i + maxConcurrency);
|
|
352
|
-
const batchResults = await Promise.allSettled(batch.map(([docId, entryMetadataList]) => processDocument(docId, entryMetadataList)));
|
|
353
|
-
results.push(...batchResults);
|
|
354
|
-
}
|
|
355
|
-
// Check for errors (excluding SymmetricKeyNotFoundError which is handled gracefully)
|
|
356
|
-
for (let i = 0; i < results.length; i++) {
|
|
357
|
-
const result = results[i];
|
|
358
|
-
if (result.status === 'rejected') {
|
|
359
|
-
const error = result.reason;
|
|
360
|
-
if (!(error instanceof errors_1.SymmetricKeyNotFoundError)) {
|
|
361
|
-
// Re-throw non-key errors
|
|
362
|
-
throw error;
|
|
363
|
-
}
|
|
596
|
+
const documentEntries = Array.from(entriesByDoc.entries())
|
|
597
|
+
.sort((a, b) => {
|
|
598
|
+
const aMinCreatedAt = Math.min(...a[1].map((e) => e.createdAt));
|
|
599
|
+
const bMinCreatedAt = Math.min(...b[1].map((e) => e.createdAt));
|
|
600
|
+
if (aMinCreatedAt !== bMinCreatedAt) {
|
|
601
|
+
return aMinCreatedAt - bMinCreatedAt;
|
|
364
602
|
}
|
|
603
|
+
return a[0].localeCompare(b[0]);
|
|
604
|
+
});
|
|
605
|
+
// Deterministic sequential processing ensures stable changefeed ordering.
|
|
606
|
+
for (const [docId, entryMetadataList] of documentEntries) {
|
|
607
|
+
await processDocument(docId, entryMetadataList);
|
|
365
608
|
}
|
|
366
609
|
// Append new entry IDs to our processed list
|
|
367
610
|
this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
|
|
611
|
+
this.cacheMetaDirty = true;
|
|
612
|
+
this.cacheManager?.markDirty();
|
|
368
613
|
this.logger.debug(`Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
|
|
614
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
615
|
+
operation: "findNewEntries",
|
|
616
|
+
time: Date.now() - syncStartedAt,
|
|
617
|
+
details: {
|
|
618
|
+
newEntryCount: newEntryMetadata.length,
|
|
619
|
+
documentCount: entriesByDoc.size,
|
|
620
|
+
indexSize: this.index.length,
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
supportsCursorScan(store) {
|
|
625
|
+
return typeof store.scanEntriesSince === "function";
|
|
626
|
+
}
|
|
627
|
+
async scanAllMetadata(store, filters) {
|
|
628
|
+
if (!this.supportsCursorScan(store)) {
|
|
629
|
+
if (filters?.docId) {
|
|
630
|
+
return store.findNewEntriesForDoc([], filters.docId);
|
|
631
|
+
}
|
|
632
|
+
return store.findNewEntries([]);
|
|
633
|
+
}
|
|
634
|
+
const all = [];
|
|
635
|
+
let cursor = null;
|
|
636
|
+
while (true) {
|
|
637
|
+
const page = await store.scanEntriesSince(cursor, 1000, filters);
|
|
638
|
+
all.push(...page.entries);
|
|
639
|
+
cursor = page.nextCursor;
|
|
640
|
+
if (!page.hasMore) {
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return all;
|
|
645
|
+
}
|
|
646
|
+
async getNewEntryMetadataForSync() {
|
|
647
|
+
const startedAt = Date.now();
|
|
648
|
+
if (!this.supportsCursorScan(this.store)) {
|
|
649
|
+
const result = await this.store.findNewEntries(this.processedEntryIds);
|
|
650
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
651
|
+
operation: "findNewEntries",
|
|
652
|
+
time: Date.now() - startedAt,
|
|
653
|
+
details: {
|
|
654
|
+
mode: "knownIds",
|
|
655
|
+
resultCount: result.length,
|
|
656
|
+
processedEntryCount: this.processedEntryIds.length,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
const allNew = [];
|
|
662
|
+
let cursor = this.processedEntryCursor;
|
|
663
|
+
while (true) {
|
|
664
|
+
const page = await this.store.scanEntriesSince(cursor, 1000);
|
|
665
|
+
allNew.push(...page.entries);
|
|
666
|
+
cursor = page.nextCursor;
|
|
667
|
+
if (!page.hasMore) {
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this.processedEntryCursor = cursor;
|
|
672
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
673
|
+
operation: "findNewEntries",
|
|
674
|
+
time: Date.now() - startedAt,
|
|
675
|
+
details: {
|
|
676
|
+
mode: "cursorScan",
|
|
677
|
+
resultCount: allNew.length,
|
|
678
|
+
cursor,
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
return allNew;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Fetch the target store's bloom filter summary, returning null if
|
|
685
|
+
* unsupported or on error (callers fall back to exact checks).
|
|
686
|
+
*/
|
|
687
|
+
async getTargetBloomSummary(targetStore) {
|
|
688
|
+
if (typeof targetStore.getIdBloomSummary !== "function") {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
return await targetStore.getIdBloomSummary();
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
this.logger.warn("Failed to get bloom summary from target store, falling back to exact checks", error);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* From a list of candidate IDs, return only those the target store is
|
|
701
|
+
* missing. Uses bloom-filter pre-screening when available, then falls
|
|
702
|
+
* back to exact `hasEntries` for the uncertain set.
|
|
703
|
+
*/
|
|
704
|
+
async filterMissingIds(targetStore, candidateIds, bloom) {
|
|
705
|
+
let definitelyMissing = [];
|
|
706
|
+
let maybeExisting = candidateIds;
|
|
707
|
+
if (bloom) {
|
|
708
|
+
definitelyMissing = [];
|
|
709
|
+
maybeExisting = [];
|
|
710
|
+
for (const id of candidateIds) {
|
|
711
|
+
if ((0, bloom_1.bloomMightContainId)(bloom, id)) {
|
|
712
|
+
maybeExisting.push(id);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
definitelyMissing.push(id);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
let missingIds = definitelyMissing;
|
|
720
|
+
if (maybeExisting.length > 0) {
|
|
721
|
+
const existing = await targetStore.hasEntries(maybeExisting);
|
|
722
|
+
const existingSet = new Set(existing);
|
|
723
|
+
missingIds = missingIds.concat(maybeExisting.filter((id) => !existingSet.has(id)));
|
|
724
|
+
}
|
|
725
|
+
return missingIds;
|
|
726
|
+
}
|
|
727
|
+
async syncEntriesFromStore(sourceStore, targetStore, options) {
|
|
728
|
+
if (options?.mode === "dense") {
|
|
729
|
+
return this.syncEntriesFromStoreDense(sourceStore, targetStore, options);
|
|
730
|
+
}
|
|
731
|
+
let transferred = 0;
|
|
732
|
+
let scanned = 0;
|
|
733
|
+
const onProgress = options?.onProgress;
|
|
734
|
+
const pageSize = options?.pageSize ?? 1000;
|
|
735
|
+
const targetBloom = await this.getTargetBloomSummary(targetStore);
|
|
736
|
+
const totalSourceEstimate = targetBloom?.totalIds;
|
|
737
|
+
if (this.supportsCursorScan(sourceStore)) {
|
|
738
|
+
onProgress?.({
|
|
739
|
+
phase: 'preparing',
|
|
740
|
+
message: 'Preparing to sync entries...',
|
|
741
|
+
transferredEntries: 0,
|
|
742
|
+
scannedEntries: 0,
|
|
743
|
+
totalSourceEntries: totalSourceEstimate,
|
|
744
|
+
});
|
|
745
|
+
let cursor = null;
|
|
746
|
+
let currentPage = 0;
|
|
747
|
+
while (true) {
|
|
748
|
+
if (options?.signal?.aborted) {
|
|
749
|
+
return { transferred, cancelled: true };
|
|
750
|
+
}
|
|
751
|
+
const page = await sourceStore.scanEntriesSince(cursor, pageSize);
|
|
752
|
+
currentPage++;
|
|
753
|
+
scanned += page.entries.length;
|
|
754
|
+
if (page.entries.length > 0) {
|
|
755
|
+
const ids = page.entries.map((m) => m.id);
|
|
756
|
+
const missingIds = await this.filterMissingIds(targetStore, ids, targetBloom);
|
|
757
|
+
if (missingIds.length > 0) {
|
|
758
|
+
const missingEntries = await sourceStore.getEntries(missingIds);
|
|
759
|
+
await targetStore.putEntries(missingEntries);
|
|
760
|
+
transferred += missingEntries.length;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
onProgress?.({
|
|
764
|
+
phase: 'transferring',
|
|
765
|
+
message: `Transferred ${transferred} entries (page ${currentPage}, scanned ${scanned})`,
|
|
766
|
+
transferredEntries: transferred,
|
|
767
|
+
scannedEntries: scanned,
|
|
768
|
+
totalSourceEntries: totalSourceEstimate,
|
|
769
|
+
currentPage,
|
|
770
|
+
});
|
|
771
|
+
cursor = page.nextCursor;
|
|
772
|
+
if (!page.hasMore) {
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return { transferred, cancelled: false };
|
|
777
|
+
}
|
|
778
|
+
onProgress?.({
|
|
779
|
+
phase: 'preparing',
|
|
780
|
+
message: 'Finding new entries...',
|
|
781
|
+
transferredEntries: 0,
|
|
782
|
+
scannedEntries: 0,
|
|
783
|
+
});
|
|
784
|
+
const targetIds = await targetStore.getAllIds();
|
|
785
|
+
const sourceNewMetadata = await sourceStore.findNewEntries(targetIds);
|
|
786
|
+
if (sourceNewMetadata.length === 0) {
|
|
787
|
+
return { transferred: 0, cancelled: false };
|
|
788
|
+
}
|
|
789
|
+
if (options?.signal?.aborted) {
|
|
790
|
+
return { transferred: 0, cancelled: true };
|
|
791
|
+
}
|
|
792
|
+
onProgress?.({
|
|
793
|
+
phase: 'transferring',
|
|
794
|
+
message: `Transferring ${sourceNewMetadata.length} entries...`,
|
|
795
|
+
transferredEntries: 0,
|
|
796
|
+
scannedEntries: sourceNewMetadata.length,
|
|
797
|
+
totalSourceEntries: sourceNewMetadata.length,
|
|
798
|
+
});
|
|
799
|
+
const sourceEntries = await sourceStore.getEntries(sourceNewMetadata.map((m) => m.id));
|
|
800
|
+
await targetStore.putEntries(sourceEntries);
|
|
801
|
+
onProgress?.({
|
|
802
|
+
phase: 'transferring',
|
|
803
|
+
message: `Transferred ${sourceEntries.length} entries`,
|
|
804
|
+
transferredEntries: sourceEntries.length,
|
|
805
|
+
scannedEntries: sourceNewMetadata.length,
|
|
806
|
+
totalSourceEntries: sourceNewMetadata.length,
|
|
807
|
+
});
|
|
808
|
+
return { transferred: sourceEntries.length, cancelled: false };
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Dense sync: transfer only the entries required to reconstruct the latest
|
|
812
|
+
* state of each document, using the batch materialization planner to skip
|
|
813
|
+
* historical entries already superseded by snapshots.
|
|
814
|
+
*
|
|
815
|
+
* Algorithm:
|
|
816
|
+
* 1. Discover all documents on the source via `doc_create` metadata.
|
|
817
|
+
* 2. Also fetch `doc_delete` metadata so deletion markers are transferred.
|
|
818
|
+
* 3. Ask the source's batch planner for the optimal replay set per document.
|
|
819
|
+
* 4. Merge all entry IDs: doc_create + doc_delete + snapshot + uncovered changes.
|
|
820
|
+
* 5. Filter out IDs the target already has (bloom + exact check).
|
|
821
|
+
* 6. Transfer only the missing entries.
|
|
822
|
+
*
|
|
823
|
+
* Attachment chunks are intentionally skipped — they are not part of the
|
|
824
|
+
* Automerge document DAG and can be fetched on demand when accessed.
|
|
825
|
+
*/
|
|
826
|
+
async syncEntriesFromStoreDense(sourceStore, targetStore, options) {
|
|
827
|
+
const onProgress = options?.onProgress;
|
|
828
|
+
onProgress?.({
|
|
829
|
+
phase: "preparing",
|
|
830
|
+
message: "Dense sync: discovering documents on source...",
|
|
831
|
+
transferredEntries: 0,
|
|
832
|
+
scannedEntries: 0,
|
|
833
|
+
});
|
|
834
|
+
if (options?.signal?.aborted) {
|
|
835
|
+
return { transferred: 0, cancelled: true };
|
|
836
|
+
}
|
|
837
|
+
// ── Phase 1: discover documents ──────────────────────────────────
|
|
838
|
+
const docCreateEntries = await sourceStore.findEntries("doc_create", null, null);
|
|
839
|
+
const docDeleteEntries = await sourceStore.findEntries("doc_delete", null, null);
|
|
840
|
+
const docIds = [...new Set(docCreateEntries.map((e) => e.docId))];
|
|
841
|
+
this.logger.info(`Dense sync: found ${docIds.length} documents on source`);
|
|
842
|
+
if (docIds.length === 0) {
|
|
843
|
+
return { transferred: 0, cancelled: false };
|
|
844
|
+
}
|
|
845
|
+
if (options?.signal?.aborted) {
|
|
846
|
+
return { transferred: 0, cancelled: true };
|
|
847
|
+
}
|
|
848
|
+
// ── Phase 2: batch plan on source ────────────────────────────────
|
|
849
|
+
onProgress?.({
|
|
850
|
+
phase: "planning",
|
|
851
|
+
message: `Dense sync: computing materialization plans for ${docIds.length} documents...`,
|
|
852
|
+
transferredEntries: 0,
|
|
853
|
+
scannedEntries: docIds.length,
|
|
854
|
+
});
|
|
855
|
+
const batchPlan = await sourceStore.planDocumentMaterializationBatch(docIds);
|
|
856
|
+
// ── Phase 3: collect needed entry IDs ────────────────────────────
|
|
857
|
+
const neededIds = new Set();
|
|
858
|
+
for (const entry of docCreateEntries) {
|
|
859
|
+
neededIds.add(entry.id);
|
|
860
|
+
}
|
|
861
|
+
for (const entry of docDeleteEntries) {
|
|
862
|
+
neededIds.add(entry.id);
|
|
863
|
+
}
|
|
864
|
+
for (const plan of batchPlan.plans) {
|
|
865
|
+
if (plan.snapshotEntryId) {
|
|
866
|
+
neededIds.add(plan.snapshotEntryId);
|
|
867
|
+
}
|
|
868
|
+
for (const id of plan.entryIdsToApply) {
|
|
869
|
+
neededIds.add(id);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
this.logger.info(`Dense sync: planner identified ${neededIds.size} required entries ` +
|
|
873
|
+
`(from ${docIds.length} documents)`);
|
|
874
|
+
if (options?.signal?.aborted) {
|
|
875
|
+
return { transferred: 0, cancelled: true };
|
|
876
|
+
}
|
|
877
|
+
// ── Phase 4: filter out entries the target already has ───────────
|
|
878
|
+
const allNeededArray = Array.from(neededIds);
|
|
879
|
+
const targetBloom = await this.getTargetBloomSummary(targetStore);
|
|
880
|
+
const missingIds = await this.filterMissingIds(targetStore, allNeededArray, targetBloom);
|
|
881
|
+
this.logger.info(`Dense sync: ${missingIds.length} entries to transfer ` +
|
|
882
|
+
`(${allNeededArray.length - missingIds.length} already present)`);
|
|
883
|
+
if (missingIds.length === 0) {
|
|
884
|
+
return { transferred: 0, cancelled: false };
|
|
885
|
+
}
|
|
886
|
+
if (options?.signal?.aborted) {
|
|
887
|
+
return { transferred: 0, cancelled: true };
|
|
888
|
+
}
|
|
889
|
+
// ── Phase 5: transfer missing entries in pages ───────────────────
|
|
890
|
+
const pageSize = options?.pageSize ?? 500;
|
|
891
|
+
let transferred = 0;
|
|
892
|
+
for (let offset = 0; offset < missingIds.length; offset += pageSize) {
|
|
893
|
+
if (options?.signal?.aborted) {
|
|
894
|
+
return { transferred, cancelled: true };
|
|
895
|
+
}
|
|
896
|
+
const batch = missingIds.slice(offset, offset + pageSize);
|
|
897
|
+
const entries = await sourceStore.getEntries(batch);
|
|
898
|
+
await targetStore.putEntries(entries);
|
|
899
|
+
transferred += entries.length;
|
|
900
|
+
onProgress?.({
|
|
901
|
+
phase: "transferring",
|
|
902
|
+
message: `Dense sync: transferred ${transferred}/${missingIds.length} entries`,
|
|
903
|
+
transferredEntries: transferred,
|
|
904
|
+
scannedEntries: allNeededArray.length,
|
|
905
|
+
totalSourceEntries: allNeededArray.length,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
this.logger.info(`Dense sync complete: transferred ${transferred} entries`);
|
|
909
|
+
return { transferred, cancelled: false };
|
|
369
910
|
}
|
|
370
911
|
getStore() {
|
|
371
912
|
return this.store;
|
|
@@ -407,18 +948,18 @@ class BaseMindooDB {
|
|
|
407
948
|
const docId = (0, uuid_1.v7)();
|
|
408
949
|
this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
409
950
|
// Create initial Automerge document
|
|
410
|
-
const initialDoc = Automerge.init();
|
|
951
|
+
const initialDoc = automerge_adapter_1.Automerge.init();
|
|
411
952
|
// Create the first change
|
|
412
953
|
const now = Date.now();
|
|
413
954
|
this.logger.debug(`Creating initial Automerge change for document ${docId}`);
|
|
414
955
|
let newDoc;
|
|
415
956
|
try {
|
|
416
|
-
newDoc = Automerge.change(initialDoc, (doc) => {
|
|
957
|
+
newDoc = automerge_adapter_1.Automerge.change(initialDoc, (doc) => {
|
|
417
958
|
// Store metadata in the document payload
|
|
418
959
|
// We need to modify the document to ensure a change is created
|
|
419
960
|
doc._attachments = [];
|
|
420
961
|
});
|
|
421
|
-
this.logger.debug(`Successfully created Automerge change, document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
|
|
962
|
+
this.logger.debug(`Successfully created Automerge change, document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
422
963
|
}
|
|
423
964
|
catch (error) {
|
|
424
965
|
this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
|
|
@@ -426,7 +967,7 @@ class BaseMindooDB {
|
|
|
426
967
|
}
|
|
427
968
|
// Get the change bytes from the document
|
|
428
969
|
this.logger.debug(`Getting change bytes from document ${docId}`);
|
|
429
|
-
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
970
|
+
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
430
971
|
if (!changeBytes) {
|
|
431
972
|
throw new Error("Failed to get change bytes from Automerge document");
|
|
432
973
|
}
|
|
@@ -435,7 +976,7 @@ class BaseMindooDB {
|
|
|
435
976
|
this.logger.debug(`Decoding change to get hash and dependencies`);
|
|
436
977
|
let decodedChange;
|
|
437
978
|
try {
|
|
438
|
-
decodedChange = Automerge.decodeChange(changeBytes);
|
|
979
|
+
decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
439
980
|
this.logger.debug(`Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
|
|
440
981
|
}
|
|
441
982
|
catch (error) {
|
|
@@ -504,8 +1045,9 @@ class BaseMindooDB {
|
|
|
504
1045
|
isDeleted: false,
|
|
505
1046
|
};
|
|
506
1047
|
// Update cache and index
|
|
507
|
-
this.
|
|
1048
|
+
this.storeCachedDocument(internalDoc);
|
|
508
1049
|
this.updateIndex(docId, internalDoc.lastModified, false);
|
|
1050
|
+
this.markDocDirty(docId);
|
|
509
1051
|
this.logger.info(`Document ${docId} created successfully`);
|
|
510
1052
|
this.logger.debug(`Document ${docId} cached and indexed (lastModified: ${internalDoc.lastModified})`);
|
|
511
1053
|
return this.wrapDocument(internalDoc);
|
|
@@ -521,20 +1063,28 @@ class BaseMindooDB {
|
|
|
521
1063
|
return this.wrapDocument(internalDoc);
|
|
522
1064
|
}
|
|
523
1065
|
async getDocumentAtTimestamp(docId, timestamp) {
|
|
1066
|
+
const startedAt = Date.now();
|
|
524
1067
|
this.logger.debug(`Getting document ${docId} at timestamp ${timestamp}`);
|
|
525
1068
|
// Get all entry metadata for this document
|
|
526
|
-
const allEntryMetadata = await this.store
|
|
1069
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
527
1070
|
// Filter entries up to the timestamp
|
|
528
1071
|
const relevantEntries = allEntryMetadata
|
|
529
1072
|
.filter(em => em.createdAt <= timestamp)
|
|
530
1073
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
531
1074
|
if (relevantEntries.length === 0) {
|
|
1075
|
+
this.performanceCallback?.onHistoryOperation?.({
|
|
1076
|
+
operation: "getDocumentAtTimestamp",
|
|
1077
|
+
docId,
|
|
1078
|
+
time: Date.now() - startedAt,
|
|
1079
|
+
scannedEntries: 0,
|
|
1080
|
+
bounded: false,
|
|
1081
|
+
});
|
|
532
1082
|
return null; // Document didn't exist at that time
|
|
533
1083
|
}
|
|
534
1084
|
// Load entries
|
|
535
1085
|
const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
|
|
536
1086
|
// Apply changes in order
|
|
537
|
-
let doc = Automerge.init();
|
|
1087
|
+
let doc = automerge_adapter_1.Automerge.init();
|
|
538
1088
|
for (const entryData of entries) {
|
|
539
1089
|
// Admin-only mode: only accept entries signed by the admin key
|
|
540
1090
|
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
@@ -551,7 +1101,7 @@ class BaseMindooDB {
|
|
|
551
1101
|
// Decrypt payload (only after signature verification passes)
|
|
552
1102
|
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
553
1103
|
// Apply change using loadIncremental - this is the recommended way for binary change data
|
|
554
|
-
doc = Automerge.loadIncremental(doc, decryptedPayload);
|
|
1104
|
+
doc = automerge_adapter_1.Automerge.loadIncremental(doc, decryptedPayload);
|
|
555
1105
|
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
556
1106
|
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
557
1107
|
if (parsed) {
|
|
@@ -576,12 +1126,20 @@ class BaseMindooDB {
|
|
|
576
1126
|
decryptionKeyId,
|
|
577
1127
|
isDeleted,
|
|
578
1128
|
};
|
|
1129
|
+
this.performanceCallback?.onHistoryOperation?.({
|
|
1130
|
+
operation: "getDocumentAtTimestamp",
|
|
1131
|
+
docId,
|
|
1132
|
+
time: Date.now() - startedAt,
|
|
1133
|
+
scannedEntries: relevantEntries.length,
|
|
1134
|
+
returnedEntries: 1,
|
|
1135
|
+
bounded: false,
|
|
1136
|
+
});
|
|
579
1137
|
return this.wrapDocument(internalDoc);
|
|
580
1138
|
}
|
|
581
1139
|
async *iterateDocumentHistory(docId) {
|
|
582
1140
|
this.logger.debug(`Iterating document history for ${docId}`);
|
|
583
1141
|
// Get all entry metadata for this document
|
|
584
|
-
const allEntryMetadata = await this.store
|
|
1142
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
585
1143
|
// Filter to only doc_create, doc_change, and doc_delete entries (exclude snapshots)
|
|
586
1144
|
const relevantEntries = allEntryMetadata
|
|
587
1145
|
.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete")
|
|
@@ -623,17 +1181,17 @@ class BaseMindooDB {
|
|
|
623
1181
|
this.logger.warn(`First entry is not doc_create, skipping`);
|
|
624
1182
|
continue;
|
|
625
1183
|
}
|
|
626
|
-
currentDoc = Automerge.init();
|
|
1184
|
+
currentDoc = automerge_adapter_1.Automerge.init();
|
|
627
1185
|
createdAt = entryMetadata.createdAt;
|
|
628
1186
|
decryptionKeyId = entryMetadata.decryptionKeyId;
|
|
629
1187
|
}
|
|
630
1188
|
// Check document heads before applying change (for non-first entries)
|
|
631
|
-
const headsBefore = isFirstEntry ? null : (currentDoc ? Automerge.getHeads(currentDoc) : null);
|
|
1189
|
+
const headsBefore = isFirstEntry ? null : (currentDoc ? automerge_adapter_1.Automerge.getHeads(currentDoc) : null);
|
|
632
1190
|
// Apply change using loadIncremental
|
|
633
1191
|
if (currentDoc === null) {
|
|
634
1192
|
throw new Error("currentDoc should not be null at this point");
|
|
635
1193
|
}
|
|
636
|
-
currentDoc = Automerge.loadIncremental(currentDoc, decryptedPayload);
|
|
1194
|
+
currentDoc = automerge_adapter_1.Automerge.loadIncremental(currentDoc, decryptedPayload);
|
|
637
1195
|
// Check if document actually changed by comparing heads
|
|
638
1196
|
// For first entry (doc_create), always yield since it's the initial creation
|
|
639
1197
|
// For delete entries, always yield
|
|
@@ -643,7 +1201,7 @@ class BaseMindooDB {
|
|
|
643
1201
|
shouldYield = true;
|
|
644
1202
|
}
|
|
645
1203
|
else {
|
|
646
|
-
const headsAfter = Automerge.getHeads(currentDoc);
|
|
1204
|
+
const headsAfter = automerge_adapter_1.Automerge.getHeads(currentDoc);
|
|
647
1205
|
const headsChanged = headsBefore !== null && JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
|
|
648
1206
|
shouldYield = headsChanged;
|
|
649
1207
|
}
|
|
@@ -655,7 +1213,7 @@ class BaseMindooDB {
|
|
|
655
1213
|
// Only yield if document actually changed (or if this is create/delete entry)
|
|
656
1214
|
if (shouldYield) {
|
|
657
1215
|
// Clone the document to ensure independence
|
|
658
|
-
const clonedDoc = Automerge.clone(currentDoc);
|
|
1216
|
+
const clonedDoc = automerge_adapter_1.Automerge.clone(currentDoc);
|
|
659
1217
|
// Create internal doc representation
|
|
660
1218
|
const internalDoc = {
|
|
661
1219
|
id: docId,
|
|
@@ -679,6 +1237,46 @@ class BaseMindooDB {
|
|
|
679
1237
|
}
|
|
680
1238
|
}
|
|
681
1239
|
}
|
|
1240
|
+
async getDocumentHistoryPage(docId, options) {
|
|
1241
|
+
const startedAt = Date.now();
|
|
1242
|
+
const limit = Math.max(1, Math.floor(options?.limit ?? 100));
|
|
1243
|
+
const offset = Math.max(0, Math.floor(options?.cursor?.offset ?? 0));
|
|
1244
|
+
// This API stays metadata-only so large history views can page cheaply
|
|
1245
|
+
// without reconstructing every historical Automerge document state.
|
|
1246
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
1247
|
+
const relevantEntries = allEntryMetadata
|
|
1248
|
+
.filter((em) => em.entryType === "doc_create" ||
|
|
1249
|
+
em.entryType === "doc_change" ||
|
|
1250
|
+
em.entryType === "doc_delete")
|
|
1251
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
1252
|
+
// Cursor paging is offset-based because the result is a bounded timeline view
|
|
1253
|
+
// over the doc's sorted change metadata, not a resumable store scan cursor.
|
|
1254
|
+
const slice = relevantEntries.slice(offset, offset + limit);
|
|
1255
|
+
const entries = slice.map((entry) => ({
|
|
1256
|
+
entryId: entry.id,
|
|
1257
|
+
entryType: entry.entryType,
|
|
1258
|
+
changeCreatedAt: entry.createdAt,
|
|
1259
|
+
changeCreatedByPublicKey: entry.createdByPublicKey,
|
|
1260
|
+
dependencyIds: [...entry.dependencyIds],
|
|
1261
|
+
isDeleted: entry.entryType === "doc_delete",
|
|
1262
|
+
}));
|
|
1263
|
+
const nextOffset = offset + entries.length;
|
|
1264
|
+
const hasMore = nextOffset < relevantEntries.length;
|
|
1265
|
+
const result = {
|
|
1266
|
+
entries,
|
|
1267
|
+
nextCursor: hasMore ? { offset: nextOffset } : null,
|
|
1268
|
+
hasMore,
|
|
1269
|
+
};
|
|
1270
|
+
this.performanceCallback?.onHistoryOperation?.({
|
|
1271
|
+
operation: "getDocumentHistoryPage",
|
|
1272
|
+
docId,
|
|
1273
|
+
time: Date.now() - startedAt,
|
|
1274
|
+
scannedEntries: relevantEntries.length,
|
|
1275
|
+
returnedEntries: entries.length,
|
|
1276
|
+
bounded: true,
|
|
1277
|
+
});
|
|
1278
|
+
return result;
|
|
1279
|
+
}
|
|
682
1280
|
async getAllDocumentIds() {
|
|
683
1281
|
// Return all non-deleted document IDs from index
|
|
684
1282
|
const docIds = [];
|
|
@@ -690,10 +1288,14 @@ class BaseMindooDB {
|
|
|
690
1288
|
return docIds;
|
|
691
1289
|
}
|
|
692
1290
|
async getAllDocumentIdsAtTimestamp(timestamp) {
|
|
1291
|
+
// findEntries() uses an exclusive upper bound (createdAt < creationDateUntil).
|
|
1292
|
+
// Query up to timestamp + 1 so entries created exactly at `timestamp` are included
|
|
1293
|
+
// for the strict checks below (createTime < timestamp, deleteTime > timestamp).
|
|
1294
|
+
const upperBoundExclusive = timestamp === Number.MAX_SAFE_INTEGER ? timestamp : timestamp + 1;
|
|
693
1295
|
// Efficiently query for doc_create and doc_delete entries before the timestamp
|
|
694
1296
|
const [creates, deletes] = await Promise.all([
|
|
695
|
-
this.store.findEntries("doc_create", null,
|
|
696
|
-
this.store.findEntries("doc_delete", null,
|
|
1297
|
+
this.store.findEntries("doc_create", null, upperBoundExclusive),
|
|
1298
|
+
this.store.findEntries("doc_delete", null, upperBoundExclusive)
|
|
697
1299
|
]);
|
|
698
1300
|
// Build sets of docIds for efficient lookup
|
|
699
1301
|
const createdDocIds = new Set();
|
|
@@ -722,9 +1324,9 @@ class BaseMindooDB {
|
|
|
722
1324
|
const createTime = createTimes.get(docId);
|
|
723
1325
|
const deleteTime = deleteTimes.get(docId);
|
|
724
1326
|
// Document exists at timestamp if:
|
|
725
|
-
// 1. It was created before the timestamp
|
|
1327
|
+
// 1. It was created at or before the timestamp
|
|
726
1328
|
// 2. Either it was never deleted, or it was deleted after the timestamp
|
|
727
|
-
if (createTime
|
|
1329
|
+
if (createTime <= timestamp) {
|
|
728
1330
|
if (!deleteTime || deleteTime > timestamp) {
|
|
729
1331
|
docIds.push(docId);
|
|
730
1332
|
}
|
|
@@ -761,24 +1363,26 @@ class BaseMindooDB {
|
|
|
761
1363
|
if (!internalDoc || internalDoc.isDeleted) {
|
|
762
1364
|
throw new Error(`Document ${docId} not found or already deleted`);
|
|
763
1365
|
}
|
|
1366
|
+
const now = Date.now();
|
|
764
1367
|
// Create deletion change by clearing all fields from the document
|
|
765
1368
|
// This ensures Automerge produces actual change bytes
|
|
766
1369
|
// The deletion is also tracked via the "doc_delete" type entry in the append-only store
|
|
767
|
-
const newDoc = Automerge.change(internalDoc.doc, (doc) => {
|
|
1370
|
+
const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (doc) => {
|
|
768
1371
|
// Remove all fields from the document
|
|
769
1372
|
for (const key of Object.keys(doc)) {
|
|
770
1373
|
delete doc[key];
|
|
771
1374
|
}
|
|
772
1375
|
// Mark as deleted for clarity
|
|
773
1376
|
doc._deleted = true;
|
|
1377
|
+
doc._lastModified = now;
|
|
774
1378
|
});
|
|
775
1379
|
// Get the change bytes from the document
|
|
776
|
-
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
1380
|
+
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
777
1381
|
if (!changeBytes) {
|
|
778
1382
|
throw new Error("Failed to get change bytes from Automerge document");
|
|
779
1383
|
}
|
|
780
1384
|
// Decode the change to get hash and dependencies
|
|
781
|
-
const decodedChange = Automerge.decodeChange(changeBytes);
|
|
1385
|
+
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
782
1386
|
const automergeHash = decodedChange.hash;
|
|
783
1387
|
const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
|
|
784
1388
|
// Encrypt the change payload first
|
|
@@ -810,7 +1414,7 @@ class BaseMindooDB {
|
|
|
810
1414
|
contentHash,
|
|
811
1415
|
docId,
|
|
812
1416
|
dependencyIds,
|
|
813
|
-
createdAt:
|
|
1417
|
+
createdAt: now,
|
|
814
1418
|
createdByPublicKey,
|
|
815
1419
|
decryptionKeyId: internalDoc.decryptionKeyId,
|
|
816
1420
|
signature,
|
|
@@ -828,9 +1432,10 @@ class BaseMindooDB {
|
|
|
828
1432
|
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
829
1433
|
// Update cache and index
|
|
830
1434
|
internalDoc.isDeleted = true;
|
|
831
|
-
internalDoc.lastModified =
|
|
832
|
-
this.
|
|
1435
|
+
internalDoc.lastModified = now;
|
|
1436
|
+
this.storeCachedDocument(internalDoc);
|
|
833
1437
|
this.updateIndex(docId, internalDoc.lastModified, true);
|
|
1438
|
+
this.markDocDirty(docId);
|
|
834
1439
|
this.logger.info(`Document ${docId} deleted successfully`);
|
|
835
1440
|
}
|
|
836
1441
|
async changeDoc(doc, changeFunc) {
|
|
@@ -854,7 +1459,7 @@ class BaseMindooDB {
|
|
|
854
1459
|
}
|
|
855
1460
|
}
|
|
856
1461
|
// Get internal document from cache or load it
|
|
857
|
-
let internalDoc = this.
|
|
1462
|
+
let internalDoc = this.getCachedDocument(docId);
|
|
858
1463
|
if (!internalDoc) {
|
|
859
1464
|
this.logger.debug(`Document ${docId} not in cache, loading from store`);
|
|
860
1465
|
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
@@ -873,7 +1478,7 @@ class BaseMindooDB {
|
|
|
873
1478
|
// Apply the change function
|
|
874
1479
|
const now = Date.now();
|
|
875
1480
|
this.logger.debug(`Applying change function to document ${docId}`);
|
|
876
|
-
this.logger.debug(`Document state before change: heads=${JSON.stringify(Automerge.getHeads(internalDoc.doc))}`);
|
|
1481
|
+
this.logger.debug(`Document state before change: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(internalDoc.doc))}`);
|
|
877
1482
|
// For async callbacks, we need to handle document modifications carefully.
|
|
878
1483
|
// Automerge.change() requires synchronous modifications, so we'll:
|
|
879
1484
|
// 1. Execute the async callback to do any async work (like signing)
|
|
@@ -916,8 +1521,8 @@ class BaseMindooDB {
|
|
|
916
1521
|
pendingDeletions.delete(prop);
|
|
917
1522
|
// Track the change
|
|
918
1523
|
pendingChanges.set(prop, value);
|
|
919
|
-
//
|
|
920
|
-
|
|
1524
|
+
// NOTE: Don't set on target immediately - Automerge requires changes
|
|
1525
|
+
// to be made inside Automerge.change(). Pending changes are applied later.
|
|
921
1526
|
}
|
|
922
1527
|
return true;
|
|
923
1528
|
},
|
|
@@ -928,8 +1533,8 @@ class BaseMindooDB {
|
|
|
928
1533
|
pendingDeletions.add(prop);
|
|
929
1534
|
// Remove from pending changes if it was there
|
|
930
1535
|
pendingChanges.delete(prop);
|
|
931
|
-
//
|
|
932
|
-
|
|
1536
|
+
// NOTE: Don't delete from target immediately - Automerge requires changes
|
|
1537
|
+
// to be made inside Automerge.change(). Pending deletions are applied later.
|
|
933
1538
|
}
|
|
934
1539
|
return true;
|
|
935
1540
|
},
|
|
@@ -1048,7 +1653,7 @@ class BaseMindooDB {
|
|
|
1048
1653
|
// Now apply the collected changes synchronously in Automerge.change()
|
|
1049
1654
|
let newDoc;
|
|
1050
1655
|
try {
|
|
1051
|
-
newDoc = Automerge.change(internalDoc.doc, (automergeDoc) => {
|
|
1656
|
+
newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (automergeDoc) => {
|
|
1052
1657
|
// Apply all pending changes (sets/updates)
|
|
1053
1658
|
for (const [key, value] of pendingChanges) {
|
|
1054
1659
|
automergeDoc[key] = value;
|
|
@@ -1087,7 +1692,7 @@ class BaseMindooDB {
|
|
|
1087
1692
|
// Update lastModified timestamp
|
|
1088
1693
|
automergeDoc._lastModified = now;
|
|
1089
1694
|
});
|
|
1090
|
-
this.logger.debug(`Successfully applied change function, new document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
|
|
1695
|
+
this.logger.debug(`Successfully applied change function, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
|
|
1091
1696
|
}
|
|
1092
1697
|
catch (error) {
|
|
1093
1698
|
this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
|
|
@@ -1095,14 +1700,14 @@ class BaseMindooDB {
|
|
|
1095
1700
|
}
|
|
1096
1701
|
// Get the change bytes from the document
|
|
1097
1702
|
this.logger.debug(`Getting change bytes from document ${docId}`);
|
|
1098
|
-
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
1703
|
+
const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
|
|
1099
1704
|
if (!changeBytes) {
|
|
1100
1705
|
//TODO decide if we just exit here or throw an error
|
|
1101
1706
|
throw new Error("Failed to get change bytes from Automerge document");
|
|
1102
1707
|
}
|
|
1103
1708
|
this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
|
|
1104
1709
|
// Decode the change to get hash and dependencies
|
|
1105
|
-
const decodedChange = Automerge.decodeChange(changeBytes);
|
|
1710
|
+
const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
|
|
1106
1711
|
const automergeHash = decodedChange.hash;
|
|
1107
1712
|
const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
|
|
1108
1713
|
// Encrypt the change payload first
|
|
@@ -1133,7 +1738,7 @@ class BaseMindooDB {
|
|
|
1133
1738
|
contentHash,
|
|
1134
1739
|
docId,
|
|
1135
1740
|
dependencyIds,
|
|
1136
|
-
createdAt:
|
|
1741
|
+
createdAt: now,
|
|
1137
1742
|
createdByPublicKey,
|
|
1138
1743
|
decryptionKeyId: internalDoc.decryptionKeyId,
|
|
1139
1744
|
signature,
|
|
@@ -1151,98 +1756,242 @@ class BaseMindooDB {
|
|
|
1151
1756
|
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
1152
1757
|
// Update cache and index
|
|
1153
1758
|
internalDoc.doc = newDoc;
|
|
1154
|
-
internalDoc.lastModified =
|
|
1155
|
-
this.
|
|
1759
|
+
internalDoc.lastModified = now;
|
|
1760
|
+
this.storeCachedDocument(internalDoc);
|
|
1156
1761
|
this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted);
|
|
1762
|
+
this.markDocDirty(docId);
|
|
1157
1763
|
this.logger.info(`Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
|
|
1764
|
+
// Snapshot creation runs as a best-effort background optimization.
|
|
1765
|
+
await this.maybeWriteSnapshotForDocument(internalDoc, useCustomKey
|
|
1766
|
+
? { signingKeyPair, signingKeyPassword, createdByPublicKey }
|
|
1767
|
+
: { createdByPublicKey });
|
|
1158
1768
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
else {
|
|
1178
|
-
left = mid + 1;
|
|
1179
|
-
startIndex = mid + 1;
|
|
1180
|
-
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Best-effort snapshot scheduling and writing.
|
|
1771
|
+
* A snapshot is written only when enough replay history has accumulated and
|
|
1772
|
+
* a cooldown window has elapsed, to avoid snapshot churn on hot documents.
|
|
1773
|
+
*/
|
|
1774
|
+
async maybeWriteSnapshotForDocument(internalDoc, options) {
|
|
1775
|
+
const docId = internalDoc.id;
|
|
1776
|
+
try {
|
|
1777
|
+
const allMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
1778
|
+
const replayEntries = allMetadata.filter((em) => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
|
|
1779
|
+
const snapshots = allMetadata
|
|
1780
|
+
.filter((em) => em.entryType === "doc_snapshot")
|
|
1781
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
1782
|
+
const latestSnapshot = snapshots[0] || null;
|
|
1783
|
+
const latestSnapshotAt = latestSnapshot?.createdAt ?? 0;
|
|
1784
|
+
const changesSinceSnapshot = replayEntries.filter((em) => em.createdAt > latestSnapshotAt).length;
|
|
1785
|
+
if (changesSinceSnapshot < this.snapshotMinChanges) {
|
|
1786
|
+
return;
|
|
1181
1787
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1788
|
+
if (latestSnapshot &&
|
|
1789
|
+
Date.now() - latestSnapshot.createdAt < this.snapshotCooldownMs) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const headHashes = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
|
|
1793
|
+
const headEntryIds = this.resolveAutomergeDepsToEntryIds(docId, headHashes);
|
|
1794
|
+
const snapshotBytes = automerge_adapter_1.Automerge.save(internalDoc.doc);
|
|
1795
|
+
const encryptedPayload = await this.tenant.encryptPayload(snapshotBytes, internalDoc.decryptionKeyId);
|
|
1796
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
1797
|
+
const pseudoSnapshotHash = `snapshot-${(0, uuid_1.v7)()}`;
|
|
1798
|
+
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, pseudoSnapshotHash, headHashes, this.getSubtle());
|
|
1799
|
+
let signature;
|
|
1800
|
+
if (options.signingKeyPair && options.signingKeyPassword) {
|
|
1801
|
+
signature = await this.tenant.signPayloadWithKey(encryptedPayload, options.signingKeyPair, options.signingKeyPassword);
|
|
1188
1802
|
}
|
|
1803
|
+
else {
|
|
1804
|
+
signature = await this.tenant.signPayload(encryptedPayload);
|
|
1805
|
+
}
|
|
1806
|
+
const snapshotEntry = {
|
|
1807
|
+
entryType: "doc_snapshot",
|
|
1808
|
+
id: entryId,
|
|
1809
|
+
contentHash,
|
|
1810
|
+
docId,
|
|
1811
|
+
dependencyIds: headEntryIds,
|
|
1812
|
+
createdAt: Date.now(),
|
|
1813
|
+
createdByPublicKey: options.createdByPublicKey,
|
|
1814
|
+
decryptionKeyId: internalDoc.decryptionKeyId,
|
|
1815
|
+
snapshotHeadHashes: headHashes,
|
|
1816
|
+
snapshotHeadEntryIds: headEntryIds,
|
|
1817
|
+
signature,
|
|
1818
|
+
originalSize: snapshotBytes.length,
|
|
1819
|
+
encryptedSize: encryptedPayload.length,
|
|
1820
|
+
encryptedData: encryptedPayload,
|
|
1821
|
+
};
|
|
1822
|
+
await this.store.putEntries([snapshotEntry]);
|
|
1823
|
+
this.logger.debug(`Created snapshot for document ${docId} with ${headHashes.length} heads and ${changesSinceSnapshot} changes since previous snapshot`);
|
|
1824
|
+
}
|
|
1825
|
+
catch (error) {
|
|
1826
|
+
this.logger.warn(`Failed to create snapshot for document ${docId}, continuing without snapshot`, error);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
async prefetchIterationWindow(indexSnapshot, startIndex) {
|
|
1830
|
+
if (this.iteratePrefetchWindowDocs <= 0) {
|
|
1831
|
+
return 0;
|
|
1189
1832
|
}
|
|
1190
|
-
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1833
|
+
const docIds = [];
|
|
1834
|
+
const seen = new Set();
|
|
1835
|
+
// Only look ahead a bounded number of uncached docs so iteration does not
|
|
1836
|
+
// materialize the entire remaining changefeed tail up front.
|
|
1837
|
+
for (let i = startIndex; i < indexSnapshot.length && docIds.length < this.iteratePrefetchWindowDocs; i++) {
|
|
1838
|
+
const docId = indexSnapshot[i].docId;
|
|
1839
|
+
if (seen.has(docId) || this.docCache.has(docId)) {
|
|
1840
|
+
continue;
|
|
1196
1841
|
}
|
|
1842
|
+
seen.add(docId);
|
|
1843
|
+
docIds.push(docId);
|
|
1197
1844
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1845
|
+
if (docIds.length === 0) {
|
|
1846
|
+
return 0;
|
|
1847
|
+
}
|
|
1848
|
+
// Prefetch is opportunistic: iteration can still fall back to on-demand
|
|
1849
|
+
// loads, so individual prefetch failures are logged and ignored here.
|
|
1850
|
+
await Promise.all(docIds.map((docId) => this.loadDocumentInternal(docId).catch((err) => {
|
|
1851
|
+
this.logger.warn(`Failed to prefetch document ${docId}:`, err);
|
|
1852
|
+
return null;
|
|
1853
|
+
})));
|
|
1854
|
+
return docIds.length;
|
|
1855
|
+
}
|
|
1856
|
+
getStartIndexForCursor(indexSnapshot, actualCursor) {
|
|
1857
|
+
let startIndex = 0;
|
|
1858
|
+
if (indexSnapshot.length === 0) {
|
|
1859
|
+
return startIndex;
|
|
1860
|
+
}
|
|
1861
|
+
const cursorSeq = typeof actualCursor.changeSeq === "number" ? actualCursor.changeSeq : 0;
|
|
1862
|
+
let left = 0;
|
|
1863
|
+
let right = indexSnapshot.length - 1;
|
|
1864
|
+
while (left <= right) {
|
|
1865
|
+
const mid = Math.floor((left + right) / 2);
|
|
1866
|
+
const entry = indexSnapshot[mid];
|
|
1867
|
+
const cursorComparable = {
|
|
1868
|
+
docId: actualCursor.docId,
|
|
1869
|
+
changeSeq: cursorSeq,
|
|
1870
|
+
};
|
|
1871
|
+
const cmp = this.compareIndexEntries(cursorComparable, entry);
|
|
1872
|
+
if (cmp < 0) {
|
|
1873
|
+
right = mid - 1;
|
|
1874
|
+
startIndex = mid;
|
|
1875
|
+
}
|
|
1876
|
+
else {
|
|
1877
|
+
left = mid + 1;
|
|
1878
|
+
startIndex = mid + 1;
|
|
1208
1879
|
}
|
|
1209
1880
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1881
|
+
if (startIndex < indexSnapshot.length) {
|
|
1882
|
+
const entry = indexSnapshot[startIndex];
|
|
1883
|
+
if (entry.changeSeq === cursorSeq && entry.docId === actualCursor.docId) {
|
|
1884
|
+
startIndex++;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return startIndex;
|
|
1888
|
+
}
|
|
1889
|
+
async *iterateChangeMetadataSince(cursor) {
|
|
1890
|
+
const startedAt = Date.now();
|
|
1891
|
+
const actualCursor = cursor ?? {
|
|
1892
|
+
changeSeq: 0,
|
|
1893
|
+
lastModified: 0,
|
|
1894
|
+
docId: "",
|
|
1895
|
+
};
|
|
1896
|
+
const indexSnapshot = [...this.index];
|
|
1897
|
+
const startIndex = this.getStartIndexForCursor(indexSnapshot, actualCursor);
|
|
1898
|
+
let yieldedDocuments = 0;
|
|
1899
|
+
try {
|
|
1900
|
+
for (let i = startIndex; i < indexSnapshot.length; i++) {
|
|
1901
|
+
const entry = indexSnapshot[i];
|
|
1231
1902
|
const currentCursor = {
|
|
1903
|
+
changeSeq: entry.changeSeq,
|
|
1232
1904
|
lastModified: entry.lastModified,
|
|
1233
1905
|
docId: entry.docId,
|
|
1234
1906
|
};
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1907
|
+
yieldedDocuments++;
|
|
1908
|
+
yield {
|
|
1909
|
+
docId: entry.docId,
|
|
1910
|
+
lastModified: entry.lastModified,
|
|
1911
|
+
isDeleted: entry.isDeleted,
|
|
1912
|
+
cursor: currentCursor,
|
|
1913
|
+
};
|
|
1238
1914
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1915
|
+
}
|
|
1916
|
+
finally {
|
|
1917
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
1918
|
+
operation: "iterateChangeMetadataSince",
|
|
1919
|
+
time: Date.now() - startedAt,
|
|
1920
|
+
details: {
|
|
1921
|
+
yieldedDocuments,
|
|
1922
|
+
startIndex,
|
|
1923
|
+
},
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
async *iterateChangesSince(cursor) {
|
|
1928
|
+
const startedAt = Date.now();
|
|
1929
|
+
// Default to initial cursor if null is provided.
|
|
1930
|
+
// Prefer deterministic sequence-based cursoring; keep legacy fallback compatibility.
|
|
1931
|
+
const actualCursor = cursor ?? { changeSeq: 0, lastModified: 0, docId: "" };
|
|
1932
|
+
this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
|
|
1933
|
+
// Use a stable snapshot of the index for this generator run so concurrent
|
|
1934
|
+
// updates do not reorder/skip entries while iterating.
|
|
1935
|
+
const indexSnapshot = [...this.index];
|
|
1936
|
+
// Find starting position using binary search
|
|
1937
|
+
// We want to find the first entry that is greater than the cursor.
|
|
1938
|
+
const startIndex = this.getStartIndexForCursor(indexSnapshot, actualCursor);
|
|
1939
|
+
let prefetchedDocuments = await this.prefetchIterationWindow(indexSnapshot, startIndex);
|
|
1940
|
+
let yieldedDocuments = 0;
|
|
1941
|
+
let loadedDocuments = 0;
|
|
1942
|
+
try {
|
|
1943
|
+
// Iterate through the stable snapshot and yield documents one at a time.
|
|
1944
|
+
for (let i = startIndex; i < indexSnapshot.length; i++) {
|
|
1945
|
+
const entry = indexSnapshot[i];
|
|
1946
|
+
try {
|
|
1947
|
+
this.logger.debug(`Yielding document ${entry.docId} from index (lastModified: ${entry.lastModified}, isDeleted: ${entry.isDeleted})`);
|
|
1948
|
+
let internalDoc = this.getCachedDocument(entry.docId);
|
|
1949
|
+
if (!internalDoc) {
|
|
1950
|
+
this.logger.debug(`Document ${entry.docId} not in cache, loading from store`);
|
|
1951
|
+
internalDoc = await this.loadDocumentInternal(entry.docId);
|
|
1952
|
+
if (internalDoc) {
|
|
1953
|
+
loadedDocuments++;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (!internalDoc) {
|
|
1957
|
+
this.logger.warn(`Document ${entry.docId} not found, skipping`);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
// Wrap the document (works for both deleted and non-deleted documents)
|
|
1961
|
+
const doc = this.wrapDocument(internalDoc);
|
|
1962
|
+
this.logger.debug(`Successfully loaded document ${entry.docId} (isDeleted: ${doc.isDeleted()})`);
|
|
1963
|
+
// Create cursor for current document
|
|
1964
|
+
const currentCursor = {
|
|
1965
|
+
changeSeq: entry.changeSeq,
|
|
1966
|
+
lastModified: entry.lastModified,
|
|
1967
|
+
docId: entry.docId,
|
|
1968
|
+
};
|
|
1969
|
+
// Yield immediately - this allows the caller to break early after each document
|
|
1970
|
+
// Deleted documents are included so external indexes can handle deletions
|
|
1971
|
+
yieldedDocuments++;
|
|
1972
|
+
yield { doc, cursor: currentCursor };
|
|
1973
|
+
prefetchedDocuments += await this.prefetchIterationWindow(indexSnapshot, i + 1);
|
|
1974
|
+
}
|
|
1975
|
+
catch (error) {
|
|
1976
|
+
this.logger.error(`Error processing document ${entry.docId}:`, error);
|
|
1977
|
+
// Stop processing on error
|
|
1978
|
+
throw error;
|
|
1979
|
+
}
|
|
1243
1980
|
}
|
|
1244
1981
|
}
|
|
1245
|
-
|
|
1982
|
+
finally {
|
|
1983
|
+
this.logger.debug(`Iteration completed`);
|
|
1984
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
1985
|
+
operation: "iterateChangesSince",
|
|
1986
|
+
time: Date.now() - startedAt,
|
|
1987
|
+
details: {
|
|
1988
|
+
yieldedDocuments,
|
|
1989
|
+
prefetchedDocuments,
|
|
1990
|
+
loadedDocuments,
|
|
1991
|
+
startIndex,
|
|
1992
|
+
},
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1246
1995
|
}
|
|
1247
1996
|
/**
|
|
1248
1997
|
* Incrementally update a cached document with new entries.
|
|
@@ -1256,7 +2005,7 @@ class BaseMindooDB {
|
|
|
1256
2005
|
}
|
|
1257
2006
|
this.logger.debug(`Applying ${newEntryMetadata.length} new entries to cached document ${docId}`);
|
|
1258
2007
|
// Get current document heads to check if it changes
|
|
1259
|
-
const headsBefore = Automerge.getHeads(cachedDoc.doc);
|
|
2008
|
+
const headsBefore = automerge_adapter_1.Automerge.getHeads(cachedDoc.doc);
|
|
1260
2009
|
// Filter entries to only include change entries (exclude snapshots)
|
|
1261
2010
|
const entriesToApply = newEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
|
|
1262
2011
|
if (entriesToApply.length === 0) {
|
|
@@ -1340,12 +2089,12 @@ class BaseMindooDB {
|
|
|
1340
2089
|
// Clone the cached document to avoid "outdated document" error
|
|
1341
2090
|
// This happens when the document has been wrapped and returned to the user
|
|
1342
2091
|
// Automerge marks documents as outdated when they're accessed, preventing direct mutation
|
|
1343
|
-
const clonedDoc = Automerge.clone(cachedDoc.doc);
|
|
2092
|
+
const clonedDoc = automerge_adapter_1.Automerge.clone(cachedDoc.doc);
|
|
1344
2093
|
// Apply all changes at once (Automerge handles dependency ordering)
|
|
1345
|
-
const result = Automerge.applyChanges(clonedDoc, changeBytes);
|
|
2094
|
+
const result = automerge_adapter_1.Automerge.applyChanges(clonedDoc, changeBytes);
|
|
1346
2095
|
const updatedDoc = result[0];
|
|
1347
2096
|
// Check if document actually changed
|
|
1348
|
-
const headsAfter = Automerge.getHeads(updatedDoc);
|
|
2097
|
+
const headsAfter = automerge_adapter_1.Automerge.getHeads(updatedDoc);
|
|
1349
2098
|
const headsChanged = JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
|
|
1350
2099
|
if (!headsChanged) {
|
|
1351
2100
|
this.logger.debug(`Document ${docId} heads unchanged after applying new entries`);
|
|
@@ -1369,57 +2118,113 @@ class BaseMindooDB {
|
|
|
1369
2118
|
isDeleted,
|
|
1370
2119
|
};
|
|
1371
2120
|
// Update cache
|
|
1372
|
-
this.
|
|
2121
|
+
this.storeCachedDocument(updatedInternalDoc);
|
|
2122
|
+
this.markDocDirty(docId);
|
|
1373
2123
|
return updatedInternalDoc;
|
|
1374
2124
|
}
|
|
1375
2125
|
/**
|
|
1376
2126
|
* Internal method to load a document from the content-addressed store
|
|
1377
2127
|
*/
|
|
1378
2128
|
async loadDocumentInternal(docId) {
|
|
2129
|
+
const startedAt = Date.now();
|
|
2130
|
+
const cacheCheckStartedAt = Date.now();
|
|
1379
2131
|
// Check cache first
|
|
1380
2132
|
if (this.docCache.has(docId)) {
|
|
1381
2133
|
this.logger.debug(`Document ${docId} found in cache, returning cached version`);
|
|
1382
|
-
|
|
2134
|
+
const cached = this.getCachedDocument(docId);
|
|
2135
|
+
this.performanceCallback?.onDocumentLoad?.({
|
|
2136
|
+
docId,
|
|
2137
|
+
cacheHit: true,
|
|
2138
|
+
metadataEntriesScanned: 0,
|
|
2139
|
+
replayEntriesLoaded: 0,
|
|
2140
|
+
snapshotUsed: false,
|
|
2141
|
+
cacheCheckTime: Date.now() - cacheCheckStartedAt,
|
|
2142
|
+
storeQueryTime: 0,
|
|
2143
|
+
entryLoadTime: 0,
|
|
2144
|
+
signatureVerificationTime: 0,
|
|
2145
|
+
decryptionTime: 0,
|
|
2146
|
+
automergeTime: 0,
|
|
2147
|
+
totalTime: Date.now() - startedAt,
|
|
2148
|
+
});
|
|
2149
|
+
return cached;
|
|
1383
2150
|
}
|
|
1384
2151
|
this.logger.debug(`===== Starting to load document ${docId} from store =====`);
|
|
2152
|
+
const cacheCheckTime = Date.now() - cacheCheckStartedAt;
|
|
2153
|
+
let storeQueryTime = 0;
|
|
2154
|
+
let entryLoadTime = 0;
|
|
2155
|
+
let signatureVerificationTime = 0;
|
|
2156
|
+
let decryptionTime = 0;
|
|
2157
|
+
let automergeTime = 0;
|
|
1385
2158
|
// Get all entry metadata for this document
|
|
1386
2159
|
// TODO: Implement loading from last snapshot if available
|
|
1387
2160
|
this.logger.debug(`Getting all entry hashes for document ${docId}`);
|
|
1388
|
-
const
|
|
2161
|
+
const storeQueryStartedAt = Date.now();
|
|
2162
|
+
const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
|
|
2163
|
+
storeQueryTime += Date.now() - storeQueryStartedAt;
|
|
1389
2164
|
this.logger.debug(`Found ${allEntryMetadata.length} total entry hashes for document ${docId}`);
|
|
1390
2165
|
if (allEntryMetadata.length === 0) {
|
|
1391
2166
|
this.logger.debug(`No entry hashes found for document ${docId}, returning null`);
|
|
2167
|
+
this.performanceCallback?.onDocumentLoad?.({
|
|
2168
|
+
docId,
|
|
2169
|
+
cacheHit: false,
|
|
2170
|
+
metadataEntriesScanned: 0,
|
|
2171
|
+
replayEntriesLoaded: 0,
|
|
2172
|
+
snapshotUsed: false,
|
|
2173
|
+
cacheCheckTime,
|
|
2174
|
+
storeQueryTime,
|
|
2175
|
+
entryLoadTime,
|
|
2176
|
+
signatureVerificationTime,
|
|
2177
|
+
decryptionTime,
|
|
2178
|
+
automergeTime,
|
|
2179
|
+
totalTime: Date.now() - startedAt,
|
|
2180
|
+
});
|
|
1392
2181
|
return null;
|
|
1393
2182
|
}
|
|
1394
2183
|
// Log all entry types
|
|
1395
2184
|
const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
|
|
1396
2185
|
this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
2186
|
+
const metadataById = new Map(allEntryMetadata.map((meta) => [meta.id, meta]));
|
|
2187
|
+
const planStartedAt = Date.now();
|
|
2188
|
+
const materializationPlan = (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, allEntryMetadata, {
|
|
2189
|
+
includeDiagnostics: true,
|
|
2190
|
+
});
|
|
2191
|
+
const planTime = Date.now() - planStartedAt;
|
|
2192
|
+
this.performanceCallback?.onSyncOperation?.({
|
|
2193
|
+
operation: "planDocumentMaterialization",
|
|
2194
|
+
time: planTime,
|
|
2195
|
+
details: {
|
|
2196
|
+
docId,
|
|
2197
|
+
metadataEntriesScanned: allEntryMetadata.length,
|
|
2198
|
+
replayEntriesLoaded: materializationPlan.entryIdsToApply.length,
|
|
2199
|
+
snapshotEntryId: materializationPlan.snapshotEntryId,
|
|
2200
|
+
diagnostics: materializationPlan.diagnostics,
|
|
2201
|
+
},
|
|
2202
|
+
});
|
|
2203
|
+
let startFromSnapshot = materializationPlan.snapshotEntryId !== null;
|
|
2204
|
+
const snapshotMeta = materializationPlan.snapshotEntryId
|
|
2205
|
+
? (metadataById.get(materializationPlan.snapshotEntryId) || null)
|
|
2206
|
+
: null;
|
|
2207
|
+
if (startFromSnapshot && !snapshotMeta) {
|
|
2208
|
+
this.logger.warn(`Planner referenced snapshot ${materializationPlan.snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
|
|
2209
|
+
startFromSnapshot = false;
|
|
2210
|
+
}
|
|
2211
|
+
if (startFromSnapshot && snapshotMeta) {
|
|
2212
|
+
this.logger.debug(`Planner selected snapshot ${snapshotMeta.id} for ${docId}`);
|
|
1408
2213
|
}
|
|
1409
2214
|
else {
|
|
1410
|
-
this.logger.debug(`
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
: allEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
|
|
1417
|
-
this.logger.debug(`Will load ${entriesToLoad.length} entries for document ${docId} (after snapshot filter)`);
|
|
2215
|
+
this.logger.debug(`Planner did not select a snapshot for ${docId}`);
|
|
2216
|
+
}
|
|
2217
|
+
const entriesToLoad = materializationPlan.entryIdsToApply
|
|
2218
|
+
.map((id) => metadataById.get(id))
|
|
2219
|
+
.filter((m) => m !== undefined);
|
|
2220
|
+
this.logger.debug(`Planner returned ${entriesToLoad.length} replay entries for ${docId}; diagnostics=${JSON.stringify(materializationPlan.diagnostics || {})}`);
|
|
1418
2221
|
// Load the snapshot first if we have one
|
|
1419
2222
|
let doc = undefined;
|
|
1420
2223
|
if (startFromSnapshot && snapshotMeta) {
|
|
1421
2224
|
this.logger.debug(`Loading snapshot for document ${docId}`);
|
|
2225
|
+
const snapshotLoadStartedAt = Date.now();
|
|
1422
2226
|
const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
|
|
2227
|
+
entryLoadTime += Date.now() - snapshotLoadStartedAt;
|
|
1423
2228
|
this.logger.debug(`Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
|
|
1424
2229
|
if (snapshotEntries.length > 0) {
|
|
1425
2230
|
const snapshotData = snapshotEntries[0];
|
|
@@ -1431,7 +2236,9 @@ class BaseMindooDB {
|
|
|
1431
2236
|
else {
|
|
1432
2237
|
// Verify signature against the encrypted snapshot (no decryption needed)
|
|
1433
2238
|
// We sign the encrypted payload, so anyone can verify signatures without decryption keys
|
|
2239
|
+
const signatureStartedAt = Date.now();
|
|
1434
2240
|
isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
|
|
2241
|
+
signatureVerificationTime += Date.now() - signatureStartedAt;
|
|
1435
2242
|
}
|
|
1436
2243
|
if (!isValid) {
|
|
1437
2244
|
this.logger.warn(`Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
|
|
@@ -1441,14 +2248,18 @@ class BaseMindooDB {
|
|
|
1441
2248
|
else {
|
|
1442
2249
|
this.logger.debug(`Snapshot signature valid, decrypting snapshot`);
|
|
1443
2250
|
// Decrypt snapshot (only after signature verification passes)
|
|
2251
|
+
const decryptStartedAt = Date.now();
|
|
1444
2252
|
const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
|
|
2253
|
+
decryptionTime += Date.now() - decryptStartedAt;
|
|
1445
2254
|
this.logger.debug(`Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
|
|
1446
2255
|
// Load snapshot using Automerge.load()
|
|
1447
2256
|
// This deserializes a full document snapshot from binary data
|
|
1448
2257
|
// According to Automerge docs: load() is equivalent to init() followed by loadIncremental()
|
|
1449
2258
|
this.logger.debug(`Loading snapshot into Automerge document`);
|
|
1450
|
-
|
|
1451
|
-
|
|
2259
|
+
const automergeStartedAt = Date.now();
|
|
2260
|
+
doc = automerge_adapter_1.Automerge.load(decryptedSnapshot);
|
|
2261
|
+
automergeTime += Date.now() - automergeStartedAt;
|
|
2262
|
+
this.logger.debug(`Successfully loaded snapshot, document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1452
2263
|
// Register the snapshot's automerge hash -> entry ID mapping
|
|
1453
2264
|
const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
|
|
1454
2265
|
if (parsed) {
|
|
@@ -1460,19 +2271,18 @@ class BaseMindooDB {
|
|
|
1460
2271
|
// If we don't have a snapshot, start from scratch
|
|
1461
2272
|
if (!doc) {
|
|
1462
2273
|
this.logger.debug(`Initializing new Automerge document for ${docId}`);
|
|
1463
|
-
doc = Automerge.init();
|
|
1464
|
-
this.logger.debug(`Initialized empty document, heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
2274
|
+
doc = automerge_adapter_1.Automerge.init();
|
|
2275
|
+
this.logger.debug(`Initialized empty document, heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1465
2276
|
}
|
|
1466
|
-
// Sort entries by timestamp
|
|
1467
|
-
entriesToLoad.sort((a, b) => a.createdAt - b.createdAt);
|
|
1468
|
-
this.logger.debug(`Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
|
|
1469
2277
|
// Load and apply all entries
|
|
1470
2278
|
this.logger.debug(`Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
|
|
2279
|
+
const entryLoadStartedAt = Date.now();
|
|
1471
2280
|
const entries = await this.store.getEntries(entriesToLoad.map(em => em.id));
|
|
2281
|
+
entryLoadTime += Date.now() - entryLoadStartedAt;
|
|
1472
2282
|
this.logger.debug(`Retrieved ${entries.length} entries from store for document ${docId}`);
|
|
1473
2283
|
this.logger.debug(`Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? 'starting from snapshot' : 'starting from scratch'})`);
|
|
1474
2284
|
// Log current document state before applying entries
|
|
1475
|
-
this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
2285
|
+
this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1476
2286
|
// Filter entries for admin-only mode first
|
|
1477
2287
|
const validEntries = entries.filter(entryData => {
|
|
1478
2288
|
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
@@ -1508,6 +2318,7 @@ class BaseMindooDB {
|
|
|
1508
2318
|
}
|
|
1509
2319
|
}
|
|
1510
2320
|
// Verify all signatures in parallel using cached keys
|
|
2321
|
+
const signatureStartedAt = Date.now();
|
|
1511
2322
|
const signatureVerificationResults = await Promise.all(validEntries.map(async (entryData) => {
|
|
1512
2323
|
const cryptoKey = keyMap.get(entryData.createdByPublicKey);
|
|
1513
2324
|
if (!cryptoKey) {
|
|
@@ -1517,6 +2328,7 @@ class BaseMindooDB {
|
|
|
1517
2328
|
const isValid = await this.verifySignatureWithKey(cryptoKey, entryData.encryptedData, entryData.signature);
|
|
1518
2329
|
return { entryData, isValid };
|
|
1519
2330
|
}));
|
|
2331
|
+
signatureVerificationTime += Date.now() - signatureStartedAt;
|
|
1520
2332
|
// Filter out entries with invalid signatures
|
|
1521
2333
|
const verifiedEntries = signatureVerificationResults
|
|
1522
2334
|
.filter(({ isValid }) => {
|
|
@@ -1533,10 +2345,12 @@ class BaseMindooDB {
|
|
|
1533
2345
|
// Parallel decryption - decrypt all entries concurrently
|
|
1534
2346
|
// Automerge handles dependency buffering internally, so we can decrypt all in parallel
|
|
1535
2347
|
this.logger.debug(`Decrypting ${verifiedEntries.length} entries in parallel`);
|
|
2348
|
+
const decryptionStartedAt = Date.now();
|
|
1536
2349
|
const decryptionResults = await Promise.all(verifiedEntries.map(async (entryData) => {
|
|
1537
2350
|
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
1538
2351
|
return { entryData, decryptedPayload };
|
|
1539
2352
|
}));
|
|
2353
|
+
decryptionTime += Date.now() - decryptionStartedAt;
|
|
1540
2354
|
// Collect change bytes and register automerge hash mappings
|
|
1541
2355
|
const changeBytes = [];
|
|
1542
2356
|
for (const { entryData, decryptedPayload } of decryptionResults) {
|
|
@@ -1551,10 +2365,12 @@ class BaseMindooDB {
|
|
|
1551
2365
|
if (changeBytes.length > 0) {
|
|
1552
2366
|
this.logger.debug(`Applying ${changeBytes.length} changes to document ${docId} using batch applyChanges`);
|
|
1553
2367
|
try {
|
|
1554
|
-
const
|
|
2368
|
+
const automergeStartedAt = Date.now();
|
|
2369
|
+
const result = automerge_adapter_1.Automerge.applyChanges(doc, changeBytes);
|
|
1555
2370
|
doc = result[0];
|
|
2371
|
+
automergeTime += Date.now() - automergeStartedAt;
|
|
1556
2372
|
this.logger.debug(`Successfully applied ${changeBytes.length} changes to document ${docId}`);
|
|
1557
|
-
this.logger.debug(`Document state after applying changes: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
2373
|
+
this.logger.debug(`Document state after applying changes: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1558
2374
|
}
|
|
1559
2375
|
catch (error) {
|
|
1560
2376
|
this.logger.error(`Error applying changes to document ${docId}:`, error);
|
|
@@ -1566,13 +2382,16 @@ class BaseMindooDB {
|
|
|
1566
2382
|
}
|
|
1567
2383
|
// Extract metadata from document (doc is guaranteed to be defined at this point)
|
|
1568
2384
|
this.logger.debug(`All entries applied successfully for document ${docId}`);
|
|
1569
|
-
this.logger.debug(`Final document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
2385
|
+
this.logger.debug(`Final document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
|
|
1570
2386
|
const payload = doc;
|
|
1571
2387
|
// Check if document was deleted by looking for a "doc_delete" type entry
|
|
1572
2388
|
const hasDeleteEntry = allEntryMetadata.some(em => em.entryType === "doc_delete");
|
|
1573
2389
|
const isDeleted = hasDeleteEntry;
|
|
1574
2390
|
this.logger.debug(`Document ${docId} isDeleted: ${isDeleted}`);
|
|
1575
|
-
|
|
2391
|
+
// The authoritative decryptionKeyId comes from the doc_create entry's metadata,
|
|
2392
|
+
// not from the Automerge payload (which does not store encryption metadata).
|
|
2393
|
+
const createEntry = allEntryMetadata.find(em => em.entryType === "doc_create");
|
|
2394
|
+
const decryptionKeyId = createEntry ? createEntry.decryptionKeyId : "default";
|
|
1576
2395
|
// Get lastModified from payload, or use the timestamp of the last entry
|
|
1577
2396
|
const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
1578
2397
|
const lastModified = payload._lastModified ||
|
|
@@ -1590,10 +2409,69 @@ class BaseMindooDB {
|
|
|
1590
2409
|
isDeleted,
|
|
1591
2410
|
};
|
|
1592
2411
|
// Update cache
|
|
1593
|
-
this.
|
|
2412
|
+
this.storeCachedDocument(internalDoc);
|
|
2413
|
+
this.markDocDirty(docId);
|
|
1594
2414
|
this.logger.debug(`===== Successfully loaded document ${docId} and cached it =====`);
|
|
2415
|
+
this.performanceCallback?.onDocumentLoad?.({
|
|
2416
|
+
docId,
|
|
2417
|
+
cacheHit: false,
|
|
2418
|
+
metadataEntriesScanned: allEntryMetadata.length,
|
|
2419
|
+
replayEntriesLoaded: entriesToLoad.length,
|
|
2420
|
+
snapshotUsed: startFromSnapshot && snapshotMeta !== null,
|
|
2421
|
+
cacheCheckTime,
|
|
2422
|
+
storeQueryTime,
|
|
2423
|
+
entryLoadTime,
|
|
2424
|
+
signatureVerificationTime,
|
|
2425
|
+
decryptionTime,
|
|
2426
|
+
automergeTime,
|
|
2427
|
+
totalTime: Date.now() - startedAt,
|
|
2428
|
+
});
|
|
1595
2429
|
return internalDoc;
|
|
1596
2430
|
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Convert an Automerge document to a plain JS object, converting Text objects to strings.
|
|
2433
|
+
* If using native Automerge (react-native-automerge-generated), this uses the native
|
|
2434
|
+
* materialize() method which properly converts Text objects to strings.
|
|
2435
|
+
* Falls back to direct access if native backend is not available.
|
|
2436
|
+
*/
|
|
2437
|
+
convertAutomergeToJS(doc) {
|
|
2438
|
+
// Check if this document has a native Automerge handle attached
|
|
2439
|
+
// The native implementation attaches metadata with Symbol.for('_am_meta')
|
|
2440
|
+
const STATE = Symbol.for('_am_meta');
|
|
2441
|
+
const meta = doc[STATE];
|
|
2442
|
+
if (meta && meta.handle && typeof meta.handle.materialize === 'function') {
|
|
2443
|
+
// Use native materialize() which properly converts Text objects to strings
|
|
2444
|
+
try {
|
|
2445
|
+
const materialized = meta.handle.materialize('/');
|
|
2446
|
+
return materialized;
|
|
2447
|
+
}
|
|
2448
|
+
catch (error) {
|
|
2449
|
+
console.error('[MindooDB] Failed to materialize document:', error);
|
|
2450
|
+
// Fall through to direct access
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
// Fallback: direct access (for WASM or if native fails)
|
|
2454
|
+
const result = {};
|
|
2455
|
+
const keys = Object.keys(doc);
|
|
2456
|
+
for (const key of keys) {
|
|
2457
|
+
const value = doc[key];
|
|
2458
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Uint8Array)) {
|
|
2459
|
+
result[key] = this.convertAutomergeToJS(value);
|
|
2460
|
+
}
|
|
2461
|
+
else if (Array.isArray(value)) {
|
|
2462
|
+
result[key] = value.map(item => {
|
|
2463
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Uint8Array)) {
|
|
2464
|
+
return this.convertAutomergeToJS(item);
|
|
2465
|
+
}
|
|
2466
|
+
return item;
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
else {
|
|
2470
|
+
result[key] = value;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return result;
|
|
2474
|
+
}
|
|
1597
2475
|
/**
|
|
1598
2476
|
* Wrap an internal document in the MindooDoc interface.
|
|
1599
2477
|
* This is the read-only wrapper - write methods throw errors.
|
|
@@ -1626,7 +2504,11 @@ class BaseMindooDB {
|
|
|
1626
2504
|
getCreatedAt: () => internalDoc.createdAt,
|
|
1627
2505
|
getLastModified: () => internalDoc.lastModified,
|
|
1628
2506
|
isDeleted: () => internalDoc.isDeleted,
|
|
1629
|
-
getData: () =>
|
|
2507
|
+
getData: () => {
|
|
2508
|
+
// Convert Automerge document to plain JS object, converting Text objects to strings
|
|
2509
|
+
const jsDoc = this.convertAutomergeToJS(internalDoc.doc);
|
|
2510
|
+
return createReadOnlyProxy(jsDoc);
|
|
2511
|
+
},
|
|
1630
2512
|
// ========== Attachment Write Methods ==========
|
|
1631
2513
|
// These throw errors in the read-only wrapper
|
|
1632
2514
|
addAttachment: async () => {
|
|
@@ -1662,7 +2544,7 @@ class BaseMindooDB {
|
|
|
1662
2544
|
* Get an attachment reference by ID from a document's _attachments array.
|
|
1663
2545
|
*/
|
|
1664
2546
|
getAttachmentRefInternal(docId, attachmentId) {
|
|
1665
|
-
const internalDoc = this.
|
|
2547
|
+
const internalDoc = this.getCachedDocument(docId);
|
|
1666
2548
|
if (!internalDoc) {
|
|
1667
2549
|
throw new Error(`Document ${docId} not found in cache`);
|
|
1668
2550
|
}
|
|
@@ -2003,7 +2885,64 @@ class BaseMindooDB {
|
|
|
2003
2885
|
};
|
|
2004
2886
|
}
|
|
2005
2887
|
/**
|
|
2006
|
-
*
|
|
2888
|
+
* Resolve a sync target to a ContentAddressedStore.
|
|
2889
|
+
* Accepts either a raw store or a MindooDB instance (calls getStore()).
|
|
2890
|
+
*/
|
|
2891
|
+
resolveStore(remote) {
|
|
2892
|
+
if ('getStore' in remote && typeof remote.getStore === 'function') {
|
|
2893
|
+
return remote.getStore();
|
|
2894
|
+
}
|
|
2895
|
+
return remote;
|
|
2896
|
+
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Apply a temporary network-auth identity override for a single sync call.
|
|
2899
|
+
*
|
|
2900
|
+
* Purpose:
|
|
2901
|
+
* - Allows per-call authentication as a different user (for example admin bootstrap)
|
|
2902
|
+
* without changing the tenant's default connected user.
|
|
2903
|
+
* - Returns a cleanup callback so caller can always restore default auth state in `finally`.
|
|
2904
|
+
*/
|
|
2905
|
+
async applyNetworkAuthOverrideForSync(remoteStore, options) {
|
|
2906
|
+
// Per-call override: use an alternate identity (for example, admin bootstrap)
|
|
2907
|
+
// only for this sync operation.
|
|
2908
|
+
const override = options?.networkAuthOverride;
|
|
2909
|
+
if (!override) {
|
|
2910
|
+
// No override requested: return a no-op cleanup callback for unified call sites.
|
|
2911
|
+
return () => { };
|
|
2912
|
+
}
|
|
2913
|
+
const overrideCapableStore = remoteStore;
|
|
2914
|
+
if (typeof overrideCapableStore.setSyncAuthOverride !== "function") {
|
|
2915
|
+
// Local/in-memory stores do not support network auth override; keep default auth.
|
|
2916
|
+
this.logger.warn("networkAuthOverride was provided, but remote store does not support auth override");
|
|
2917
|
+
return () => { };
|
|
2918
|
+
}
|
|
2919
|
+
const subtle = this.tenant.getCryptoAdapter().getSubtle();
|
|
2920
|
+
// Decrypt and import override signing key (Ed25519) for challenge signing.
|
|
2921
|
+
const signingKeyBuffer = await this.tenant.decryptPrivateKey(override.user.userSigningKeyPair.privateKey, override.password, "signing");
|
|
2922
|
+
const signingKey = await subtle.importKey("pkcs8", signingKeyBuffer, { name: "Ed25519" }, false, ["sign"]);
|
|
2923
|
+
// Decrypt and import override encryption key (RSA-OAEP) so encrypted network
|
|
2924
|
+
// entries can be decrypted with the same override identity.
|
|
2925
|
+
const encryptionKeyBuffer = await this.tenant.decryptPrivateKey(override.user.userEncryptionKeyPair.privateKey, override.password, "encryption");
|
|
2926
|
+
const encryptionKey = await subtle.importKey("pkcs8", encryptionKeyBuffer, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
|
|
2927
|
+
// Activate temporary override on the remote store.
|
|
2928
|
+
overrideCapableStore.setSyncAuthOverride({
|
|
2929
|
+
username: override.user.username,
|
|
2930
|
+
signingKey,
|
|
2931
|
+
privateEncryptionKey: encryptionKey,
|
|
2932
|
+
});
|
|
2933
|
+
// Return cleanup callback so caller can always restore normal auth in finally.
|
|
2934
|
+
return () => {
|
|
2935
|
+
if (typeof overrideCapableStore.clearSyncAuthOverride === "function") {
|
|
2936
|
+
overrideCapableStore.clearSyncAuthOverride();
|
|
2937
|
+
}
|
|
2938
|
+
else {
|
|
2939
|
+
// Backward-compatible fallback if explicit clear API is not implemented.
|
|
2940
|
+
overrideCapableStore.setSyncAuthOverride?.(null);
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* Pull changes from a remote content-addressed store or another MindooDB instance.
|
|
2007
2946
|
*
|
|
2008
2947
|
* This method:
|
|
2009
2948
|
* 1. Finds entries in the remote store that we don't have locally
|
|
@@ -2011,69 +2950,93 @@ class BaseMindooDB {
|
|
|
2011
2950
|
* 3. Stores them in our local store
|
|
2012
2951
|
* 4. Syncs the local store to process the new entries
|
|
2013
2952
|
*
|
|
2014
|
-
* @param
|
|
2015
|
-
* @
|
|
2953
|
+
* @param remote The remote store or MindooDB instance to pull entries from
|
|
2954
|
+
* @param options Optional sync options for progress tracking, paging, and cancellation
|
|
2955
|
+
* @return A promise that resolves with the sync result
|
|
2016
2956
|
*/
|
|
2017
|
-
async pullChangesFrom(
|
|
2957
|
+
async pullChangesFrom(remote, options) {
|
|
2958
|
+
const remoteStore = this.resolveStore(remote);
|
|
2018
2959
|
if (this.store.getId() !== remoteStore.getId()) {
|
|
2019
2960
|
throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
|
|
2020
2961
|
}
|
|
2021
2962
|
this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}`);
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2963
|
+
const restoreAuthOverride = await this.applyNetworkAuthOverrideForSync(remoteStore, options);
|
|
2964
|
+
try {
|
|
2965
|
+
const syncResult = await this.syncEntriesFromStore(remoteStore, this.store, options);
|
|
2966
|
+
this.logger.debug(`Transferred ${syncResult.transferred} entries from remote store`);
|
|
2967
|
+
if (syncResult.cancelled) {
|
|
2968
|
+
this.logger.info(`Pull cancelled after transferring ${syncResult.transferred} entries`);
|
|
2969
|
+
return { transferredEntries: syncResult.transferred, cancelled: true };
|
|
2970
|
+
}
|
|
2971
|
+
if (syncResult.transferred === 0) {
|
|
2972
|
+
this.logger.debug(`No new entries to pull`);
|
|
2973
|
+
return { transferredEntries: 0, cancelled: false };
|
|
2974
|
+
}
|
|
2975
|
+
options?.onProgress?.({
|
|
2976
|
+
phase: 'processing',
|
|
2977
|
+
message: `Processing ${syncResult.transferred} new entries...`,
|
|
2978
|
+
transferredEntries: syncResult.transferred,
|
|
2979
|
+
scannedEntries: syncResult.transferred,
|
|
2980
|
+
});
|
|
2981
|
+
// Sync the local store to process the new entries
|
|
2982
|
+
// This will update the index, cache, and processedEntryIds
|
|
2983
|
+
await this.syncStoreChanges();
|
|
2984
|
+
this.logger.info(`Pull complete, synced ${syncResult.transferred} entries`);
|
|
2985
|
+
options?.onProgress?.({
|
|
2986
|
+
phase: 'complete',
|
|
2987
|
+
message: `Pull complete: ${syncResult.transferred} entries synced`,
|
|
2988
|
+
transferredEntries: syncResult.transferred,
|
|
2989
|
+
scannedEntries: syncResult.transferred,
|
|
2990
|
+
});
|
|
2991
|
+
return { transferredEntries: syncResult.transferred, cancelled: false };
|
|
2992
|
+
}
|
|
2993
|
+
finally {
|
|
2994
|
+
restoreAuthOverride();
|
|
2031
2995
|
}
|
|
2032
|
-
// Get the full entries from the remote store
|
|
2033
|
-
const newEntries = await remoteStore.getEntries(newEntryMetadata.map(em => em.id));
|
|
2034
|
-
this.logger.debug(`Retrieved ${newEntries.length} entries from remote store`);
|
|
2035
|
-
// Store all entries in our local store
|
|
2036
|
-
// The putEntries method handles deduplication (no-op if entry already exists)
|
|
2037
|
-
await this.store.putEntries(newEntries);
|
|
2038
|
-
this.logger.debug(`Stored ${newEntries.length} entries in local store`);
|
|
2039
|
-
// Sync the local store to process the new entries
|
|
2040
|
-
// This will update the index, cache, and processedEntryIds
|
|
2041
|
-
await this.syncStoreChanges();
|
|
2042
|
-
this.logger.info(`Pull complete, synced ${newEntries.length} entries`);
|
|
2043
2996
|
}
|
|
2044
2997
|
/**
|
|
2045
|
-
* Push changes to a remote content-addressed store.
|
|
2998
|
+
* Push changes to a remote content-addressed store or another MindooDB instance.
|
|
2046
2999
|
*
|
|
2047
3000
|
* This method:
|
|
2048
3001
|
* 1. Finds entries in our local store that the remote doesn't have
|
|
2049
3002
|
* 2. Retrieves those entries from our local store
|
|
2050
3003
|
* 3. Stores them in the remote store
|
|
2051
3004
|
*
|
|
2052
|
-
* @param
|
|
2053
|
-
* @
|
|
3005
|
+
* @param remote The remote store or MindooDB instance to push entries to
|
|
3006
|
+
* @param options Optional sync options for progress tracking, paging, and cancellation
|
|
3007
|
+
* @return A promise that resolves with the sync result
|
|
2054
3008
|
*/
|
|
2055
|
-
async pushChangesTo(
|
|
3009
|
+
async pushChangesTo(remote, options) {
|
|
3010
|
+
const remoteStore = this.resolveStore(remote);
|
|
2056
3011
|
if (this.store.getId() !== remoteStore.getId()) {
|
|
2057
3012
|
throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
|
|
2058
3013
|
}
|
|
2059
3014
|
this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}`);
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
3015
|
+
const restoreAuthOverride = await this.applyNetworkAuthOverrideForSync(remoteStore, options);
|
|
3016
|
+
try {
|
|
3017
|
+
const syncResult = await this.syncEntriesFromStore(this.store, remoteStore, options);
|
|
3018
|
+
this.logger.debug(`Transferred ${syncResult.transferred} entries to remote store`);
|
|
3019
|
+
if (syncResult.cancelled) {
|
|
3020
|
+
this.logger.info(`Push cancelled after transferring ${syncResult.transferred} entries`);
|
|
3021
|
+
return { transferredEntries: syncResult.transferred, cancelled: true };
|
|
3022
|
+
}
|
|
3023
|
+
if (syncResult.transferred === 0) {
|
|
3024
|
+
this.logger.debug(`No new entries to push`);
|
|
3025
|
+
}
|
|
3026
|
+
else {
|
|
3027
|
+
this.logger.info(`Pushed ${syncResult.transferred} entries to remote store`);
|
|
3028
|
+
}
|
|
3029
|
+
options?.onProgress?.({
|
|
3030
|
+
phase: 'complete',
|
|
3031
|
+
message: `Push complete: ${syncResult.transferred} entries transferred`,
|
|
3032
|
+
transferredEntries: syncResult.transferred,
|
|
3033
|
+
scannedEntries: syncResult.transferred,
|
|
3034
|
+
});
|
|
3035
|
+
return { transferredEntries: syncResult.transferred, cancelled: false };
|
|
3036
|
+
}
|
|
3037
|
+
finally {
|
|
3038
|
+
restoreAuthOverride();
|
|
2069
3039
|
}
|
|
2070
|
-
// Get the full entries from our local store
|
|
2071
|
-
const newEntries = await this.store.getEntries(newEntryMetadata.map(em => em.id));
|
|
2072
|
-
this.logger.debug(`Retrieved ${newEntries.length} entries from local store`);
|
|
2073
|
-
// Store all entries in the remote store
|
|
2074
|
-
// The putEntries method handles deduplication (no-op if entry already exists)
|
|
2075
|
-
await remoteStore.putEntries(newEntries);
|
|
2076
|
-
this.logger.info(`Pushed ${newEntries.length} entries to remote store`);
|
|
2077
3040
|
}
|
|
2078
3041
|
}
|
|
2079
3042
|
exports.BaseMindooDB = BaseMindooDB;
|