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
|
@@ -0,0 +1,1358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node.js filesystem-backed implementation of {@link ContentAddressedStore}.
|
|
4
|
+
*
|
|
5
|
+
* This module provides a persistent, crash-resilient content-addressed store
|
|
6
|
+
* designed for the MindooDB sync protocol. Each database gets its own
|
|
7
|
+
* directory subtree where entry metadata (JSON) and encrypted payloads (binary)
|
|
8
|
+
* are stored as individual files. An optional in-memory index accelerates
|
|
9
|
+
* lookups, cursor-based scans, and bloom-filter generation for efficient
|
|
10
|
+
* network sync.
|
|
11
|
+
*
|
|
12
|
+
* ### On-disk layout
|
|
13
|
+
*
|
|
14
|
+
* ```
|
|
15
|
+
* <basePath>/<dbId>/
|
|
16
|
+
* entries/ one JSON file per entry id (source of truth)
|
|
17
|
+
* content/ one .bin file per unique contentHash
|
|
18
|
+
* metadata-index.json compact snapshot of the in-memory index
|
|
19
|
+
* metadata-segments/ append-only mutation log (upsert / delete)
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ### Durability & crash safety
|
|
23
|
+
*
|
|
24
|
+
* Every file write follows a write-tmp → fsync → atomic-rename → dir-fsync
|
|
25
|
+
* protocol so readers never observe partially written data. Metadata segments
|
|
26
|
+
* provide a low-write-amplification WAL that is periodically compacted into a
|
|
27
|
+
* full snapshot.
|
|
28
|
+
*
|
|
29
|
+
* ### Indexing
|
|
30
|
+
*
|
|
31
|
+
* When `indexingEnabled` is true (the default), the store maintains three
|
|
32
|
+
* in-memory structures:
|
|
33
|
+
* - `entries` — Map<id, metadata> for O(1) point lookups
|
|
34
|
+
* - `docIndex` — Map<docId, Set<id>> for fast per-document queries
|
|
35
|
+
* - `orderedMetadata` — sorted array for O(log N) cursor scans
|
|
36
|
+
*
|
|
37
|
+
* On startup the index is reconstructed from (snapshot + segments), then
|
|
38
|
+
* validated against the authoritative entry files.
|
|
39
|
+
*
|
|
40
|
+
* @module BasicOnDiskContentAddressedStore
|
|
41
|
+
*/
|
|
42
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
43
|
+
if (k2 === undefined) k2 = k;
|
|
44
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
45
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
46
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
47
|
+
}
|
|
48
|
+
Object.defineProperty(o, k2, desc);
|
|
49
|
+
}) : (function(o, m, k, k2) {
|
|
50
|
+
if (k2 === undefined) k2 = k;
|
|
51
|
+
o[k2] = m[k];
|
|
52
|
+
}));
|
|
53
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
54
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
55
|
+
}) : function(o, v) {
|
|
56
|
+
o["default"] = v;
|
|
57
|
+
});
|
|
58
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
59
|
+
var ownKeys = function(o) {
|
|
60
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
61
|
+
var ar = [];
|
|
62
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
return ownKeys(o);
|
|
66
|
+
};
|
|
67
|
+
return function (mod) {
|
|
68
|
+
if (mod && mod.__esModule) return mod;
|
|
69
|
+
var result = {};
|
|
70
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
71
|
+
__setModuleDefault(result, mod);
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
})();
|
|
75
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76
|
+
exports.BasicOnDiskContentAddressedStoreFactory = exports.BasicOnDiskContentAddressedStore = void 0;
|
|
77
|
+
const promises_1 = require("fs/promises");
|
|
78
|
+
const fs_1 = require("fs");
|
|
79
|
+
const path = __importStar(require("path"));
|
|
80
|
+
const bloom_1 = require("../../core/appendonlystores/bloom");
|
|
81
|
+
const MaterializationPlanner_1 = require("../../core/appendonlystores/MaterializationPlanner");
|
|
82
|
+
const logging_1 = require("../../core/logging");
|
|
83
|
+
/**
|
|
84
|
+
* When the number of uncompacted segment files reaches this threshold,
|
|
85
|
+
* the store will persist a fresh metadata-index snapshot and delete the
|
|
86
|
+
* covered segment files. Set to 0 to disable file-count–based compaction.
|
|
87
|
+
*/
|
|
88
|
+
const DEFAULT_METADATA_SEGMENT_COMPACTION_MIN_FILES = 32;
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Serialization helpers
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Convert a runtime {@link StoreEntryMetadata} to its JSON-safe form.
|
|
94
|
+
* The only transformation is encoding the binary `signature` as base64.
|
|
95
|
+
*/
|
|
96
|
+
function serializeMetadata(metadata) {
|
|
97
|
+
return {
|
|
98
|
+
...metadata,
|
|
99
|
+
signature: Buffer.from(metadata.signature).toString("base64"),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Restore a {@link StoreEntryMetadata} from its JSON-safe serialized form.
|
|
104
|
+
* Decodes the base64 `signature` back into a `Uint8Array`.
|
|
105
|
+
*/
|
|
106
|
+
function deserializeMetadata(serialized) {
|
|
107
|
+
return {
|
|
108
|
+
...serialized,
|
|
109
|
+
signature: new Uint8Array(Buffer.from(serialized.signature, "base64")),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Filename helpers
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
/**
|
|
116
|
+
* Derive the on-disk filename for an entry's metadata file.
|
|
117
|
+
* The entry id is URI-encoded so it is safe for any filesystem.
|
|
118
|
+
*/
|
|
119
|
+
function toEntryFileName(id) {
|
|
120
|
+
return `${encodeURIComponent(id)}.json`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Recover an entry id from its on-disk metadata filename.
|
|
124
|
+
* Reverses the encoding applied by {@link toEntryFileName}.
|
|
125
|
+
*/
|
|
126
|
+
function fromEntryFileName(fileName) {
|
|
127
|
+
const withoutExt = fileName.endsWith(".json") ? fileName.slice(0, -5) : fileName;
|
|
128
|
+
return decodeURIComponent(withoutExt);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Generate a temporary file path for atomic writes.
|
|
132
|
+
*
|
|
133
|
+
* The suffix includes the process PID, current timestamp, and a random token
|
|
134
|
+
* to avoid collisions when multiple processes or rapid sequential writes
|
|
135
|
+
* target the same final path.
|
|
136
|
+
*/
|
|
137
|
+
function tempPathFor(finalPath) {
|
|
138
|
+
return `${finalPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Node.js persistent ContentAddressedStore implementation.
|
|
142
|
+
*
|
|
143
|
+
* On-disk layout per database id:
|
|
144
|
+
*
|
|
145
|
+
* <basePath>/<dbId>/
|
|
146
|
+
* entries/ // one metadata JSON file per entry id
|
|
147
|
+
* content/ // one binary payload file per contentHash
|
|
148
|
+
* metadata-index.json // compact snapshot of ordered metadata (optional)
|
|
149
|
+
* metadata-segments/ // append-only metadata mutation log (optional)
|
|
150
|
+
*
|
|
151
|
+
* Design goals:
|
|
152
|
+
* - Strong visibility: readers only observe committed files (atomic rename).
|
|
153
|
+
* - Crash resilience: fsync + rename + directory fsync for durability.
|
|
154
|
+
* - High sync/read throughput: in-memory ordered index + cursor scan.
|
|
155
|
+
* - Low write amplification: append metadata segments, compact periodically.
|
|
156
|
+
*/
|
|
157
|
+
class BasicOnDiskContentAddressedStore {
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Constructor
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/**
|
|
162
|
+
* Create a new on-disk content-addressed store for the given database id.
|
|
163
|
+
*
|
|
164
|
+
* Initialization (directory creation, index loading) happens asynchronously.
|
|
165
|
+
* All public methods internally await the init promise so callers do not
|
|
166
|
+
* need to wait explicitly — but {@link awaitIndexReady} can be used to
|
|
167
|
+
* observe progress.
|
|
168
|
+
*
|
|
169
|
+
* @param dbId Unique database identifier (becomes the directory name).
|
|
170
|
+
* @param logger Optional logger; a default console logger is created if omitted.
|
|
171
|
+
* @param options Store configuration (base path, indexing, compaction thresholds, etc.).
|
|
172
|
+
*/
|
|
173
|
+
constructor(dbId, logger, options) {
|
|
174
|
+
/**
|
|
175
|
+
* Set of segment file names that have been replayed into the in-memory
|
|
176
|
+
* index (or were appended by this process). Only these files are eligible
|
|
177
|
+
* for compaction — files that appeared concurrently from another process
|
|
178
|
+
* are excluded until they have been replayed.
|
|
179
|
+
*/
|
|
180
|
+
this.appliedMetadataSegmentFiles = new Set();
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// In-memory indexes (populated only when indexingEnabled = true)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
/**
|
|
185
|
+
* Primary index: entry id -> full metadata.
|
|
186
|
+
* Provides O(1) point lookups for `hasEntries`, `getEntries`,
|
|
187
|
+
* `readMetadataById`, and dependency traversal.
|
|
188
|
+
*/
|
|
189
|
+
this.entries = new Map();
|
|
190
|
+
/**
|
|
191
|
+
* Secondary index: docId -> set of entry ids belonging to that document.
|
|
192
|
+
* Enables fast per-document queries in `findNewEntriesForDoc`.
|
|
193
|
+
*/
|
|
194
|
+
this.docIndex = new Map();
|
|
195
|
+
/**
|
|
196
|
+
* Reference count per content hash. Tracks how many entry ids share the
|
|
197
|
+
* same payload blob, used during `purgeDocHistory` to decide whether a
|
|
198
|
+
* content file can be safely deleted.
|
|
199
|
+
*/
|
|
200
|
+
this.contentRefCount = new Map();
|
|
201
|
+
/**
|
|
202
|
+
* All metadata sorted by `(createdAt ASC, id ASC)`.
|
|
203
|
+
* Supports O(log N) lower-bound binary search for cursor-based scans
|
|
204
|
+
* in {@link scanEntriesSince}.
|
|
205
|
+
*/
|
|
206
|
+
this.orderedMetadata = [];
|
|
207
|
+
this.dbId = dbId;
|
|
208
|
+
this.logger =
|
|
209
|
+
logger ||
|
|
210
|
+
new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), `BasicOnDiskStore:${dbId}`, true);
|
|
211
|
+
this.storeRoot = path.join(options?.basePath?.toString() || ".mindoodb-store", dbId);
|
|
212
|
+
this.entriesDir = path.join(this.storeRoot, "entries");
|
|
213
|
+
this.contentDir = path.join(this.storeRoot, "content");
|
|
214
|
+
this.metadataIndexPath = path.join(this.storeRoot, "metadata-index.json");
|
|
215
|
+
this.metadataSegmentsDir = path.join(this.storeRoot, "metadata-segments");
|
|
216
|
+
this.indexingEnabled = options?.indexingEnabled !== false;
|
|
217
|
+
const compactionOption = options?.metadataSegmentCompactionMinFiles;
|
|
218
|
+
this.metadataSegmentCompactionMinFiles =
|
|
219
|
+
compactionOption === undefined || compactionOption === null
|
|
220
|
+
? DEFAULT_METADATA_SEGMENT_COMPACTION_MIN_FILES
|
|
221
|
+
: Math.max(0, Math.floor(Number(compactionOption)));
|
|
222
|
+
const compactionMaxBytesOption = options?.metadataSegmentCompactionMaxBytes;
|
|
223
|
+
this.metadataSegmentCompactionMaxBytes =
|
|
224
|
+
compactionMaxBytesOption === undefined || compactionMaxBytesOption === null
|
|
225
|
+
? 0
|
|
226
|
+
: Math.max(0, Math.floor(Number(compactionMaxBytesOption)));
|
|
227
|
+
this.compactionStatus = {
|
|
228
|
+
enabled: this.indexingEnabled &&
|
|
229
|
+
(this.metadataSegmentCompactionMinFiles > 0 || this.metadataSegmentCompactionMaxBytes > 0),
|
|
230
|
+
compactionMinFiles: this.metadataSegmentCompactionMinFiles,
|
|
231
|
+
compactionMaxBytes: this.metadataSegmentCompactionMaxBytes,
|
|
232
|
+
totalCompactions: 0,
|
|
233
|
+
totalCompactedFiles: 0,
|
|
234
|
+
totalCompactedBytes: 0,
|
|
235
|
+
totalCompactionDurationMs: 0,
|
|
236
|
+
lastCompactionAt: null,
|
|
237
|
+
lastCompactedFiles: 0,
|
|
238
|
+
lastCompactedBytes: 0,
|
|
239
|
+
lastCompactionDurationMs: 0,
|
|
240
|
+
};
|
|
241
|
+
this.indexStatus = {
|
|
242
|
+
phase: this.indexingEnabled ? "building" : "ready",
|
|
243
|
+
indexingEnabled: this.indexingEnabled,
|
|
244
|
+
progress01: this.indexingEnabled ? 0 : 1,
|
|
245
|
+
};
|
|
246
|
+
this.initPromise = this.initialize(options);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* One-time async initialization performed in the constructor's microtask.
|
|
250
|
+
*
|
|
251
|
+
* Steps:
|
|
252
|
+
* 1. Optionally wipe all local data (if `clearLocalDataOnStartup`).
|
|
253
|
+
* 2. Ensure on-disk directory structure exists.
|
|
254
|
+
* 3. When indexing is enabled:
|
|
255
|
+
* a. Attempt to load the persisted metadata-index snapshot (fast path).
|
|
256
|
+
* b. Replay any append-only segment files written since the snapshot.
|
|
257
|
+
* c. Validate the resulting in-memory index against the authoritative
|
|
258
|
+
* entry files on disk.
|
|
259
|
+
* d. If validation fails (stale snapshot, corruption, etc.), do a full
|
|
260
|
+
* rebuild from the entry files and write a fresh snapshot.
|
|
261
|
+
*/
|
|
262
|
+
async initialize(options) {
|
|
263
|
+
if (options?.clearLocalDataOnStartup) {
|
|
264
|
+
await this.clearAllLocalDataInternal();
|
|
265
|
+
}
|
|
266
|
+
await (0, promises_1.mkdir)(this.entriesDir, { recursive: true });
|
|
267
|
+
await (0, promises_1.mkdir)(this.contentDir, { recursive: true });
|
|
268
|
+
await (0, promises_1.mkdir)(this.metadataSegmentsDir, { recursive: true });
|
|
269
|
+
if (!this.indexingEnabled) {
|
|
270
|
+
this.indexStatus = {
|
|
271
|
+
phase: "ready",
|
|
272
|
+
indexingEnabled: false,
|
|
273
|
+
progress01: 1,
|
|
274
|
+
};
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Startup path:
|
|
278
|
+
// 1) load snapshot (fast path)
|
|
279
|
+
// 2) replay append-only segments (apply recent mutations)
|
|
280
|
+
// 3) validate against authoritative entry files
|
|
281
|
+
// 4) rebuild from entry files when snapshot/segments are stale/corrupt
|
|
282
|
+
await this.tryLoadPersistedIndex();
|
|
283
|
+
await this.replayMetadataSegments();
|
|
284
|
+
const indexCoverageOk = await this.validatePersistedIndexCoverage();
|
|
285
|
+
if (!indexCoverageOk) {
|
|
286
|
+
await this.rebuildInMemoryIndexes();
|
|
287
|
+
await this.persistMetadataIndex();
|
|
288
|
+
await this.clearMetadataSegments(Array.from(this.appliedMetadataSegmentFiles));
|
|
289
|
+
}
|
|
290
|
+
this.indexStatus = {
|
|
291
|
+
phase: "ready",
|
|
292
|
+
indexingEnabled: true,
|
|
293
|
+
progress01: 1,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Block until async initialization has completed. Called by every public method. */
|
|
297
|
+
async ensureInitialized() {
|
|
298
|
+
await this.initPromise;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Unconditionally remove the entire store directory tree and reset all
|
|
302
|
+
* in-memory state. Called by the constructor when `clearLocalDataOnStartup`
|
|
303
|
+
* is set, and by {@link clearAllLocalData} at runtime.
|
|
304
|
+
*/
|
|
305
|
+
async clearAllLocalDataInternal() {
|
|
306
|
+
await (0, promises_1.rm)(this.storeRoot, { recursive: true, force: true });
|
|
307
|
+
this.entries.clear();
|
|
308
|
+
this.docIndex.clear();
|
|
309
|
+
this.contentRefCount.clear();
|
|
310
|
+
this.orderedMetadata = [];
|
|
311
|
+
this.appliedMetadataSegmentFiles.clear();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Full index rebuild from the authoritative entry files on disk.
|
|
315
|
+
*
|
|
316
|
+
* Clears all in-memory structures, reads every `entries/*.json` file,
|
|
317
|
+
* populates the primary, secondary, and ref-count indexes, and finally
|
|
318
|
+
* sorts `orderedMetadata` by `(createdAt, id)`.
|
|
319
|
+
*
|
|
320
|
+
* This is the slowest startup path (O(N) filesystem reads) and is only
|
|
321
|
+
* used when the persisted snapshot + segments cannot be validated.
|
|
322
|
+
*/
|
|
323
|
+
async rebuildInMemoryIndexes() {
|
|
324
|
+
this.entries.clear();
|
|
325
|
+
this.docIndex.clear();
|
|
326
|
+
this.contentRefCount.clear();
|
|
327
|
+
this.orderedMetadata = [];
|
|
328
|
+
const files = await this.listEntryFiles();
|
|
329
|
+
for (const fileName of files) {
|
|
330
|
+
const metadata = await this.readMetadataByFileName(fileName);
|
|
331
|
+
if (!metadata) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
this.entries.set(metadata.id, metadata);
|
|
335
|
+
if (!this.docIndex.has(metadata.docId)) {
|
|
336
|
+
this.docIndex.set(metadata.docId, new Set());
|
|
337
|
+
}
|
|
338
|
+
this.docIndex.get(metadata.docId).add(metadata.id);
|
|
339
|
+
this.contentRefCount.set(metadata.contentHash, (this.contentRefCount.get(metadata.contentHash) || 0) + 1);
|
|
340
|
+
this.orderedMetadata.push(metadata);
|
|
341
|
+
}
|
|
342
|
+
this.orderedMetadata.sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt - b.createdAt));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Attempt to hydrate the in-memory index from the persisted
|
|
346
|
+
* `metadata-index.json` snapshot.
|
|
347
|
+
*
|
|
348
|
+
* This is the fast startup path: a single file read replaces the need to
|
|
349
|
+
* enumerate and parse every individual entry file.
|
|
350
|
+
*
|
|
351
|
+
* @returns `true` if the snapshot was loaded successfully; `false` if it
|
|
352
|
+
* does not exist or could not be parsed (caller should fall back
|
|
353
|
+
* to a full rebuild).
|
|
354
|
+
*/
|
|
355
|
+
async tryLoadPersistedIndex() {
|
|
356
|
+
if (!(await this.fileExists(this.metadataIndexPath))) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const raw = await (0, promises_1.readFile)(this.metadataIndexPath, "utf-8");
|
|
361
|
+
const serialized = JSON.parse(raw);
|
|
362
|
+
const loaded = serialized.map((s) => deserializeMetadata(s));
|
|
363
|
+
this.entries.clear();
|
|
364
|
+
this.docIndex.clear();
|
|
365
|
+
this.contentRefCount.clear();
|
|
366
|
+
this.orderedMetadata = [];
|
|
367
|
+
for (const metadata of loaded) {
|
|
368
|
+
this.entries.set(metadata.id, metadata);
|
|
369
|
+
if (!this.docIndex.has(metadata.docId)) {
|
|
370
|
+
this.docIndex.set(metadata.docId, new Set());
|
|
371
|
+
}
|
|
372
|
+
this.docIndex.get(metadata.docId).add(metadata.id);
|
|
373
|
+
this.contentRefCount.set(metadata.contentHash, (this.contentRefCount.get(metadata.contentHash) || 0) + 1);
|
|
374
|
+
this.orderedMetadata.push(metadata);
|
|
375
|
+
}
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
this.logger.warn(`Failed to load metadata index, rebuilding from entry files: ${String(err)}`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Validate that the in-memory index (loaded from snapshot + segments)
|
|
385
|
+
* is consistent with the authoritative entry files on disk.
|
|
386
|
+
*
|
|
387
|
+
* Checks two invariants:
|
|
388
|
+
* 1. The number of entry files matches `this.entries.size`.
|
|
389
|
+
* 2. Every entry file's id exists in the in-memory map.
|
|
390
|
+
*
|
|
391
|
+
* This is a cheap O(N) directory listing + set-membership check (no file
|
|
392
|
+
* reads) that catches stale snapshots, missing segments, and external
|
|
393
|
+
* mutations that happened while the process was down.
|
|
394
|
+
*
|
|
395
|
+
* @returns `true` if the index fully covers the on-disk entries.
|
|
396
|
+
*/
|
|
397
|
+
async validatePersistedIndexCoverage() {
|
|
398
|
+
if (!this.indexingEnabled) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
// The entry files are the source of truth.
|
|
402
|
+
// Snapshot+segments are only an acceleration structure.
|
|
403
|
+
const files = await this.listEntryFiles();
|
|
404
|
+
if (files.length !== this.entries.size) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
for (const fileName of files) {
|
|
408
|
+
const entryId = fromEntryFileName(fileName);
|
|
409
|
+
if (!this.entries.has(entryId)) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Write a full metadata-index snapshot to disk.
|
|
417
|
+
*
|
|
418
|
+
* Serializes the entire `orderedMetadata` array to `metadata-index.json`
|
|
419
|
+
* using an atomic write. After this, all segment files that have been
|
|
420
|
+
* replayed into the in-memory state are redundant and can be deleted.
|
|
421
|
+
*/
|
|
422
|
+
async persistMetadataIndex() {
|
|
423
|
+
if (!this.indexingEnabled) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const serialized = JSON.stringify(this.orderedMetadata.map((m) => serializeMetadata(m)));
|
|
427
|
+
await this.writeFileAtomic(this.metadataIndexPath, serialized);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Delete the given segment files from disk and remove them from the
|
|
431
|
+
* `appliedMetadataSegmentFiles` tracking set.
|
|
432
|
+
*
|
|
433
|
+
* Deletion failures (e.g. race with another process) are silently ignored.
|
|
434
|
+
*/
|
|
435
|
+
async clearMetadataSegments(files) {
|
|
436
|
+
for (const fileName of files) {
|
|
437
|
+
const filePath = path.join(this.metadataSegmentsDir, fileName);
|
|
438
|
+
await (0, promises_1.unlink)(filePath).catch(() => {
|
|
439
|
+
// Ignore races or already-deleted files.
|
|
440
|
+
});
|
|
441
|
+
this.appliedMetadataSegmentFiles.delete(fileName);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* List all `.json` segment files in the segments directory, sorted
|
|
446
|
+
* lexicographically. Since segment filenames are prefixed with a
|
|
447
|
+
* millisecond timestamp, lexicographic order approximates chronological
|
|
448
|
+
* append order.
|
|
449
|
+
*/
|
|
450
|
+
async listMetadataSegmentFiles() {
|
|
451
|
+
try {
|
|
452
|
+
const files = await (0, promises_1.readdir)(this.metadataSegmentsDir);
|
|
453
|
+
return files.filter((f) => f.endsWith(".json")).sort();
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Replay all segment files into the in-memory index.
|
|
461
|
+
*
|
|
462
|
+
* Each segment file contains a JSON array of upsert/delete records.
|
|
463
|
+
* They are applied in lexicographic filename order (≈ chronological order)
|
|
464
|
+
* on top of whatever state the snapshot already provided.
|
|
465
|
+
*
|
|
466
|
+
* Successfully replayed files are tracked in `appliedMetadataSegmentFiles`
|
|
467
|
+
* so they become eligible for compaction. Corrupt or unreadable segment
|
|
468
|
+
* files are warned about and skipped — the subsequent
|
|
469
|
+
* {@link validatePersistedIndexCoverage} step will catch any resulting
|
|
470
|
+
* inconsistencies.
|
|
471
|
+
*/
|
|
472
|
+
async replayMetadataSegments() {
|
|
473
|
+
// Segments are replayed in lexicographic filename order.
|
|
474
|
+
// Segment names encode timestamp first, so lexical order approximates append order.
|
|
475
|
+
const files = await this.listMetadataSegmentFiles();
|
|
476
|
+
for (const fileName of files) {
|
|
477
|
+
const filePath = path.join(this.metadataSegmentsDir, fileName);
|
|
478
|
+
try {
|
|
479
|
+
const raw = await (0, promises_1.readFile)(filePath, "utf-8");
|
|
480
|
+
const records = JSON.parse(raw);
|
|
481
|
+
for (const record of records) {
|
|
482
|
+
if (record.op === "upsert") {
|
|
483
|
+
const metadata = deserializeMetadata(record.metadata);
|
|
484
|
+
this.applyMetadataUpsert(metadata);
|
|
485
|
+
}
|
|
486
|
+
else if (record.op === "delete") {
|
|
487
|
+
this.applyMetadataDelete(record.id);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
this.appliedMetadataSegmentFiles.add(fileName);
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
this.logger.warn(`Failed to replay metadata segment ${fileName}: ${String(err)}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Append a new segment file containing the given mutation records.
|
|
499
|
+
*
|
|
500
|
+
* Segment files act as a lightweight write-ahead log: rather than
|
|
501
|
+
* rewriting the entire metadata-index snapshot on every mutation, we
|
|
502
|
+
* append a small file. This keeps per-write I/O proportional to the
|
|
503
|
+
* batch size rather than total store size.
|
|
504
|
+
*
|
|
505
|
+
* After writing, triggers {@link maybeCompactMetadataSegments} to check
|
|
506
|
+
* whether the accumulated segments should be folded into a fresh snapshot.
|
|
507
|
+
*/
|
|
508
|
+
async appendMetadataSegment(records) {
|
|
509
|
+
if (!this.indexingEnabled || records.length === 0) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// Name carries time + pid + random suffix to minimize cross-process collisions.
|
|
513
|
+
const segmentName = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}.json`;
|
|
514
|
+
const segmentPath = path.join(this.metadataSegmentsDir, segmentName);
|
|
515
|
+
await this.writeFileAtomic(segmentPath, JSON.stringify(records));
|
|
516
|
+
this.appliedMetadataSegmentFiles.add(segmentName);
|
|
517
|
+
await this.maybeCompactMetadataSegments();
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Conditionally compact metadata segments when a threshold is exceeded.
|
|
521
|
+
*
|
|
522
|
+
* Two independent thresholds can trigger compaction:
|
|
523
|
+
* - **File count**: the number of applied segment files ≥
|
|
524
|
+
* `metadataSegmentCompactionMinFiles`.
|
|
525
|
+
* - **Total bytes**: the cumulative size of applied segment files ≥
|
|
526
|
+
* `metadataSegmentCompactionMaxBytes`.
|
|
527
|
+
*
|
|
528
|
+
* Compaction writes a fresh metadata-index snapshot (which already
|
|
529
|
+
* includes all replayed segment data) and then deletes the covered
|
|
530
|
+
* segment files. Only files tracked in `appliedMetadataSegmentFiles`
|
|
531
|
+
* are compacted — files written concurrently by another process are
|
|
532
|
+
* left untouched.
|
|
533
|
+
*
|
|
534
|
+
* Compaction statistics are accumulated in `this.compactionStatus`.
|
|
535
|
+
*/
|
|
536
|
+
async maybeCompactMetadataSegments() {
|
|
537
|
+
if (this.metadataSegmentCompactionMinFiles <= 0 &&
|
|
538
|
+
this.metadataSegmentCompactionMaxBytes <= 0) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const allSegmentFiles = await this.listMetadataSegmentFiles();
|
|
542
|
+
// Only compact files we have actually replayed or appended.
|
|
543
|
+
// This avoids deleting a file that appeared concurrently but is not yet represented
|
|
544
|
+
// in our in-memory state/snapshot.
|
|
545
|
+
const compactableFiles = allSegmentFiles.filter((fileName) => this.appliedMetadataSegmentFiles.has(fileName));
|
|
546
|
+
if (compactableFiles.length === 0) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const compactByFileCount = this.metadataSegmentCompactionMinFiles > 0 &&
|
|
550
|
+
compactableFiles.length >= this.metadataSegmentCompactionMinFiles;
|
|
551
|
+
const compactableBytes = this.metadataSegmentCompactionMaxBytes > 0
|
|
552
|
+
? await this.computeSegmentFilesTotalBytes(compactableFiles)
|
|
553
|
+
: 0;
|
|
554
|
+
const compactByBytes = this.metadataSegmentCompactionMaxBytes > 0 &&
|
|
555
|
+
compactableBytes >= this.metadataSegmentCompactionMaxBytes;
|
|
556
|
+
if (!compactByFileCount && !compactByBytes) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const compactedFiles = compactableFiles.length;
|
|
560
|
+
const compactedBytes = compactableBytes > 0 ? compactableBytes : await this.computeSegmentFilesTotalBytes(compactableFiles);
|
|
561
|
+
const startedAt = Date.now();
|
|
562
|
+
// Compaction = materialize full snapshot, then delete covered segments.
|
|
563
|
+
await this.persistMetadataIndex();
|
|
564
|
+
await this.clearMetadataSegments(compactableFiles);
|
|
565
|
+
const durationMs = Date.now() - startedAt;
|
|
566
|
+
this.compactionStatus.totalCompactions += 1;
|
|
567
|
+
this.compactionStatus.totalCompactedFiles += compactedFiles;
|
|
568
|
+
this.compactionStatus.totalCompactedBytes += compactedBytes;
|
|
569
|
+
this.compactionStatus.totalCompactionDurationMs += durationMs;
|
|
570
|
+
this.compactionStatus.lastCompactionAt = Date.now();
|
|
571
|
+
this.compactionStatus.lastCompactedFiles = compactedFiles;
|
|
572
|
+
this.compactionStatus.lastCompactedBytes = compactedBytes;
|
|
573
|
+
this.compactionStatus.lastCompactionDurationMs = durationMs;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Sum the on-disk sizes of the given segment files.
|
|
577
|
+
* Used to evaluate the byte-based compaction threshold.
|
|
578
|
+
* Files that have been deleted between listing and stat are silently skipped.
|
|
579
|
+
*/
|
|
580
|
+
async computeSegmentFilesTotalBytes(files) {
|
|
581
|
+
let totalBytes = 0;
|
|
582
|
+
for (const fileName of files) {
|
|
583
|
+
const filePath = path.join(this.metadataSegmentsDir, fileName);
|
|
584
|
+
try {
|
|
585
|
+
const stats = await (0, promises_1.stat)(filePath);
|
|
586
|
+
totalBytes += stats.size;
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// Ignore races where a segment vanished after listing.
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return totalBytes;
|
|
593
|
+
}
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
// In-memory index helpers
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
/**
|
|
598
|
+
* Compare two metadata entries by the canonical sort key `(createdAt, id)`.
|
|
599
|
+
* Used to maintain and query the `orderedMetadata` sorted array.
|
|
600
|
+
*/
|
|
601
|
+
compareMetadata(a, b) {
|
|
602
|
+
if (a.createdAt !== b.createdAt) {
|
|
603
|
+
return a.createdAt - b.createdAt;
|
|
604
|
+
}
|
|
605
|
+
return a.id.localeCompare(b.id);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Binary search for the first index in `orderedMetadata` that is strictly
|
|
609
|
+
* greater than the given cursor position `(createdAt, id)`.
|
|
610
|
+
*
|
|
611
|
+
* Returns 0 when cursor is `null` (start from the beginning).
|
|
612
|
+
* Runs in O(log N) time.
|
|
613
|
+
*/
|
|
614
|
+
lowerBoundForCursor(cursor) {
|
|
615
|
+
if (cursor === null) {
|
|
616
|
+
return 0;
|
|
617
|
+
}
|
|
618
|
+
let left = 0;
|
|
619
|
+
let right = this.orderedMetadata.length;
|
|
620
|
+
while (left < right) {
|
|
621
|
+
const mid = Math.floor((left + right) / 2);
|
|
622
|
+
const candidate = this.orderedMetadata[mid];
|
|
623
|
+
const greater = candidate.createdAt > cursor.createdAt ||
|
|
624
|
+
(candidate.createdAt === cursor.createdAt && candidate.id > cursor.id);
|
|
625
|
+
if (greater) {
|
|
626
|
+
right = mid;
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
left = mid + 1;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return left;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Test whether a metadata entry passes the given scan filters.
|
|
636
|
+
* Evaluates docId, entryTypes whitelist, and creationDate range.
|
|
637
|
+
* Returns `true` when no filters are provided or all checks pass.
|
|
638
|
+
*/
|
|
639
|
+
metadataMatchesFilters(meta, filters) {
|
|
640
|
+
if (filters?.docId && meta.docId !== filters.docId) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
if (filters?.entryTypes && filters.entryTypes.length > 0 && !filters.entryTypes.includes(meta.entryType)) {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
if (filters?.creationDateFrom !== undefined && filters.creationDateFrom !== null && meta.createdAt < filters.creationDateFrom) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
if (filters?.creationDateUntil !== undefined && filters.creationDateUntil !== null && meta.createdAt >= filters.creationDateUntil) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Insert a metadata entry into `orderedMetadata` at the correct sorted
|
|
656
|
+
* position using binary search + splice. O(log N) search + O(N) splice.
|
|
657
|
+
*/
|
|
658
|
+
insertOrderedMetadata(meta) {
|
|
659
|
+
let left = 0;
|
|
660
|
+
let right = this.orderedMetadata.length;
|
|
661
|
+
while (left < right) {
|
|
662
|
+
const mid = Math.floor((left + right) / 2);
|
|
663
|
+
if (this.compareMetadata(this.orderedMetadata[mid], meta) < 0) {
|
|
664
|
+
left = mid + 1;
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
right = mid;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
this.orderedMetadata.splice(left, 0, meta);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Remove an entry from `orderedMetadata` by id.
|
|
674
|
+
* Uses a linear scan; acceptable because removals (purge) are rare.
|
|
675
|
+
*/
|
|
676
|
+
removeOrderedMetadataById(id) {
|
|
677
|
+
const idx = this.orderedMetadata.findIndex((m) => m.id === id);
|
|
678
|
+
if (idx >= 0) {
|
|
679
|
+
this.orderedMetadata.splice(idx, 1);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Remove an entry from all in-memory indexes.
|
|
684
|
+
*
|
|
685
|
+
* Updates `entries`, `docIndex`, `orderedMetadata`, and `contentRefCount`.
|
|
686
|
+
* When the content hash reference count drops to zero, the hash is removed
|
|
687
|
+
* from the ref-count map (the caller is responsible for deleting the
|
|
688
|
+
* on-disk content file if needed).
|
|
689
|
+
*/
|
|
690
|
+
applyMetadataDelete(id) {
|
|
691
|
+
const existing = this.entries.get(id);
|
|
692
|
+
if (!existing) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this.entries.delete(id);
|
|
696
|
+
this.docIndex.get(existing.docId)?.delete(id);
|
|
697
|
+
if ((this.docIndex.get(existing.docId)?.size || 0) === 0) {
|
|
698
|
+
this.docIndex.delete(existing.docId);
|
|
699
|
+
}
|
|
700
|
+
this.removeOrderedMetadataById(id);
|
|
701
|
+
const nextRef = (this.contentRefCount.get(existing.contentHash) || 1) - 1;
|
|
702
|
+
if (nextRef <= 0) {
|
|
703
|
+
this.contentRefCount.delete(existing.contentHash);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
this.contentRefCount.set(existing.contentHash, nextRef);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Insert or replace an entry in all in-memory indexes.
|
|
711
|
+
*
|
|
712
|
+
* If the id already exists, the old entry is removed first (via
|
|
713
|
+
* {@link applyMetadataDelete}) before the new one is inserted.
|
|
714
|
+
* Updates `entries`, `docIndex`, `orderedMetadata`, and `contentRefCount`.
|
|
715
|
+
*/
|
|
716
|
+
applyMetadataUpsert(metadata) {
|
|
717
|
+
const existing = this.entries.get(metadata.id);
|
|
718
|
+
if (existing) {
|
|
719
|
+
this.applyMetadataDelete(metadata.id);
|
|
720
|
+
}
|
|
721
|
+
this.entries.set(metadata.id, metadata);
|
|
722
|
+
if (!this.docIndex.has(metadata.docId)) {
|
|
723
|
+
this.docIndex.set(metadata.docId, new Set());
|
|
724
|
+
}
|
|
725
|
+
this.docIndex.get(metadata.docId).add(metadata.id);
|
|
726
|
+
this.contentRefCount.set(metadata.contentHash, (this.contentRefCount.get(metadata.contentHash) || 0) + 1);
|
|
727
|
+
this.insertOrderedMetadata(metadata);
|
|
728
|
+
}
|
|
729
|
+
// ---------------------------------------------------------------------------
|
|
730
|
+
// Filesystem I/O helpers
|
|
731
|
+
// ---------------------------------------------------------------------------
|
|
732
|
+
/** List all `.json` files in the `entries/` directory. */
|
|
733
|
+
async listEntryFiles() {
|
|
734
|
+
try {
|
|
735
|
+
const files = await (0, promises_1.readdir)(this.entriesDir);
|
|
736
|
+
return files.filter((f) => f.endsWith(".json"));
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/** Resolve the absolute path to the metadata file for a given entry id. */
|
|
743
|
+
metadataPathForId(id) {
|
|
744
|
+
return path.join(this.entriesDir, toEntryFileName(id));
|
|
745
|
+
}
|
|
746
|
+
/** Resolve the absolute path to the binary content file for a given hash. */
|
|
747
|
+
contentPathForHash(contentHash) {
|
|
748
|
+
return path.join(this.contentDir, `${contentHash}.bin`);
|
|
749
|
+
}
|
|
750
|
+
/** Check whether a file exists without reading its contents. */
|
|
751
|
+
async fileExists(filePath) {
|
|
752
|
+
try {
|
|
753
|
+
await (0, promises_1.access)(filePath, fs_1.constants.F_OK);
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Write data to a file with full crash-safety guarantees.
|
|
762
|
+
*
|
|
763
|
+
* Protocol:
|
|
764
|
+
* 1. Write to a temporary file (unique per process + timestamp + random).
|
|
765
|
+
* 2. `fsync` the temp file to flush OS buffers to disk.
|
|
766
|
+
* 3. Atomically `rename` temp → final path (POSIX guarantees atomicity).
|
|
767
|
+
* 4. `fsync` the parent directory so the new directory entry is durable.
|
|
768
|
+
*
|
|
769
|
+
* This ensures readers never observe a partially written file, even after
|
|
770
|
+
* an unclean shutdown.
|
|
771
|
+
*/
|
|
772
|
+
async writeFileAtomic(filePath, data) {
|
|
773
|
+
// Durability protocol:
|
|
774
|
+
// 1) write temp file
|
|
775
|
+
// 2) fsync temp file contents
|
|
776
|
+
// 3) atomic rename temp -> final
|
|
777
|
+
// 4) fsync parent directory (persist directory entry update)
|
|
778
|
+
const tmpPath = tempPathFor(filePath);
|
|
779
|
+
const tmpHandle = await (0, promises_1.open)(tmpPath, "w");
|
|
780
|
+
try {
|
|
781
|
+
await tmpHandle.writeFile(data);
|
|
782
|
+
await tmpHandle.sync();
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
await tmpHandle.close();
|
|
786
|
+
}
|
|
787
|
+
await (0, promises_1.rename)(tmpPath, filePath);
|
|
788
|
+
await this.syncParentDirectory(filePath);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Fsync the parent directory of a file to ensure its directory entry
|
|
792
|
+
* update is persisted. Required on POSIX systems after rename/unlink
|
|
793
|
+
* to guarantee that the directory metadata change survives a crash.
|
|
794
|
+
*
|
|
795
|
+
* Failures are logged at debug level and swallowed — some platforms
|
|
796
|
+
* (e.g. certain macOS configurations) do not support opening directories
|
|
797
|
+
* for fsync.
|
|
798
|
+
*/
|
|
799
|
+
async syncParentDirectory(filePath) {
|
|
800
|
+
const dirPath = path.dirname(filePath);
|
|
801
|
+
try {
|
|
802
|
+
const dirHandle = await (0, promises_1.open)(dirPath, "r");
|
|
803
|
+
try {
|
|
804
|
+
await dirHandle.sync();
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
await dirHandle.close();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
catch (err) {
|
|
811
|
+
this.logger.debug(`Directory fsync skipped for ${dirPath}: ${String(err)}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Look up an entry's metadata by id.
|
|
816
|
+
*
|
|
817
|
+
* When indexing is enabled, this is an O(1) in-memory map lookup.
|
|
818
|
+
* Otherwise, falls back to reading the entry file from disk.
|
|
819
|
+
*
|
|
820
|
+
* @returns The metadata if found, or `null`.
|
|
821
|
+
*/
|
|
822
|
+
async readMetadataById(id) {
|
|
823
|
+
if (this.indexingEnabled) {
|
|
824
|
+
return this.entries.get(id) || null;
|
|
825
|
+
}
|
|
826
|
+
const filePath = this.metadataPathForId(id);
|
|
827
|
+
if (!(await this.fileExists(filePath))) {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
const raw = await (0, promises_1.readFile)(filePath, "utf-8");
|
|
831
|
+
return deserializeMetadata(JSON.parse(raw));
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Read and deserialize a metadata entry directly from its on-disk file.
|
|
835
|
+
* Used during index rebuilds and non-indexed fallback paths.
|
|
836
|
+
* Parse failures are logged and result in `null`.
|
|
837
|
+
*/
|
|
838
|
+
async readMetadataByFileName(fileName) {
|
|
839
|
+
const filePath = path.join(this.entriesDir, fileName);
|
|
840
|
+
try {
|
|
841
|
+
const raw = await (0, promises_1.readFile)(filePath, "utf-8");
|
|
842
|
+
const metadata = deserializeMetadata(JSON.parse(raw));
|
|
843
|
+
return metadata;
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
this.logger.warn(`Failed to read metadata file ${fileName}: ${String(err)}`);
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Return all metadata entries in the store.
|
|
852
|
+
*
|
|
853
|
+
* When indexing is enabled, returns a shallow copy of `orderedMetadata`
|
|
854
|
+
* (already sorted). Otherwise, reads and parses every entry file from
|
|
855
|
+
* disk (unsorted).
|
|
856
|
+
*/
|
|
857
|
+
async listAllMetadata() {
|
|
858
|
+
if (this.indexingEnabled) {
|
|
859
|
+
return [...this.orderedMetadata];
|
|
860
|
+
}
|
|
861
|
+
const files = await this.listEntryFiles();
|
|
862
|
+
const result = [];
|
|
863
|
+
for (const fileName of files) {
|
|
864
|
+
const metadata = await this.readMetadataByFileName(fileName);
|
|
865
|
+
if (metadata) {
|
|
866
|
+
result.push(metadata);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
// ---------------------------------------------------------------------------
|
|
872
|
+
// ContentAddressedStore interface — public API
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
/** Return the database id this store was created for. */
|
|
875
|
+
getId() {
|
|
876
|
+
return this.dbId;
|
|
877
|
+
}
|
|
878
|
+
getCacheIdentity() {
|
|
879
|
+
return `disk:${this.storeRoot}`;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Persist one or more entries to disk.
|
|
883
|
+
*
|
|
884
|
+
* For each entry the method:
|
|
885
|
+
* 1. Skips if the metadata file already exists (content-addressed idempotency).
|
|
886
|
+
* 2. Writes the encrypted payload to `content/<hash>.bin` (deduplicated by hash).
|
|
887
|
+
* 3. Writes the metadata to `entries/<id>.json`.
|
|
888
|
+
* 4. Updates the in-memory index and appends a segment record.
|
|
889
|
+
*
|
|
890
|
+
* The commit order (content → metadata → segment) ensures that a crash at
|
|
891
|
+
* any point leaves the store in a consistent state: orphaned content blobs
|
|
892
|
+
* are harmless, and the index can always be rebuilt from the metadata files.
|
|
893
|
+
*/
|
|
894
|
+
async putEntries(entries) {
|
|
895
|
+
await this.ensureInitialized();
|
|
896
|
+
let hasMutation = false;
|
|
897
|
+
const segmentRecords = [];
|
|
898
|
+
for (const entry of entries) {
|
|
899
|
+
const metadataPath = this.metadataPathForId(entry.id);
|
|
900
|
+
if (await this.fileExists(metadataPath)) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
// Commit order is intentional:
|
|
904
|
+
// - write payload by content hash first (deduplicated)
|
|
905
|
+
// - write metadata by id second (entry becomes discoverable)
|
|
906
|
+
// - append segment record last (index acceleration catches up)
|
|
907
|
+
const contentPath = this.contentPathForHash(entry.contentHash);
|
|
908
|
+
if (!(await this.fileExists(contentPath))) {
|
|
909
|
+
await this.writeFileAtomic(contentPath, entry.encryptedData);
|
|
910
|
+
}
|
|
911
|
+
const { encryptedData, ...metadata } = entry;
|
|
912
|
+
const serialized = JSON.stringify(serializeMetadata(metadata));
|
|
913
|
+
await this.writeFileAtomic(metadataPath, serialized);
|
|
914
|
+
hasMutation = true;
|
|
915
|
+
if (this.indexingEnabled) {
|
|
916
|
+
this.applyMetadataUpsert(metadata);
|
|
917
|
+
segmentRecords.push({
|
|
918
|
+
op: "upsert",
|
|
919
|
+
metadata: serializeMetadata(metadata),
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (hasMutation && this.indexingEnabled) {
|
|
924
|
+
await this.appendMetadataSegment(segmentRecords);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Retrieve full entries (metadata + encrypted payload) by id.
|
|
929
|
+
*
|
|
930
|
+
* IDs that are not found or whose content file is missing are silently
|
|
931
|
+
* omitted from the result. When indexing is enabled, the metadata lookup
|
|
932
|
+
* is O(1); the content read is always a disk I/O.
|
|
933
|
+
*/
|
|
934
|
+
async getEntries(ids) {
|
|
935
|
+
await this.ensureInitialized();
|
|
936
|
+
const result = [];
|
|
937
|
+
for (const id of ids) {
|
|
938
|
+
const metadata = await this.readMetadataById(id);
|
|
939
|
+
if (!metadata) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const contentPath = this.contentPathForHash(metadata.contentHash);
|
|
943
|
+
if (!(await this.fileExists(contentPath))) {
|
|
944
|
+
this.logger.warn(`Content ${metadata.contentHash} not found for entry ${id}`);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const encryptedData = new Uint8Array(await (0, promises_1.readFile)(contentPath));
|
|
948
|
+
result.push({ ...metadata, encryptedData });
|
|
949
|
+
}
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Check which of the given ids exist in the store.
|
|
954
|
+
*
|
|
955
|
+
* When indexing is enabled, this is a fast in-memory set-membership test.
|
|
956
|
+
* Otherwise, it checks for the existence of the corresponding metadata
|
|
957
|
+
* file on disk.
|
|
958
|
+
*
|
|
959
|
+
* @returns The subset of `ids` that are present in the store.
|
|
960
|
+
*/
|
|
961
|
+
async hasEntries(ids) {
|
|
962
|
+
await this.ensureInitialized();
|
|
963
|
+
const existing = [];
|
|
964
|
+
for (const id of ids) {
|
|
965
|
+
if (this.indexingEnabled) {
|
|
966
|
+
if (this.entries.has(id)) {
|
|
967
|
+
existing.push(id);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
if (await this.fileExists(this.metadataPathForId(id))) {
|
|
972
|
+
existing.push(id);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return existing;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Find all entries whose ids are not in `knownIds`.
|
|
980
|
+
*
|
|
981
|
+
* Used during sync to discover entries the caller has not yet seen.
|
|
982
|
+
* Returns metadata only (no payload).
|
|
983
|
+
*/
|
|
984
|
+
async findNewEntries(knownIds) {
|
|
985
|
+
await this.ensureInitialized();
|
|
986
|
+
const known = new Set(knownIds);
|
|
987
|
+
const all = await this.listAllMetadata();
|
|
988
|
+
return all.filter((meta) => !known.has(meta.id));
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Find entries for a specific document that are not in `knownIds`.
|
|
992
|
+
*
|
|
993
|
+
* When indexing is enabled, leverages the `docIndex` for an O(K) scan
|
|
994
|
+
* (K = entries for this doc) instead of the full store.
|
|
995
|
+
*/
|
|
996
|
+
async findNewEntriesForDoc(knownIds, docId) {
|
|
997
|
+
await this.ensureInitialized();
|
|
998
|
+
const known = new Set(knownIds);
|
|
999
|
+
if (this.indexingEnabled) {
|
|
1000
|
+
const ids = this.docIndex.get(docId) || new Set();
|
|
1001
|
+
const result = [];
|
|
1002
|
+
for (const id of ids) {
|
|
1003
|
+
if (known.has(id)) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const metadata = this.entries.get(id);
|
|
1007
|
+
if (metadata) {
|
|
1008
|
+
result.push(metadata);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return result;
|
|
1012
|
+
}
|
|
1013
|
+
const all = await this.listAllMetadata();
|
|
1014
|
+
return all.filter((meta) => meta.docId === docId && !known.has(meta.id));
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Find entries matching a given type and optional creation-date range.
|
|
1018
|
+
*
|
|
1019
|
+
* @param type Entry type to filter by (e.g. "doc_change").
|
|
1020
|
+
* @param creationDateFrom Inclusive lower bound (epoch ms), or `null` for no lower bound.
|
|
1021
|
+
* @param creationDateUntil Exclusive upper bound (epoch ms), or `null` for no upper bound.
|
|
1022
|
+
* @returns Metadata of all matching entries.
|
|
1023
|
+
*/
|
|
1024
|
+
async findEntries(type, creationDateFrom, creationDateUntil) {
|
|
1025
|
+
await this.ensureInitialized();
|
|
1026
|
+
const all = await this.listAllMetadata();
|
|
1027
|
+
return all.filter((meta) => {
|
|
1028
|
+
if (meta.entryType !== type) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
if (creationDateFrom !== null && meta.createdAt < creationDateFrom) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (creationDateUntil !== null && meta.createdAt >= creationDateUntil) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
return true;
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Return all entry ids in the store.
|
|
1042
|
+
*
|
|
1043
|
+
* When indexing is enabled, returns ids in `(createdAt, id)` order.
|
|
1044
|
+
* Without indexing, order depends on the filesystem directory listing.
|
|
1045
|
+
*/
|
|
1046
|
+
async getAllIds() {
|
|
1047
|
+
await this.ensureInitialized();
|
|
1048
|
+
if (this.indexingEnabled) {
|
|
1049
|
+
return this.orderedMetadata.map((m) => m.id);
|
|
1050
|
+
}
|
|
1051
|
+
const files = await this.listEntryFiles();
|
|
1052
|
+
return files.map(fromEntryFileName);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Cursor-based paginated scan over entry metadata.
|
|
1056
|
+
*
|
|
1057
|
+
* Designed for high-volume synchronization: the caller repeatedly advances
|
|
1058
|
+
* a cursor to stream through all entries in deterministic
|
|
1059
|
+
* `(createdAt ASC, id ASC)` order, optionally applying filters.
|
|
1060
|
+
*
|
|
1061
|
+
* **Indexed path** (default):
|
|
1062
|
+
* - O(log N) binary search for the cursor position.
|
|
1063
|
+
* - O(pageSize) slice for unfiltered scans; O(scan window) for filtered.
|
|
1064
|
+
*
|
|
1065
|
+
* **Non-indexed fallback**:
|
|
1066
|
+
* - Reads and sorts *all* metadata on every call. Suitable only for
|
|
1067
|
+
* small stores or when indexing is explicitly disabled.
|
|
1068
|
+
*
|
|
1069
|
+
* @param cursor Position to resume from (exclusive), or `null` to start
|
|
1070
|
+
* from the beginning.
|
|
1071
|
+
* @param limit Maximum number of entries to return in this page.
|
|
1072
|
+
* @param filters Optional filters (docId, entryTypes, date range).
|
|
1073
|
+
* @returns A page of metadata entries, the next cursor, and a `hasMore` flag.
|
|
1074
|
+
*/
|
|
1075
|
+
async scanEntriesSince(cursor, limit = Number.MAX_SAFE_INTEGER, filters) {
|
|
1076
|
+
await this.ensureInitialized();
|
|
1077
|
+
if (this.indexingEnabled) {
|
|
1078
|
+
// Indexed path: O(logN) lower-bound + O(pageSize) slice/filter.
|
|
1079
|
+
const startIndex = this.lowerBoundForCursor(cursor);
|
|
1080
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
1081
|
+
const page = this.orderedMetadata.slice(startIndex, startIndex + limit);
|
|
1082
|
+
const last = page.length > 0 ? page[page.length - 1] : null;
|
|
1083
|
+
return {
|
|
1084
|
+
entries: page,
|
|
1085
|
+
nextCursor: last ? { createdAt: last.createdAt, id: last.id } : cursor,
|
|
1086
|
+
hasMore: startIndex + page.length < this.orderedMetadata.length,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
const page = [];
|
|
1090
|
+
let idx = startIndex;
|
|
1091
|
+
while (idx < this.orderedMetadata.length && page.length < limit) {
|
|
1092
|
+
const meta = this.orderedMetadata[idx];
|
|
1093
|
+
if (this.metadataMatchesFilters(meta, filters)) {
|
|
1094
|
+
page.push(meta);
|
|
1095
|
+
}
|
|
1096
|
+
idx++;
|
|
1097
|
+
}
|
|
1098
|
+
let hasMore = false;
|
|
1099
|
+
while (idx < this.orderedMetadata.length) {
|
|
1100
|
+
if (this.metadataMatchesFilters(this.orderedMetadata[idx], filters)) {
|
|
1101
|
+
hasMore = true;
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
idx++;
|
|
1105
|
+
}
|
|
1106
|
+
const last = page.length > 0 ? page[page.length - 1] : null;
|
|
1107
|
+
return {
|
|
1108
|
+
entries: page,
|
|
1109
|
+
nextCursor: last ? { createdAt: last.createdAt, id: last.id } : cursor,
|
|
1110
|
+
hasMore,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
// Non-indexed fallback: scan and sort all metadata on demand.
|
|
1114
|
+
const all = (await this.listAllMetadata())
|
|
1115
|
+
.filter((meta) => {
|
|
1116
|
+
if (filters?.docId && meta.docId !== filters.docId) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
if (filters?.entryTypes && filters.entryTypes.length > 0 && !filters.entryTypes.includes(meta.entryType)) {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
if (filters?.creationDateFrom !== undefined && filters.creationDateFrom !== null && meta.createdAt < filters.creationDateFrom) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
if (filters?.creationDateUntil !== undefined && filters.creationDateUntil !== null && meta.createdAt >= filters.creationDateUntil) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
return true;
|
|
1129
|
+
})
|
|
1130
|
+
.sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt - b.createdAt));
|
|
1131
|
+
const startIndex = cursor === null
|
|
1132
|
+
? 0
|
|
1133
|
+
: all.findIndex((meta) => meta.createdAt > cursor.createdAt || (meta.createdAt === cursor.createdAt && meta.id > cursor.id));
|
|
1134
|
+
if (startIndex === -1) {
|
|
1135
|
+
return { entries: [], nextCursor: cursor, hasMore: false };
|
|
1136
|
+
}
|
|
1137
|
+
const page = all.slice(startIndex, startIndex + limit);
|
|
1138
|
+
const last = page.length > 0 ? page[page.length - 1] : null;
|
|
1139
|
+
return {
|
|
1140
|
+
entries: page,
|
|
1141
|
+
nextCursor: last ? { createdAt: last.createdAt, id: last.id } : cursor,
|
|
1142
|
+
hasMore: startIndex + page.length < all.length,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Build a bloom filter summary of all entry ids in this store.
|
|
1147
|
+
*
|
|
1148
|
+
* The summary is a compact probabilistic set representation that a remote
|
|
1149
|
+
* peer can use during sync to cheaply test which of its own ids are
|
|
1150
|
+
* *definitely absent* from this store — avoiding per-id network
|
|
1151
|
+
* round-trips. See {@link createIdBloomSummary} for details on the bloom
|
|
1152
|
+
* filter implementation.
|
|
1153
|
+
*
|
|
1154
|
+
* @returns A serializable {@link StoreIdBloomSummary} with a base64-encoded
|
|
1155
|
+
* bitset, suitable for transmission over the network.
|
|
1156
|
+
*/
|
|
1157
|
+
async getIdBloomSummary() {
|
|
1158
|
+
await this.ensureInitialized();
|
|
1159
|
+
const ids = await this.getAllIds();
|
|
1160
|
+
return (0, bloom_1.createIdBloomSummary)(ids);
|
|
1161
|
+
}
|
|
1162
|
+
async planDocumentMaterialization(docId, options) {
|
|
1163
|
+
await this.ensureInitialized();
|
|
1164
|
+
let entries = [];
|
|
1165
|
+
if (this.indexingEnabled) {
|
|
1166
|
+
const ids = this.docIndex.get(docId) || new Set();
|
|
1167
|
+
entries = Array.from(ids)
|
|
1168
|
+
.map((id) => this.entries.get(id))
|
|
1169
|
+
.filter((m) => m !== undefined);
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
const all = await this.listAllMetadata();
|
|
1173
|
+
entries = all.filter((m) => m.docId === docId);
|
|
1174
|
+
}
|
|
1175
|
+
return (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, entries, options);
|
|
1176
|
+
}
|
|
1177
|
+
async planDocumentMaterializationBatch(docIds, options) {
|
|
1178
|
+
await this.ensureInitialized();
|
|
1179
|
+
const all = await this.listAllMetadata();
|
|
1180
|
+
return (0, MaterializationPlanner_1.computeBatchMaterializationPlan)(all, docIds, options);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Traverse the dependency graph starting from a given entry id.
|
|
1184
|
+
*
|
|
1185
|
+
* Performs a breadth-first traversal through `dependencyIds` links,
|
|
1186
|
+
* collecting all reachable entries. The result is returned in
|
|
1187
|
+
* **dependency-first order** (deepest dependencies first) so callers
|
|
1188
|
+
* can process entries from oldest to newest.
|
|
1189
|
+
*
|
|
1190
|
+
* Use cases:
|
|
1191
|
+
* - Loading all chunks of a large attachment (traverse from the last
|
|
1192
|
+
* chunk back to the first).
|
|
1193
|
+
* - Reconstructing a document state by collecting all changes.
|
|
1194
|
+
* - Stopping at snapshots to avoid loading the full history.
|
|
1195
|
+
*
|
|
1196
|
+
* @param startId The entry id to begin traversal from.
|
|
1197
|
+
* @param options Optional traversal controls:
|
|
1198
|
+
* - `stopAtEntryType` — stop expanding when hitting this type (the
|
|
1199
|
+
* matching entry is still included).
|
|
1200
|
+
* - `maxDepth` — maximum number of hops from `startId`.
|
|
1201
|
+
* - `includeStart` — whether `startId` itself appears in the result
|
|
1202
|
+
* (default `true`).
|
|
1203
|
+
* @returns Entry ids in dependency-first order.
|
|
1204
|
+
*/
|
|
1205
|
+
async resolveDependencies(startId, options) {
|
|
1206
|
+
await this.ensureInitialized();
|
|
1207
|
+
const stopAtEntryType = options?.stopAtEntryType;
|
|
1208
|
+
const maxDepth = options?.maxDepth;
|
|
1209
|
+
const includeStart = options?.includeStart !== false;
|
|
1210
|
+
// Breadth-first traversal with a visited set.
|
|
1211
|
+
// We reverse at the end to return dependency-first order.
|
|
1212
|
+
const result = [];
|
|
1213
|
+
const visited = new Set();
|
|
1214
|
+
const queue = [{ id: startId, depth: 0 }];
|
|
1215
|
+
while (queue.length > 0) {
|
|
1216
|
+
const { id, depth } = queue.shift();
|
|
1217
|
+
if (visited.has(id)) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
visited.add(id);
|
|
1221
|
+
if (maxDepth !== undefined && depth > maxDepth) {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const entry = await this.readMetadataById(id);
|
|
1225
|
+
if (!entry) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (stopAtEntryType && entry.entryType === stopAtEntryType && id !== startId) {
|
|
1229
|
+
result.push(id);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (id !== startId || includeStart) {
|
|
1233
|
+
result.push(id);
|
|
1234
|
+
}
|
|
1235
|
+
for (const depId of entry.dependencyIds) {
|
|
1236
|
+
if (!visited.has(depId)) {
|
|
1237
|
+
queue.push({ id: depId, depth: depth + 1 });
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
result.reverse();
|
|
1242
|
+
return result;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Permanently delete all entries belonging to a document (GDPR purge).
|
|
1246
|
+
*
|
|
1247
|
+
* Steps:
|
|
1248
|
+
* 1. Identify all metadata entries with the given `docId`.
|
|
1249
|
+
* 2. Delete each metadata file and update the in-memory index.
|
|
1250
|
+
* 3. Delete orphaned content blobs — content files whose hash is no
|
|
1251
|
+
* longer referenced by any remaining entry.
|
|
1252
|
+
* 4. Append "delete" segment records so the index stays consistent
|
|
1253
|
+
* across restarts.
|
|
1254
|
+
*
|
|
1255
|
+
* @param docId The document whose entire history should be erased.
|
|
1256
|
+
*/
|
|
1257
|
+
async purgeDocHistory(docId) {
|
|
1258
|
+
await this.ensureInitialized();
|
|
1259
|
+
const all = await this.listAllMetadata();
|
|
1260
|
+
const toDelete = all.filter((meta) => meta.docId === docId);
|
|
1261
|
+
if (toDelete.length === 0) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
// GDPR purge removes all metadata for one document, then drops orphaned payloads.
|
|
1265
|
+
const deletedHashes = new Set();
|
|
1266
|
+
const deletedIds = [];
|
|
1267
|
+
for (const meta of toDelete) {
|
|
1268
|
+
deletedHashes.add(meta.contentHash);
|
|
1269
|
+
deletedIds.push(meta.id);
|
|
1270
|
+
const metadataPath = this.metadataPathForId(meta.id);
|
|
1271
|
+
await (0, promises_1.unlink)(metadataPath).catch(() => {
|
|
1272
|
+
// Ignore races or already-deleted files.
|
|
1273
|
+
});
|
|
1274
|
+
await this.syncParentDirectory(metadataPath);
|
|
1275
|
+
if (this.indexingEnabled) {
|
|
1276
|
+
this.applyMetadataDelete(meta.id);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const remaining = await this.listAllMetadata();
|
|
1280
|
+
const remainingHashes = new Set(remaining.map((m) => m.contentHash));
|
|
1281
|
+
for (const hash of deletedHashes) {
|
|
1282
|
+
if (!remainingHashes.has(hash)) {
|
|
1283
|
+
const contentPath = this.contentPathForHash(hash);
|
|
1284
|
+
await (0, promises_1.unlink)(contentPath).catch(() => {
|
|
1285
|
+
// Ignore races or already-deleted files.
|
|
1286
|
+
});
|
|
1287
|
+
await this.syncParentDirectory(contentPath);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (this.indexingEnabled) {
|
|
1291
|
+
await this.appendMetadataSegment(deletedIds.map((id) => ({ op: "delete", id })));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Delete all data for this database and recreate empty directories.
|
|
1296
|
+
*
|
|
1297
|
+
* Resets the index status to "building" temporarily, removes the entire
|
|
1298
|
+
* store root, and re-creates the directory structure. The index is
|
|
1299
|
+
* immediately marked "ready" afterwards since the store is now empty.
|
|
1300
|
+
*/
|
|
1301
|
+
async clearAllLocalData() {
|
|
1302
|
+
await this.ensureInitialized();
|
|
1303
|
+
this.indexStatus = {
|
|
1304
|
+
phase: this.indexingEnabled ? "building" : "ready",
|
|
1305
|
+
indexingEnabled: this.indexingEnabled,
|
|
1306
|
+
progress01: this.indexingEnabled ? 0 : 1,
|
|
1307
|
+
};
|
|
1308
|
+
await this.clearAllLocalDataInternal();
|
|
1309
|
+
await (0, promises_1.mkdir)(this.entriesDir, { recursive: true });
|
|
1310
|
+
await (0, promises_1.mkdir)(this.contentDir, { recursive: true });
|
|
1311
|
+
await (0, promises_1.mkdir)(this.metadataSegmentsDir, { recursive: true });
|
|
1312
|
+
if (this.indexingEnabled) {
|
|
1313
|
+
this.indexStatus = { phase: "ready", indexingEnabled: true, progress01: 1 };
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Wait until the in-memory index has finished building and return its
|
|
1318
|
+
* status. Since initialization is fully awaited via `initPromise`, this
|
|
1319
|
+
* effectively waits for the constructor's async init to complete.
|
|
1320
|
+
*/
|
|
1321
|
+
async awaitIndexReady(_options) {
|
|
1322
|
+
await this.ensureInitialized();
|
|
1323
|
+
return this.indexStatus;
|
|
1324
|
+
}
|
|
1325
|
+
/** Return the current index build status without waiting. */
|
|
1326
|
+
getIndexBuildStatus() {
|
|
1327
|
+
return this.indexStatus;
|
|
1328
|
+
}
|
|
1329
|
+
/** Return a snapshot of cumulative metadata-segment compaction statistics. */
|
|
1330
|
+
async getCompactionStatus() {
|
|
1331
|
+
return { ...this.compactionStatus };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
exports.BasicOnDiskContentAddressedStore = BasicOnDiskContentAddressedStore;
|
|
1335
|
+
/**
|
|
1336
|
+
* Factory that creates {@link BasicOnDiskContentAddressedStore} instances.
|
|
1337
|
+
*
|
|
1338
|
+
* Implements the {@link ContentAddressedStoreFactory} interface so it can be
|
|
1339
|
+
* plugged into MindooDB's store creation pipeline. Each call to
|
|
1340
|
+
* `createStore` returns a fresh store backed by its own on-disk directory
|
|
1341
|
+
* under the configured base path.
|
|
1342
|
+
*/
|
|
1343
|
+
class BasicOnDiskContentAddressedStoreFactory {
|
|
1344
|
+
/**
|
|
1345
|
+
* Create a new on-disk content-addressed store for the given database.
|
|
1346
|
+
*
|
|
1347
|
+
* @param dbId Unique database identifier (becomes the directory name).
|
|
1348
|
+
* @param options Store configuration forwarded to the constructor.
|
|
1349
|
+
* @returns A {@link CreateStoreResult} containing the new store as `docStore`.
|
|
1350
|
+
*/
|
|
1351
|
+
createStore(dbId, options) {
|
|
1352
|
+
return {
|
|
1353
|
+
docStore: new BasicOnDiskContentAddressedStore(dbId, undefined, options),
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
exports.BasicOnDiskContentAddressedStoreFactory = BasicOnDiskContentAddressedStoreFactory;
|
|
1358
|
+
//# sourceMappingURL=BasicOnDiskContentAddressedStore.js.map
|