mindoodb 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -19
- 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 +11 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +94 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +13 -2
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +86 -0
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +19 -2
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +96 -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 +61 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +685 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts +25 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts.map +1 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js +41 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js.map +1 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +13 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/core/BaseMindooDB.d.ts +12 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +210 -108
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +40 -6
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +179 -21
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +10 -0
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts +22 -28
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +161 -65
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +68 -0
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js +659 -0
- package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +43 -3
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +162 -23
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/bloom.d.ts +85 -0
- package/dist/core/appendonlystores/bloom.d.ts.map +1 -0
- package/dist/core/appendonlystores/bloom.js +228 -0
- package/dist/core/appendonlystores/bloom.js.map +1 -0
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +22 -2
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +10 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +123 -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/crypto/pbkdf2Iterations.d.ts +12 -0
- package/dist/core/crypto/pbkdf2Iterations.d.ts.map +1 -0
- package/dist/core/crypto/pbkdf2Iterations.js +55 -0
- package/dist/core/crypto/pbkdf2Iterations.js.map +1 -0
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +30 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/keys/KeyBag.d.ts +32 -12
- package/dist/core/keys/KeyBag.d.ts.map +1 -1
- package/dist/core/keys/KeyBag.js +86 -26
- package/dist/core/keys/KeyBag.js.map +1 -1
- package/dist/core/keys/KeyContext.d.ts +4 -0
- package/dist/core/keys/KeyContext.d.ts.map +1 -0
- package/dist/core/keys/KeyContext.js +11 -0
- package/dist/core/keys/KeyContext.js.map +1 -0
- package/dist/core/types.d.ts +218 -50
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/MindooURI.d.ts +58 -0
- package/dist/core/uri/MindooURI.d.ts.map +1 -0
- package/dist/core/uri/MindooURI.js +169 -0
- package/dist/core/uri/MindooURI.js.map +1 -0
- package/dist/core/userid/index.d.ts +0 -10
- package/dist/core/userid/index.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +541 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +1334 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +5 -1
- package/dist/node/index.js.map +1 -1
- package/dist/reactnative/crypto/AESAdapter.d.ts +18 -0
- package/dist/reactnative/crypto/AESAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/AESAdapter.js +112 -0
- package/dist/reactnative/crypto/AESAdapter.js.map +1 -0
- package/dist/reactnative/crypto/Ed25519Adapter.d.ts +34 -0
- package/dist/reactnative/crypto/Ed25519Adapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/Ed25519Adapter.js +386 -0
- package/dist/reactnative/crypto/Ed25519Adapter.js.map +1 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.d.ts +23 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.js +51 -0
- package/dist/reactnative/crypto/ExpoGoCryptoAdapter.js.map +1 -0
- package/dist/reactnative/crypto/HMACAdapter.d.ts +13 -0
- package/dist/reactnative/crypto/HMACAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/HMACAdapter.js +50 -0
- package/dist/reactnative/crypto/HMACAdapter.js.map +1 -0
- package/dist/reactnative/crypto/KeyFormatConverter.d.ts +21 -0
- package/dist/reactnative/crypto/KeyFormatConverter.d.ts.map +1 -0
- package/dist/reactnative/crypto/KeyFormatConverter.js +72 -0
- package/dist/reactnative/crypto/KeyFormatConverter.js.map +1 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.d.ts +19 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.js +67 -0
- package/dist/reactnative/crypto/PBKDF2Adapter.js.map +1 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.d.ts +37 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.js +72 -0
- package/dist/reactnative/crypto/QuickCryptoAdapter.js.map +1 -0
- package/dist/reactnative/crypto/RSAAdapter.d.ts +38 -0
- package/dist/reactnative/crypto/RSAAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/RSAAdapter.js +184 -0
- package/dist/reactnative/crypto/RSAAdapter.js.map +1 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.d.ts +33 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.js +61 -0
- package/dist/reactnative/crypto/ReactNativeCryptoAdapter.js.map +1 -0
- package/dist/reactnative/crypto/SHA256Adapter.d.ts +9 -0
- package/dist/reactnative/crypto/SHA256Adapter.d.ts.map +1 -0
- package/dist/reactnative/crypto/SHA256Adapter.js +29 -0
- package/dist/reactnative/crypto/SHA256Adapter.js.map +1 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.d.ts +63 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.d.ts.map +1 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.js +350 -0
- package/dist/reactnative/crypto/SubtleCryptoPolyfill.js.map +1 -0
- package/dist/reactnative/index.d.ts +8 -0
- package/dist/reactnative/index.d.ts.map +1 -0
- package/dist/reactnative/index.js +38 -0
- package/dist/reactnative/index.js.map +1 -0
- package/package.json +51 -6
- package/patches/PATCHES.md +111 -0
- package/patches/react-native+0.76.9.patch +34 -0
- package/patches/react-native-quick-crypto+1.0.7.patch +42 -0
- package/scripts/cli.js +21 -0
- package/scripts/setup-react-native.js +228 -0
- package/templates/reactnative/App.js +252 -0
- package/templates/reactnative/app.json +29 -0
- package/templates/reactnative/babel.config.js +6 -0
- package/templates/reactnative/index.js +26 -0
- package/templates/reactnative/metro.config.snippet.js +39 -0
- package/templates/reactnative/mindoodb-polyfills.js +116 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IndexedDB-backed ContentAddressedStore for browser environments.
|
|
4
|
+
*
|
|
5
|
+
* Provides persistent, indexed storage using the IndexedDB API. All indexing
|
|
6
|
+
* is delegated to IDB itself (no in-memory maps), keeping memory usage low
|
|
7
|
+
* even for stores with hundreds of thousands of entries.
|
|
8
|
+
*
|
|
9
|
+
* Schema (IDB version 1):
|
|
10
|
+
* - `entries` : metadata per entry id, indexed by docId, contentHash,
|
|
11
|
+
* (entryType, createdAt), and (createdAt, id)
|
|
12
|
+
* - `content` : encrypted payload per contentHash with reference counting
|
|
13
|
+
* - `bloom_cache` : single cached bloom filter summary, incrementally updated
|
|
14
|
+
* on inserts and fully rebuilt on purge or size threshold
|
|
15
|
+
*
|
|
16
|
+
* @module IndexedDBContentAddressedStore
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.IndexedDBContentAddressedStore = void 0;
|
|
20
|
+
const logging_1 = require("../../core/logging");
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// IDB Constants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const IDB_VERSION = 1;
|
|
25
|
+
const ENTRIES_STORE = "entries";
|
|
26
|
+
const CONTENT_STORE = "content";
|
|
27
|
+
const BLOOM_CACHE_STORE = "bloom_cache";
|
|
28
|
+
// Index names
|
|
29
|
+
const IDX_DOC_ID = "by_docId";
|
|
30
|
+
const IDX_ENTRY_TYPE_CREATED_AT = "by_entryType_createdAt";
|
|
31
|
+
const IDX_CREATED_AT_ID = "by_createdAt_id";
|
|
32
|
+
const IDX_CONTENT_HASH = "by_contentHash";
|
|
33
|
+
// Bloom cache key
|
|
34
|
+
const BLOOM_CACHE_KEY = "current";
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Browser-compatible bloom filter helpers (no Buffer dependency)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
const DEFAULT_FALSE_POSITIVE_RATE = 0.01;
|
|
39
|
+
const DEFAULT_SALT = "mindoodb-bloom-v1";
|
|
40
|
+
function fnv1a32(input) {
|
|
41
|
+
let hash = 0x811c9dc5;
|
|
42
|
+
for (let i = 0; i < input.length; i++) {
|
|
43
|
+
hash ^= input.charCodeAt(i);
|
|
44
|
+
hash = Math.imul(hash, 0x01000193);
|
|
45
|
+
}
|
|
46
|
+
return hash >>> 0;
|
|
47
|
+
}
|
|
48
|
+
function djb2_32(input) {
|
|
49
|
+
let hash = 5381;
|
|
50
|
+
for (let i = 0; i < input.length; i++) {
|
|
51
|
+
hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
return hash >>> 0;
|
|
54
|
+
}
|
|
55
|
+
function computeBloomShape(itemCount, falsePositiveRate) {
|
|
56
|
+
if (itemCount <= 0) {
|
|
57
|
+
return { bitCount: 64, hashCount: 2 };
|
|
58
|
+
}
|
|
59
|
+
const ln2 = Math.log(2);
|
|
60
|
+
const bitCount = Math.max(64, Math.ceil((-itemCount * Math.log(falsePositiveRate)) / (ln2 * ln2)));
|
|
61
|
+
const hashCount = Math.max(1, Math.round((bitCount / itemCount) * ln2));
|
|
62
|
+
return { bitCount, hashCount };
|
|
63
|
+
}
|
|
64
|
+
function bloomIndexes(id, bitCount, hashCount, salt) {
|
|
65
|
+
const h1 = fnv1a32(`${salt}:${id}`);
|
|
66
|
+
const h2 = djb2_32(`${id}:${salt}`) || 1;
|
|
67
|
+
const indexes = [];
|
|
68
|
+
for (let i = 0; i < hashCount; i++) {
|
|
69
|
+
indexes.push((h1 + i * h2) % bitCount);
|
|
70
|
+
}
|
|
71
|
+
return indexes;
|
|
72
|
+
}
|
|
73
|
+
function uint8ArrayToBase64(bytes) {
|
|
74
|
+
let binary = "";
|
|
75
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
76
|
+
binary += String.fromCharCode(bytes[i]);
|
|
77
|
+
}
|
|
78
|
+
return btoa(binary);
|
|
79
|
+
}
|
|
80
|
+
function base64ToUint8Array(base64) {
|
|
81
|
+
const binary = atob(base64);
|
|
82
|
+
const bytes = new Uint8Array(binary.length);
|
|
83
|
+
for (let i = 0; i < binary.length; i++) {
|
|
84
|
+
bytes[i] = binary.charCodeAt(i);
|
|
85
|
+
}
|
|
86
|
+
return bytes;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build a bloom filter summary from a list of entry IDs (browser-compatible).
|
|
90
|
+
* Produces output identical to core bloom.ts but without Node.js Buffer.
|
|
91
|
+
*/
|
|
92
|
+
function createIdBloomSummaryBrowser(ids, falsePositiveRate = DEFAULT_FALSE_POSITIVE_RATE) {
|
|
93
|
+
const { bitCount, hashCount } = computeBloomShape(ids.length, falsePositiveRate);
|
|
94
|
+
const bitset = new Uint8Array(Math.ceil(bitCount / 8));
|
|
95
|
+
for (const id of ids) {
|
|
96
|
+
const idxs = bloomIndexes(id, bitCount, hashCount, DEFAULT_SALT);
|
|
97
|
+
for (const idx of idxs) {
|
|
98
|
+
const byteIndex = Math.floor(idx / 8);
|
|
99
|
+
const bitOffset = idx % 8;
|
|
100
|
+
bitset[byteIndex] |= 1 << bitOffset;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
version: "bloom-v1",
|
|
105
|
+
totalIds: ids.length,
|
|
106
|
+
bitCount,
|
|
107
|
+
hashCount,
|
|
108
|
+
salt: DEFAULT_SALT,
|
|
109
|
+
bitsetBase64: uint8ArrayToBase64(bitset),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Threshold multiplier for bloom filter rebuild.
|
|
114
|
+
* When totalIds exceeds originalSizedFor * BLOOM_REBUILD_FACTOR the filter
|
|
115
|
+
* is rebuilt from scratch to maintain the target false-positive rate.
|
|
116
|
+
*/
|
|
117
|
+
const BLOOM_REBUILD_FACTOR = 2;
|
|
118
|
+
/**
|
|
119
|
+
* Incrementally add new IDs to an existing bloom filter summary.
|
|
120
|
+
*
|
|
121
|
+
* Because bloom filters only ever set bits to 1, adding IDs is safe without
|
|
122
|
+
* a full recompute: decode the bitset, OR in the new bits, re-encode.
|
|
123
|
+
*
|
|
124
|
+
* Returns `null` when the filter should be fully rebuilt instead (totalIds
|
|
125
|
+
* has grown past BLOOM_REBUILD_FACTOR * the size it was originally sized
|
|
126
|
+
* for, meaning the false-positive rate has degraded beyond acceptable).
|
|
127
|
+
*/
|
|
128
|
+
function incrementalBloomUpdate(summary, newIds) {
|
|
129
|
+
if (newIds.length === 0)
|
|
130
|
+
return summary;
|
|
131
|
+
const newTotal = summary.totalIds + newIds.length;
|
|
132
|
+
// Check if the filter was sized for far fewer items than it now holds.
|
|
133
|
+
// originalSizedFor is the item count the current bitCount/hashCount was
|
|
134
|
+
// optimised for. We back-derive it from the bloom shape formula:
|
|
135
|
+
// bitCount = -(n * ln(p)) / (ln2)^2 => n = -bitCount * (ln2)^2 / ln(p)
|
|
136
|
+
const ln2 = Math.log(2);
|
|
137
|
+
const originalSizedFor = Math.max(1, Math.floor((-summary.bitCount * ln2 * ln2) /
|
|
138
|
+
Math.log(DEFAULT_FALSE_POSITIVE_RATE)));
|
|
139
|
+
if (newTotal > originalSizedFor * BLOOM_REBUILD_FACTOR) {
|
|
140
|
+
// False-positive rate has degraded too far; caller should rebuild.
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const bitset = base64ToUint8Array(summary.bitsetBase64);
|
|
144
|
+
for (const id of newIds) {
|
|
145
|
+
const idxs = bloomIndexes(id, summary.bitCount, summary.hashCount, summary.salt);
|
|
146
|
+
for (const idx of idxs) {
|
|
147
|
+
const byteIndex = Math.floor(idx / 8);
|
|
148
|
+
const bitOffset = idx % 8;
|
|
149
|
+
bitset[byteIndex] |= 1 << bitOffset;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
...summary,
|
|
154
|
+
totalIds: newTotal,
|
|
155
|
+
bitsetBase64: uint8ArrayToBase64(bitset),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// IDB helpers
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/** Wrap an IDBRequest in a Promise. */
|
|
162
|
+
function reqToPromise(request) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
request.onsuccess = () => resolve(request.result);
|
|
165
|
+
request.onerror = () => reject(request.error);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Wrap an IDBTransaction completion in a Promise. */
|
|
169
|
+
function txToPromise(tx) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
tx.oncomplete = () => resolve();
|
|
172
|
+
tx.onerror = () => reject(tx.error);
|
|
173
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Main class
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/**
|
|
180
|
+
* An IndexedDB-backed ContentAddressedStore.
|
|
181
|
+
*
|
|
182
|
+
* Each instance maps to a single IndexedDB database named
|
|
183
|
+
* `mindoodb_<prefix>_<storeId>` where `prefix` comes from `options.basePath`
|
|
184
|
+
* and defaults to `"default"`.
|
|
185
|
+
*
|
|
186
|
+
* Entries are stored in an `entries` object store with IDB indexes that
|
|
187
|
+
* replace the in-memory Map indexes used by the InMemory and OnDisk stores.
|
|
188
|
+
* Encrypted payloads are deduplicated via a `content` object store keyed
|
|
189
|
+
* by `contentHash` with a `refCount` field.
|
|
190
|
+
*
|
|
191
|
+
* A single `bloom_cache` record avoids recomputing the bloom filter on every
|
|
192
|
+
* `getIdBloomSummary()` call.
|
|
193
|
+
*/
|
|
194
|
+
class IndexedDBContentAddressedStore {
|
|
195
|
+
constructor(storeId, logger, options) {
|
|
196
|
+
this.db = null;
|
|
197
|
+
this.openPromise = null;
|
|
198
|
+
this.storeId = storeId;
|
|
199
|
+
this.logger =
|
|
200
|
+
logger ??
|
|
201
|
+
new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), `IndexedDBStore:${storeId}`, true);
|
|
202
|
+
const prefix = options?.basePath || "default";
|
|
203
|
+
this.idbName = `mindoodb_${prefix}_${storeId}`;
|
|
204
|
+
}
|
|
205
|
+
// -------------------------------------------------------------------------
|
|
206
|
+
// IDB lifecycle
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
/** Lazily open the IDB database, creating object stores on first access. */
|
|
209
|
+
ensureOpen() {
|
|
210
|
+
if (this.db) {
|
|
211
|
+
return Promise.resolve(this.db);
|
|
212
|
+
}
|
|
213
|
+
if (this.openPromise) {
|
|
214
|
+
return this.openPromise;
|
|
215
|
+
}
|
|
216
|
+
this.openPromise = new Promise((resolve, reject) => {
|
|
217
|
+
const request = indexedDB.open(this.idbName, IDB_VERSION);
|
|
218
|
+
request.onupgradeneeded = (event) => {
|
|
219
|
+
const db = event.target.result;
|
|
220
|
+
// entries store
|
|
221
|
+
if (!db.objectStoreNames.contains(ENTRIES_STORE)) {
|
|
222
|
+
const entriesStore = db.createObjectStore(ENTRIES_STORE, {
|
|
223
|
+
keyPath: "id",
|
|
224
|
+
});
|
|
225
|
+
entriesStore.createIndex(IDX_DOC_ID, "docId", { unique: false });
|
|
226
|
+
entriesStore.createIndex(IDX_ENTRY_TYPE_CREATED_AT, ["entryType", "createdAt"], { unique: false });
|
|
227
|
+
entriesStore.createIndex(IDX_CREATED_AT_ID, ["createdAt", "id"], {
|
|
228
|
+
unique: true,
|
|
229
|
+
});
|
|
230
|
+
entriesStore.createIndex(IDX_CONTENT_HASH, "contentHash", {
|
|
231
|
+
unique: false,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// content store
|
|
235
|
+
if (!db.objectStoreNames.contains(CONTENT_STORE)) {
|
|
236
|
+
db.createObjectStore(CONTENT_STORE, { keyPath: "contentHash" });
|
|
237
|
+
}
|
|
238
|
+
// bloom cache store
|
|
239
|
+
if (!db.objectStoreNames.contains(BLOOM_CACHE_STORE)) {
|
|
240
|
+
db.createObjectStore(BLOOM_CACHE_STORE, { keyPath: "key" });
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
request.onsuccess = () => {
|
|
244
|
+
this.db = request.result;
|
|
245
|
+
this.db.onversionchange = () => {
|
|
246
|
+
this.logger.warn("IndexedDB version change detected, closing connection");
|
|
247
|
+
this.db?.close();
|
|
248
|
+
this.db = null;
|
|
249
|
+
this.openPromise = null;
|
|
250
|
+
};
|
|
251
|
+
resolve(this.db);
|
|
252
|
+
};
|
|
253
|
+
request.onerror = () => {
|
|
254
|
+
this.openPromise = null;
|
|
255
|
+
reject(request.error);
|
|
256
|
+
};
|
|
257
|
+
request.onblocked = () => {
|
|
258
|
+
this.logger.warn(`IndexedDB open blocked for ${this.idbName} – close other tabs?`);
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
return this.openPromise;
|
|
262
|
+
}
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// ContentAddressedStore interface
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
getId() {
|
|
267
|
+
return this.storeId;
|
|
268
|
+
}
|
|
269
|
+
async putEntries(entries) {
|
|
270
|
+
if (entries.length === 0)
|
|
271
|
+
return;
|
|
272
|
+
const db = await this.ensureOpen();
|
|
273
|
+
const tx = db.transaction([ENTRIES_STORE, CONTENT_STORE, BLOOM_CACHE_STORE], "readwrite");
|
|
274
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
275
|
+
const contentOS = tx.objectStore(CONTENT_STORE);
|
|
276
|
+
const bloomOS = tx.objectStore(BLOOM_CACHE_STORE);
|
|
277
|
+
const insertedIds = [];
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
// Check if entry already exists (skip duplicate)
|
|
280
|
+
const existing = await reqToPromise(entriesOS.get(entry.id));
|
|
281
|
+
if (existing) {
|
|
282
|
+
this.logger.debug(`Entry ${entry.id} already exists, skipping`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Separate metadata from encrypted data
|
|
286
|
+
const { encryptedData, ...metadata } = entry;
|
|
287
|
+
// Store metadata
|
|
288
|
+
await reqToPromise(entriesOS.put(metadata));
|
|
289
|
+
// Deduplicate content
|
|
290
|
+
const existingContent = await reqToPromise(contentOS.get(entry.contentHash));
|
|
291
|
+
if (existingContent) {
|
|
292
|
+
existingContent.refCount += 1;
|
|
293
|
+
await reqToPromise(contentOS.put(existingContent));
|
|
294
|
+
this.logger.debug(`Content ${entry.contentHash.substring(0, 8)}... already exists (deduplicated)`);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
await reqToPromise(contentOS.put({
|
|
298
|
+
contentHash: entry.contentHash,
|
|
299
|
+
data: encryptedData,
|
|
300
|
+
refCount: 1,
|
|
301
|
+
}));
|
|
302
|
+
this.logger.debug(`Stored content for hash ${entry.contentHash.substring(0, 8)}...`);
|
|
303
|
+
}
|
|
304
|
+
insertedIds.push(entry.id);
|
|
305
|
+
this.logger.debug(`Stored entry ${entry.id} for doc ${entry.docId}`);
|
|
306
|
+
}
|
|
307
|
+
// Incrementally update bloom cache with newly inserted IDs
|
|
308
|
+
if (insertedIds.length > 0) {
|
|
309
|
+
const cached = await reqToPromise(bloomOS.get(BLOOM_CACHE_KEY));
|
|
310
|
+
if (cached && !cached.dirty && cached.summary) {
|
|
311
|
+
// Try incremental update (returns null if rebuild is needed)
|
|
312
|
+
const updated = incrementalBloomUpdate(cached.summary, insertedIds);
|
|
313
|
+
if (updated) {
|
|
314
|
+
await reqToPromise(bloomOS.put({
|
|
315
|
+
key: BLOOM_CACHE_KEY,
|
|
316
|
+
summary: updated,
|
|
317
|
+
dirty: false,
|
|
318
|
+
}));
|
|
319
|
+
this.logger.debug(`Incrementally updated bloom cache (+${insertedIds.length} IDs, total ${updated.totalIds})`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Filter has grown past rebuild threshold; mark dirty for full recompute
|
|
323
|
+
await reqToPromise(bloomOS.put({ key: BLOOM_CACHE_KEY, summary: null, dirty: true }));
|
|
324
|
+
this.logger.debug("Bloom cache exceeded rebuild threshold, marked dirty");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// No valid cache exists; mark dirty so next getIdBloomSummary() rebuilds
|
|
329
|
+
await reqToPromise(bloomOS.put({ key: BLOOM_CACHE_KEY, summary: null, dirty: true }));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await txToPromise(tx);
|
|
333
|
+
}
|
|
334
|
+
async getEntries(ids) {
|
|
335
|
+
if (ids.length === 0)
|
|
336
|
+
return [];
|
|
337
|
+
const db = await this.ensureOpen();
|
|
338
|
+
const tx = db.transaction([ENTRIES_STORE, CONTENT_STORE], "readonly");
|
|
339
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
340
|
+
const contentOS = tx.objectStore(CONTENT_STORE);
|
|
341
|
+
const result = [];
|
|
342
|
+
for (const id of ids) {
|
|
343
|
+
const metadata = (await reqToPromise(entriesOS.get(id)));
|
|
344
|
+
if (!metadata) {
|
|
345
|
+
this.logger.warn(`Entry ${id} not found`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const contentRecord = await reqToPromise(contentOS.get(metadata.contentHash));
|
|
349
|
+
if (!contentRecord) {
|
|
350
|
+
this.logger.warn(`Content ${metadata.contentHash} not found for entry ${id}`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
result.push({
|
|
354
|
+
...metadata,
|
|
355
|
+
encryptedData: new Uint8Array(contentRecord.data),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
this.logger.debug(`Retrieved ${result.length} entries out of ${ids.length} requested`);
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
async hasEntries(ids) {
|
|
362
|
+
if (ids.length === 0)
|
|
363
|
+
return [];
|
|
364
|
+
const db = await this.ensureOpen();
|
|
365
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
366
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
367
|
+
const existing = [];
|
|
368
|
+
for (const id of ids) {
|
|
369
|
+
const key = await reqToPromise(entriesOS.getKey(id));
|
|
370
|
+
if (key !== undefined) {
|
|
371
|
+
existing.push(id);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
this.logger.debug(`Found ${existing.length} existing entries out of ${ids.length} checked`);
|
|
375
|
+
return existing;
|
|
376
|
+
}
|
|
377
|
+
async findNewEntries(knownIds) {
|
|
378
|
+
const knownSet = new Set(knownIds);
|
|
379
|
+
const db = await this.ensureOpen();
|
|
380
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
381
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
382
|
+
const newEntries = [];
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
const cursorReq = entriesOS.openCursor();
|
|
385
|
+
cursorReq.onsuccess = () => {
|
|
386
|
+
const cursor = cursorReq.result;
|
|
387
|
+
if (!cursor) {
|
|
388
|
+
this.logger.debug(`Found ${newEntries.length} new entries out of total`);
|
|
389
|
+
resolve(newEntries);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const metadata = cursor.value;
|
|
393
|
+
if (!knownSet.has(metadata.id)) {
|
|
394
|
+
newEntries.push(metadata);
|
|
395
|
+
}
|
|
396
|
+
cursor.continue();
|
|
397
|
+
};
|
|
398
|
+
cursorReq.onerror = () => reject(cursorReq.error);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
async findNewEntriesForDoc(knownIds, docId) {
|
|
402
|
+
const knownSet = new Set(knownIds);
|
|
403
|
+
const db = await this.ensureOpen();
|
|
404
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
405
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
406
|
+
const index = entriesOS.index(IDX_DOC_ID);
|
|
407
|
+
const newEntries = [];
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
const cursorReq = index.openCursor(IDBKeyRange.only(docId));
|
|
410
|
+
cursorReq.onsuccess = () => {
|
|
411
|
+
const cursor = cursorReq.result;
|
|
412
|
+
if (!cursor) {
|
|
413
|
+
this.logger.debug(`Found ${newEntries.length} new entries for doc ${docId}`);
|
|
414
|
+
resolve(newEntries);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const metadata = cursor.value;
|
|
418
|
+
if (!knownSet.has(metadata.id)) {
|
|
419
|
+
newEntries.push(metadata);
|
|
420
|
+
}
|
|
421
|
+
cursor.continue();
|
|
422
|
+
};
|
|
423
|
+
cursorReq.onerror = () => reject(cursorReq.error);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async findEntries(type, creationDateFrom, creationDateUntil) {
|
|
427
|
+
const db = await this.ensureOpen();
|
|
428
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
429
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
430
|
+
const index = entriesOS.index(IDX_ENTRY_TYPE_CREATED_AT);
|
|
431
|
+
// Build key range on [entryType, createdAt]
|
|
432
|
+
const lower = [type, creationDateFrom ?? -Infinity];
|
|
433
|
+
const upper = [type, creationDateUntil ?? Infinity];
|
|
434
|
+
const range = IDBKeyRange.bound(lower, upper, false, // lowerOpen: inclusive
|
|
435
|
+
creationDateUntil !== null // upperOpen: exclusive when creationDateUntil is set
|
|
436
|
+
);
|
|
437
|
+
const results = [];
|
|
438
|
+
return new Promise((resolve, reject) => {
|
|
439
|
+
const cursorReq = index.openCursor(range);
|
|
440
|
+
cursorReq.onsuccess = () => {
|
|
441
|
+
const cursor = cursorReq.result;
|
|
442
|
+
if (!cursor) {
|
|
443
|
+
this.logger.debug(`Found ${results.length} entries of type ${type} in date range`);
|
|
444
|
+
resolve(results);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
results.push(cursor.value);
|
|
448
|
+
cursor.continue();
|
|
449
|
+
};
|
|
450
|
+
cursorReq.onerror = () => reject(cursorReq.error);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async getAllIds() {
|
|
454
|
+
const db = await this.ensureOpen();
|
|
455
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
456
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
457
|
+
const keys = await reqToPromise(entriesOS.getAllKeys());
|
|
458
|
+
this.logger.debug(`Returning ${keys.length} entry IDs`);
|
|
459
|
+
return keys;
|
|
460
|
+
}
|
|
461
|
+
async scanEntriesSince(cursor, limit = Number.MAX_SAFE_INTEGER, filters) {
|
|
462
|
+
const db = await this.ensureOpen();
|
|
463
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
464
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
465
|
+
const index = entriesOS.index(IDX_CREATED_AT_ID);
|
|
466
|
+
// Build lower bound from cursor
|
|
467
|
+
let range = null;
|
|
468
|
+
if (cursor) {
|
|
469
|
+
range = IDBKeyRange.lowerBound([cursor.createdAt, cursor.id], true);
|
|
470
|
+
}
|
|
471
|
+
const page = [];
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
const cursorReq = index.openCursor(range);
|
|
474
|
+
let hasMore = false;
|
|
475
|
+
cursorReq.onsuccess = () => {
|
|
476
|
+
const idbCursor = cursorReq.result;
|
|
477
|
+
if (!idbCursor) {
|
|
478
|
+
const last = page.length > 0 ? page[page.length - 1] : null;
|
|
479
|
+
resolve({
|
|
480
|
+
entries: page,
|
|
481
|
+
nextCursor: last
|
|
482
|
+
? { createdAt: last.createdAt, id: last.id }
|
|
483
|
+
: cursor,
|
|
484
|
+
hasMore,
|
|
485
|
+
});
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const meta = idbCursor.value;
|
|
489
|
+
if (page.length >= limit) {
|
|
490
|
+
// We already have enough entries. Check if this one matches filters
|
|
491
|
+
// to determine hasMore.
|
|
492
|
+
if (this.matchesScanFilters(meta, filters)) {
|
|
493
|
+
hasMore = true;
|
|
494
|
+
}
|
|
495
|
+
const last = page[page.length - 1];
|
|
496
|
+
resolve({
|
|
497
|
+
entries: page,
|
|
498
|
+
nextCursor: { createdAt: last.createdAt, id: last.id },
|
|
499
|
+
hasMore: true,
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (this.matchesScanFilters(meta, filters)) {
|
|
504
|
+
page.push(meta);
|
|
505
|
+
}
|
|
506
|
+
idbCursor.continue();
|
|
507
|
+
};
|
|
508
|
+
cursorReq.onerror = () => reject(cursorReq.error);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
async getIdBloomSummary() {
|
|
512
|
+
const db = await this.ensureOpen();
|
|
513
|
+
// Try to read from cache first
|
|
514
|
+
const readTx = db.transaction(BLOOM_CACHE_STORE, "readonly");
|
|
515
|
+
const bloomOS = readTx.objectStore(BLOOM_CACHE_STORE);
|
|
516
|
+
const cached = await reqToPromise(bloomOS.get(BLOOM_CACHE_KEY));
|
|
517
|
+
if (cached && !cached.dirty && cached.summary) {
|
|
518
|
+
this.logger.debug("Returning cached bloom summary");
|
|
519
|
+
return cached.summary;
|
|
520
|
+
}
|
|
521
|
+
// Recompute bloom summary
|
|
522
|
+
this.logger.debug("Recomputing bloom summary");
|
|
523
|
+
const ids = await this.getAllIds();
|
|
524
|
+
const summary = createIdBloomSummaryBrowser(ids);
|
|
525
|
+
// Persist to cache
|
|
526
|
+
const writeTx = db.transaction(BLOOM_CACHE_STORE, "readwrite");
|
|
527
|
+
const writeBloomOS = writeTx.objectStore(BLOOM_CACHE_STORE);
|
|
528
|
+
await reqToPromise(writeBloomOS.put({
|
|
529
|
+
key: BLOOM_CACHE_KEY,
|
|
530
|
+
summary,
|
|
531
|
+
dirty: false,
|
|
532
|
+
}));
|
|
533
|
+
await txToPromise(writeTx);
|
|
534
|
+
return summary;
|
|
535
|
+
}
|
|
536
|
+
async resolveDependencies(startId, options) {
|
|
537
|
+
const stopAtEntryType = options?.stopAtEntryType;
|
|
538
|
+
const maxDepth = options?.maxDepth;
|
|
539
|
+
const includeStart = options?.includeStart !== false;
|
|
540
|
+
const db = await this.ensureOpen();
|
|
541
|
+
const tx = db.transaction(ENTRIES_STORE, "readonly");
|
|
542
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
543
|
+
const result = [];
|
|
544
|
+
const visited = new Set();
|
|
545
|
+
const queue = [
|
|
546
|
+
{ id: startId, depth: 0 },
|
|
547
|
+
];
|
|
548
|
+
let queueIdx = 0;
|
|
549
|
+
while (queueIdx < queue.length) {
|
|
550
|
+
const { id, depth } = queue[queueIdx++];
|
|
551
|
+
if (visited.has(id))
|
|
552
|
+
continue;
|
|
553
|
+
visited.add(id);
|
|
554
|
+
if (maxDepth !== undefined && depth > maxDepth)
|
|
555
|
+
continue;
|
|
556
|
+
const entry = (await reqToPromise(entriesOS.get(id)));
|
|
557
|
+
if (!entry) {
|
|
558
|
+
this.logger.warn(`Entry ${id} not found during dependency resolution`);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (stopAtEntryType && entry.entryType === stopAtEntryType && id !== startId) {
|
|
562
|
+
result.push(id);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (id !== startId || includeStart) {
|
|
566
|
+
result.push(id);
|
|
567
|
+
}
|
|
568
|
+
for (const depId of entry.dependencyIds) {
|
|
569
|
+
if (!visited.has(depId)) {
|
|
570
|
+
queue.push({ id: depId, depth: depth + 1 });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
result.reverse();
|
|
575
|
+
this.logger.debug(`Resolved ${result.length} dependencies for ${startId}`);
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
async purgeDocHistory(docId) {
|
|
579
|
+
this.logger.info(`Purging entry history for document: ${docId}`);
|
|
580
|
+
const db = await this.ensureOpen();
|
|
581
|
+
const tx = db.transaction([ENTRIES_STORE, CONTENT_STORE, BLOOM_CACHE_STORE], "readwrite");
|
|
582
|
+
const entriesOS = tx.objectStore(ENTRIES_STORE);
|
|
583
|
+
const contentOS = tx.objectStore(CONTENT_STORE);
|
|
584
|
+
const bloomOS = tx.objectStore(BLOOM_CACHE_STORE);
|
|
585
|
+
const docIndex = entriesOS.index(IDX_DOC_ID);
|
|
586
|
+
// Collect all entries for this docId
|
|
587
|
+
const entriesToPurge = [];
|
|
588
|
+
await new Promise((resolve, reject) => {
|
|
589
|
+
const cursorReq = docIndex.openCursor(IDBKeyRange.only(docId));
|
|
590
|
+
cursorReq.onsuccess = () => {
|
|
591
|
+
const cursor = cursorReq.result;
|
|
592
|
+
if (!cursor) {
|
|
593
|
+
resolve();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
entriesToPurge.push(cursor.value);
|
|
597
|
+
cursor.continue();
|
|
598
|
+
};
|
|
599
|
+
cursorReq.onerror = () => reject(cursorReq.error);
|
|
600
|
+
});
|
|
601
|
+
if (entriesToPurge.length === 0) {
|
|
602
|
+
this.logger.debug(`No entries found for document ${docId}, nothing to purge`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Delete entries and decrement content ref counts
|
|
606
|
+
for (const entry of entriesToPurge) {
|
|
607
|
+
await reqToPromise(entriesOS.delete(entry.id));
|
|
608
|
+
const contentRecord = await reqToPromise(contentOS.get(entry.contentHash));
|
|
609
|
+
if (contentRecord) {
|
|
610
|
+
const newCount = contentRecord.refCount - 1;
|
|
611
|
+
if (newCount <= 0) {
|
|
612
|
+
await reqToPromise(contentOS.delete(entry.contentHash));
|
|
613
|
+
this.logger.debug(`Cleaned up orphaned content ${entry.contentHash.substring(0, 8)}...`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
contentRecord.refCount = newCount;
|
|
617
|
+
await reqToPromise(contentOS.put(contentRecord));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Invalidate bloom cache
|
|
622
|
+
await reqToPromise(bloomOS.put({ key: BLOOM_CACHE_KEY, summary: null, dirty: true }));
|
|
623
|
+
await txToPromise(tx);
|
|
624
|
+
this.logger.info(`Purged ${entriesToPurge.length} entries for document ${docId}`);
|
|
625
|
+
}
|
|
626
|
+
async clearAllLocalData() {
|
|
627
|
+
this.logger.info(`Clearing all local data for store ${this.storeId}`);
|
|
628
|
+
if (this.db) {
|
|
629
|
+
this.db.close();
|
|
630
|
+
this.db = null;
|
|
631
|
+
this.openPromise = null;
|
|
632
|
+
}
|
|
633
|
+
return new Promise((resolve, reject) => {
|
|
634
|
+
const request = indexedDB.deleteDatabase(this.idbName);
|
|
635
|
+
request.onsuccess = () => {
|
|
636
|
+
this.logger.info(`Deleted IndexedDB database ${this.idbName}`);
|
|
637
|
+
resolve();
|
|
638
|
+
};
|
|
639
|
+
request.onerror = () => reject(request.error);
|
|
640
|
+
request.onblocked = () => {
|
|
641
|
+
this.logger.warn(`IndexedDB delete blocked for ${this.idbName} – close other tabs?`);
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
async awaitIndexReady(_options) {
|
|
646
|
+
return {
|
|
647
|
+
phase: "ready",
|
|
648
|
+
indexingEnabled: true,
|
|
649
|
+
progress01: 1,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
getIndexBuildStatus() {
|
|
653
|
+
return {
|
|
654
|
+
phase: "ready",
|
|
655
|
+
indexingEnabled: true,
|
|
656
|
+
progress01: 1,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
// -------------------------------------------------------------------------
|
|
660
|
+
// Private helpers
|
|
661
|
+
// -------------------------------------------------------------------------
|
|
662
|
+
matchesScanFilters(meta, filters) {
|
|
663
|
+
if (filters?.docId && meta.docId !== filters.docId) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
if (filters?.entryTypes &&
|
|
667
|
+
filters.entryTypes.length > 0 &&
|
|
668
|
+
!filters.entryTypes.includes(meta.entryType)) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if (filters?.creationDateFrom !== undefined &&
|
|
672
|
+
filters.creationDateFrom !== null &&
|
|
673
|
+
meta.createdAt < filters.creationDateFrom) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
if (filters?.creationDateUntil !== undefined &&
|
|
677
|
+
filters.creationDateUntil !== null &&
|
|
678
|
+
meta.createdAt >= filters.creationDateUntil) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
exports.IndexedDBContentAddressedStore = IndexedDBContentAddressedStore;
|
|
685
|
+
//# sourceMappingURL=IndexedDBContentAddressedStore.js.map
|