mindoodb 0.0.1 → 0.0.2

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.
Files changed (77) hide show
  1. package/README.md +5 -5
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +9 -2
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +39 -22
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  6. package/dist/appendonlystores/network/HttpTransport.d.ts +8 -2
  7. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  8. package/dist/appendonlystores/network/HttpTransport.js +50 -21
  9. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +15 -2
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +52 -27
  13. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  14. package/dist/core/BaseMindooDB.d.ts +26 -4
  15. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  16. package/dist/core/BaseMindooDB.js +632 -253
  17. package/dist/core/BaseMindooDB.js.map +1 -1
  18. package/dist/core/BaseMindooTenant.d.ts +3 -1
  19. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  20. package/dist/core/BaseMindooTenant.js +83 -75
  21. package/dist/core/BaseMindooTenant.js.map +1 -1
  22. package/dist/core/BaseMindooTenantDirectory.d.ts +3 -1
  23. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  24. package/dist/core/BaseMindooTenantDirectory.js +40 -40
  25. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  26. package/dist/core/BaseMindooTenantFactory.d.ts +3 -1
  27. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  28. package/dist/core/BaseMindooTenantFactory.js +18 -12
  29. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  30. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +8 -2
  31. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  32. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +44 -18
  33. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  34. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +3 -0
  35. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  36. package/dist/core/appendonlystores/network/AuthenticationService.js +21 -17
  37. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  38. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +15 -1
  39. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  40. package/dist/core/appendonlystores/types.d.ts +14 -1
  41. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  42. package/dist/core/crypto/MindooDocSigner.d.ts +3 -1
  43. package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -1
  44. package/dist/core/crypto/MindooDocSigner.js +9 -5
  45. package/dist/core/crypto/MindooDocSigner.js.map +1 -1
  46. package/dist/core/crypto/RSAEncryption.d.ts +3 -1
  47. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
  48. package/dist/core/crypto/RSAEncryption.js +13 -9
  49. package/dist/core/crypto/RSAEncryption.js.map +1 -1
  50. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +1 -1
  51. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +2 -2
  52. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
  53. package/dist/core/indexing/virtualviews/index.d.ts +1 -1
  54. package/dist/core/indexing/virtualviews/index.js +1 -1
  55. package/dist/core/keys/KeyBag.d.ts +4 -1
  56. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  57. package/dist/core/keys/KeyBag.js +10 -5
  58. package/dist/core/keys/KeyBag.js.map +1 -1
  59. package/dist/core/logging/LogLevel.d.ts +21 -0
  60. package/dist/core/logging/LogLevel.d.ts.map +1 -0
  61. package/dist/core/logging/LogLevel.js +56 -0
  62. package/dist/core/logging/LogLevel.js.map +1 -0
  63. package/dist/core/logging/Logger.d.ts +37 -0
  64. package/dist/core/logging/Logger.d.ts.map +1 -0
  65. package/dist/core/logging/Logger.js +3 -0
  66. package/dist/core/logging/Logger.js.map +1 -0
  67. package/dist/core/logging/MindooLogger.d.ts +28 -0
  68. package/dist/core/logging/MindooLogger.d.ts.map +1 -0
  69. package/dist/core/logging/MindooLogger.js +217 -0
  70. package/dist/core/logging/MindooLogger.js.map +1 -0
  71. package/dist/core/logging/index.d.ts +4 -0
  72. package/dist/core/logging/index.d.ts.map +1 -0
  73. package/dist/core/logging/index.js +10 -0
  74. package/dist/core/logging/index.js.map +1 -0
  75. package/dist/core/types.d.ts +96 -28
  76. package/dist/core/types.d.ts.map +1 -1
  77. package/package.json +1 -1
@@ -38,6 +38,7 @@ const Automerge = __importStar(require("@automerge/automerge"));
38
38
  const uuid_1 = require("uuid");
39
39
  const idGeneration_1 = require("./utils/idGeneration");
40
40
  const errors_1 = require("./errors");
41
+ const logging_1 = require("./logging");
41
42
  /**
42
43
  * Default chunk size for attachments: 256KB
43
44
  */
@@ -62,7 +63,7 @@ const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
62
63
  * - Automerge.applyChanges(doc, changes) - verify method signature
63
64
  */
64
65
  class BaseMindooDB {
65
- constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false) {
66
+ constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false, logger, performanceCallback) {
66
67
  // Internal index: sorted array of document entries, maintained in order by (lastModified, docId)
67
68
  // This allows efficient incremental processing without sorting on each call
68
69
  this.index = [];
@@ -75,11 +76,19 @@ class BaseMindooDB {
75
76
  // Index: automergeHash -> entryId for each document
76
77
  // Used for resolving Automerge dependency hashes to entry IDs
77
78
  this.automergeHashToEntryId = new Map(); // Map<docId, Map<automergeHash, entryId>>
79
+ // Cache for imported public keys (CryptoKey objects) to avoid re-importing the same key
80
+ // Map<publicKeyPEM, CryptoKey>
81
+ this.publicKeyCache = new Map();
78
82
  this.tenant = tenant;
79
83
  this.store = store;
80
84
  this.attachmentStore = attachmentStore;
81
85
  this.chunkSizeBytes = attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
82
86
  this._isAdminOnlyDb = adminOnlyDb;
87
+ // Create logger if not provided (for backward compatibility)
88
+ this.logger =
89
+ logger ||
90
+ new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), "BaseMindooDB", true);
91
+ this.performanceCallback = performanceCallback;
83
92
  }
84
93
  /**
85
94
  * Get the admin public key from the tenant.
@@ -91,6 +100,44 @@ class BaseMindooDB {
91
100
  isAdminOnlyDb() {
92
101
  return this._isAdminOnlyDb;
93
102
  }
103
+ /**
104
+ * Get or import a CryptoKey for signature verification, with caching.
105
+ * This avoids re-importing the same public key multiple times.
106
+ */
107
+ async getOrImportPublicKey(publicKey) {
108
+ // Check cache first
109
+ if (this.publicKeyCache.has(publicKey)) {
110
+ return this.publicKeyCache.get(publicKey);
111
+ }
112
+ // Validate the public key is trusted
113
+ const directory = await this.tenant.openDirectory();
114
+ const isTrusted = await directory.validatePublicSigningKey(publicKey);
115
+ if (!isTrusted) {
116
+ this.logger.warn(`Public key not trusted: ${publicKey}`);
117
+ return null;
118
+ }
119
+ // Import the key
120
+ const subtle = this.tenant.getCryptoAdapter().getSubtle();
121
+ const publicKeyBuffer = this.tenant.pemToArrayBuffer(publicKey);
122
+ const cryptoKey = await subtle.importKey("spki", publicKeyBuffer, {
123
+ name: "Ed25519",
124
+ }, false, // not extractable
125
+ ["verify"]);
126
+ // Cache the imported key
127
+ this.publicKeyCache.set(publicKey, cryptoKey);
128
+ return cryptoKey;
129
+ }
130
+ /**
131
+ * Verify a signature using a pre-imported CryptoKey.
132
+ * This bypasses the key import step for better performance when keys are cached.
133
+ */
134
+ async verifySignatureWithKey(cryptoKey, payload, signature) {
135
+ const subtle = this.tenant.getCryptoAdapter().getSubtle();
136
+ const isValid = await subtle.verify({
137
+ name: "Ed25519",
138
+ }, cryptoKey, signature.buffer, payload.buffer);
139
+ return isValid;
140
+ }
94
141
  /**
95
142
  * Compare two index entries for sorting.
96
143
  * Returns negative if a < b, positive if a > b, 0 if equal.
@@ -107,25 +154,33 @@ class BaseMindooDB {
107
154
  * When a document changes, it's removed from its current position and inserted
108
155
  * at the correct sorted position to maintain order by (lastModified, docId).
109
156
  *
157
+ * Optimized to only update lookup map for entries that actually moved.
158
+ *
110
159
  * @param docId The document ID
111
160
  * @param lastModified The new last modified timestamp
112
161
  * @param isDeleted Whether the document is deleted
113
162
  */
114
163
  updateIndex(docId, lastModified, isDeleted) {
115
- // Remove existing entry if present
164
+ const newEntry = { docId, lastModified, isDeleted };
116
165
  const existingIndex = this.indexLookup.get(docId);
166
+ // Check if the entry already exists and hasn't changed position
117
167
  if (existingIndex !== undefined) {
118
- // Remove from array
168
+ const existingEntry = this.index[existingIndex];
169
+ // If position hasn't changed (same lastModified and docId), no update needed
170
+ if (existingEntry.lastModified === lastModified && existingEntry.isDeleted === isDeleted) {
171
+ return; // No change needed
172
+ }
173
+ // Remove from current position
119
174
  this.index.splice(existingIndex, 1);
120
- // Update lookup map for all entries after the removed one
121
- for (let i = existingIndex; i < this.index.length; i++) {
175
+ // Update lookup map for entries that moved (only those after the removed position)
176
+ // We'll update the full range after insertion to be safe
177
+ const minAffectedIndex = Math.min(existingIndex, this.index.length);
178
+ for (let i = minAffectedIndex; i < this.index.length; i++) {
122
179
  this.indexLookup.set(this.index[i].docId, i);
123
180
  }
124
- // Remove the entry from lookup
125
181
  this.indexLookup.delete(docId);
126
182
  }
127
183
  // Find insertion point using binary search to maintain sorted order
128
- const newEntry = { docId, lastModified, isDeleted };
129
184
  let insertIndex = this.index.length; // Default to end
130
185
  // Binary search for insertion point
131
186
  let left = 0;
@@ -144,7 +199,8 @@ class BaseMindooDB {
144
199
  }
145
200
  // Insert at the correct position
146
201
  this.index.splice(insertIndex, 0, newEntry);
147
- // Update lookup map for all entries from insertion point onwards
202
+ // Update lookup map for entries from insertion point onwards
203
+ // Only update entries that actually moved (from insertIndex to end)
148
204
  for (let i = insertIndex; i < this.index.length; i++) {
149
205
  this.indexLookup.set(this.index[i].docId, i);
150
206
  }
@@ -182,7 +238,7 @@ class BaseMindooDB {
182
238
  entryIds.push(entryId);
183
239
  }
184
240
  else {
185
- console.warn(`[BaseMindooDB] Could not resolve automerge hash ${hash} to entry ID for doc ${docId}`);
241
+ this.logger.warn(`Could not resolve automerge hash ${hash} to entry ID for doc ${docId}`);
186
242
  }
187
243
  }
188
244
  return entryIds;
@@ -197,7 +253,7 @@ class BaseMindooDB {
197
253
  * Initialize the database instance.
198
254
  */
199
255
  async initialize() {
200
- console.log(`[BaseMindooDB] Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
256
+ this.logger.info(`Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
201
257
  await this.syncStoreChanges();
202
258
  }
203
259
  /**
@@ -206,13 +262,13 @@ class BaseMindooDB {
206
262
  * On first call (when processedEntryIds is empty), it will process all entries.
207
263
  */
208
264
  async syncStoreChanges() {
209
- console.log(`[BaseMindooDB] Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
210
- console.log(`[BaseMindooDB] Already processed ${this.processedEntryIds.length} entry IDs`);
265
+ this.logger.debug(`Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
266
+ this.logger.debug(`Already processed ${this.processedEntryIds.length} entry IDs`);
211
267
  // Find new entries that we haven't processed yet
212
268
  const newEntryMetadata = await this.store.findNewEntries(this.processedEntryIds);
213
- console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries`);
269
+ this.logger.debug(`Found ${newEntryMetadata.length} new entries`);
214
270
  if (newEntryMetadata.length === 0) {
215
- console.log(`[BaseMindooDB] No new entries to process`);
271
+ this.logger.debug(`No new entries to process`);
216
272
  return;
217
273
  }
218
274
  // Group new entries by document ID
@@ -224,49 +280,92 @@ class BaseMindooDB {
224
280
  entriesByDoc.get(entryMeta.docId).push(entryMeta);
225
281
  }
226
282
  // Process each document with new entries
227
- // Reload documents to apply all changes (including new ones)
228
- console.log(`[BaseMindooDB] Processing ${entriesByDoc.size} documents with new entries`);
229
- for (const [docId, entryMetadataList] of entriesByDoc) {
283
+ // Use incremental cache updates when possible
284
+ // Process documents in parallel with concurrency limit
285
+ this.logger.debug(`Processing ${entriesByDoc.size} documents with new entries`);
286
+ // Helper function to process a single document
287
+ const processDocument = async (docId, entryMetadataList) => {
230
288
  try {
231
- console.log(`[BaseMindooDB] ===== Processing document ${docId} with ${entryMetadataList.length} new entry(s) in syncStoreChanges =====`);
232
- // Clear cache for this document so it gets reloaded with all entries
233
- this.docCache.delete(docId);
234
- console.log(`[BaseMindooDB] Cleared cache for document ${docId}`);
235
- // Reload document (this will load all entries including new ones)
236
- console.log(`[BaseMindooDB] About to call loadDocumentInternal for document ${docId}`);
237
- const doc = await this.loadDocumentInternal(docId);
238
- console.log(`[BaseMindooDB] loadDocumentInternal returned for document ${docId}, result: ${doc ? 'success' : 'null'}`);
239
- if (doc) {
240
- console.log(`[BaseMindooDB] Successfully reloaded document ${docId}, updating index`);
241
- this.updateIndex(docId, doc.lastModified, doc.isDeleted);
242
- console.log(`[BaseMindooDB] Updated index for document ${docId} (lastModified: ${doc.lastModified}, isDeleted: ${doc.isDeleted})`);
289
+ this.logger.debug(`===== Processing document ${docId} with ${entryMetadataList.length} new entry(s) in syncStoreChanges =====`);
290
+ // Check if document is cached
291
+ const cachedDoc = this.docCache.get(docId);
292
+ let updatedDoc = null;
293
+ if (cachedDoc) {
294
+ // Document is cached - try incremental update
295
+ this.logger.debug(`Document ${docId} found in cache, attempting incremental update`);
296
+ try {
297
+ updatedDoc = await this.applyNewEntriesToCachedDocument(cachedDoc, entryMetadataList);
298
+ if (updatedDoc) {
299
+ this.logger.debug(`Successfully updated cached document ${docId} incrementally`);
300
+ // Only update index if document actually changed
301
+ this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
302
+ this.logger.debug(`Updated index for document ${docId} (lastModified: ${updatedDoc.lastModified}, isDeleted: ${updatedDoc.isDeleted})`);
303
+ }
304
+ else {
305
+ this.logger.debug(`Document ${docId} unchanged after applying new entries, skipping index update`);
306
+ }
307
+ }
308
+ catch (error) {
309
+ // If incremental update fails, fall back to full reload
310
+ this.logger.warn(`Incremental update failed for document ${docId}, falling back to full reload:`, error);
311
+ this.docCache.delete(docId);
312
+ updatedDoc = await this.loadDocumentInternal(docId);
313
+ if (updatedDoc) {
314
+ this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
315
+ }
316
+ }
243
317
  }
244
318
  else {
245
- console.warn(`[BaseMindooDB] Document ${docId} returned null from loadDocumentInternal`);
319
+ // Document not cached - load from scratch
320
+ this.logger.debug(`Document ${docId} not in cache, loading from store`);
321
+ updatedDoc = await this.loadDocumentInternal(docId);
322
+ if (updatedDoc) {
323
+ this.logger.debug(`Successfully loaded document ${docId}, updating index`);
324
+ this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
325
+ this.logger.debug(`Updated index for document ${docId} (lastModified: ${updatedDoc.lastModified}, isDeleted: ${updatedDoc.isDeleted})`);
326
+ }
327
+ else {
328
+ this.logger.warn(`Document ${docId} returned null from loadDocumentInternal`);
329
+ }
246
330
  }
247
331
  }
248
332
  catch (error) {
249
333
  // Handle missing symmetric key gracefully - skip documents we can't decrypt
250
334
  if (error instanceof errors_1.SymmetricKeyNotFoundError) {
251
- console.log(`[BaseMindooDB] Skipping document ${docId} - missing key: ${error.keyId}`);
335
+ this.logger.debug(`Skipping document ${docId} - missing key: ${error.keyId}`);
252
336
  // Mark entry IDs as processed so we don't retry them
253
337
  this.processedEntryIds.push(...entryMetadataList.map(em => em.id));
254
- continue; // Skip to next document
255
- }
256
- console.error(`[BaseMindooDB] ===== ERROR processing document ${docId} in syncStoreChanges =====`);
257
- console.error(`[BaseMindooDB] Error type: ${error instanceof Error ? error.constructor.name : typeof error}`);
258
- console.error(`[BaseMindooDB] Error message: ${error instanceof Error ? error.message : String(error)}`);
259
- if (error instanceof Error && error.stack) {
260
- console.error(`[BaseMindooDB] Error stack: ${error.stack}`);
338
+ return; // Skip this document
261
339
  }
340
+ this.logger.error(`===== ERROR processing document ${docId} in syncStoreChanges =====`, error);
262
341
  // Re-throw the error so we can see what's happening in the test
263
- console.error(`[BaseMindooDB] Re-throwing error for document ${docId}`);
264
342
  throw error;
265
343
  }
344
+ };
345
+ // Process documents in parallel with concurrency limit (default: 10)
346
+ const maxConcurrency = 10;
347
+ const documentEntries = Array.from(entriesByDoc.entries());
348
+ // Process in batches to limit concurrency
349
+ const results = [];
350
+ for (let i = 0; i < documentEntries.length; i += maxConcurrency) {
351
+ const batch = documentEntries.slice(i, i + maxConcurrency);
352
+ const batchResults = await Promise.allSettled(batch.map(([docId, entryMetadataList]) => processDocument(docId, entryMetadataList)));
353
+ results.push(...batchResults);
354
+ }
355
+ // Check for errors (excluding SymmetricKeyNotFoundError which is handled gracefully)
356
+ for (let i = 0; i < results.length; i++) {
357
+ const result = results[i];
358
+ if (result.status === 'rejected') {
359
+ const error = result.reason;
360
+ if (!(error instanceof errors_1.SymmetricKeyNotFoundError)) {
361
+ // Re-throw non-key errors
362
+ throw error;
363
+ }
364
+ }
266
365
  }
267
366
  // Append new entry IDs to our processed list
268
367
  this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
269
- console.log(`[BaseMindooDB] Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
368
+ this.logger.debug(`Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
270
369
  }
271
370
  getStore() {
272
371
  return this.store;
@@ -306,12 +405,12 @@ class BaseMindooDB {
306
405
  }
307
406
  // Generate UUID7 for document ID
308
407
  const docId = (0, uuid_1.v7)();
309
- console.log(`[BaseMindooDB] Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
408
+ this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
310
409
  // Create initial Automerge document
311
410
  const initialDoc = Automerge.init();
312
411
  // Create the first change
313
412
  const now = Date.now();
314
- console.log(`[BaseMindooDB] Creating initial Automerge change for document ${docId}`);
413
+ this.logger.debug(`Creating initial Automerge change for document ${docId}`);
315
414
  let newDoc;
316
415
  try {
317
416
  newDoc = Automerge.change(initialDoc, (doc) => {
@@ -319,59 +418,59 @@ class BaseMindooDB {
319
418
  // We need to modify the document to ensure a change is created
320
419
  doc._attachments = [];
321
420
  });
322
- console.log(`[BaseMindooDB] Successfully created Automerge change, document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
421
+ this.logger.debug(`Successfully created Automerge change, document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
323
422
  }
324
423
  catch (error) {
325
- console.error(`[BaseMindooDB] Error in Automerge.change for document ${docId}:`, error);
424
+ this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
326
425
  throw error;
327
426
  }
328
427
  // Get the change bytes from the document
329
- console.log(`[BaseMindooDB] Getting change bytes from document ${docId}`);
428
+ this.logger.debug(`Getting change bytes from document ${docId}`);
330
429
  const changeBytes = Automerge.getLastLocalChange(newDoc);
331
430
  if (!changeBytes) {
332
431
  throw new Error("Failed to get change bytes from Automerge document");
333
432
  }
334
- console.log(`[BaseMindooDB] Got change bytes: ${changeBytes.length} bytes`);
433
+ this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
335
434
  // Decode the change to get hash and dependencies
336
- console.log(`[BaseMindooDB] Decoding change to get hash and dependencies`);
435
+ this.logger.debug(`Decoding change to get hash and dependencies`);
337
436
  let decodedChange;
338
437
  try {
339
438
  decodedChange = Automerge.decodeChange(changeBytes);
340
- console.log(`[BaseMindooDB] Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
439
+ this.logger.debug(`Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
341
440
  }
342
441
  catch (error) {
343
- console.error(`[BaseMindooDB] Error decoding change for document ${docId}:`, error);
442
+ this.logger.error(`Error decoding change for document ${docId}:`, error);
344
443
  throw error;
345
444
  }
346
445
  const automergeHash = decodedChange.hash;
347
446
  const automergeDepHashes = decodedChange.deps || []; // First change has no dependencies
348
447
  // Encrypt the change payload first
349
- console.log(`[BaseMindooDB] Encrypting change payload for document ${docId}`);
448
+ this.logger.debug(`Encrypting change payload for document ${docId}`);
350
449
  const encryptedPayload = await this.tenant.encryptPayload(changeBytes, keyId);
351
- console.log(`[BaseMindooDB] Encrypted payload: ${changeBytes.length} -> ${encryptedPayload.length} bytes`);
450
+ this.logger.debug(`Encrypted payload: ${changeBytes.length} -> ${encryptedPayload.length} bytes`);
352
451
  // Compute content hash from encrypted data
353
452
  const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
354
- console.log(`[BaseMindooDB] Computed content hash: ${contentHash.substring(0, 16)}...`);
453
+ this.logger.debug(`Computed content hash: ${contentHash.substring(0, 16)}...`);
355
454
  // Generate entry ID with blockchain-like chaining
356
455
  const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
357
- console.log(`[BaseMindooDB] Generated entry ID: ${entryId}`);
456
+ this.logger.debug(`Generated entry ID: ${entryId}`);
358
457
  // Resolve Automerge dependency hashes to entry IDs (empty for first change)
359
458
  const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
360
459
  // Sign the encrypted payload - either with custom key or current user's key
361
460
  let signature;
362
461
  let createdByPublicKey;
363
462
  if (useCustomSigningKey) {
364
- console.log(`[BaseMindooDB] Signing encrypted payload for document ${docId} with provided key`);
463
+ this.logger.debug(`Signing encrypted payload for document ${docId} with provided key`);
365
464
  signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
366
465
  createdByPublicKey = signingKeyPair.publicKey;
367
466
  }
368
467
  else {
369
- console.log(`[BaseMindooDB] Signing encrypted payload for document ${docId}`);
468
+ this.logger.debug(`Signing encrypted payload for document ${docId}`);
370
469
  const currentUser = await this.tenant.getCurrentUserId();
371
470
  signature = await this.tenant.signPayload(encryptedPayload);
372
471
  createdByPublicKey = currentUser.userSigningPublicKey;
373
472
  }
374
- console.log(`[BaseMindooDB] Signed payload, signature length: ${signature.length} bytes`);
473
+ this.logger.debug(`Signed payload, signature length: ${signature.length} bytes`);
375
474
  // Create entry metadata
376
475
  const entryMetadata = {
377
476
  entryType: "doc_create",
@@ -407,8 +506,8 @@ class BaseMindooDB {
407
506
  // Update cache and index
408
507
  this.docCache.set(docId, internalDoc);
409
508
  this.updateIndex(docId, internalDoc.lastModified, false);
410
- console.log(`[BaseMindooDB] Document ${docId} created successfully`);
411
- console.log(`[BaseMindooDB] Document ${docId} cached and indexed (lastModified: ${internalDoc.lastModified})`);
509
+ this.logger.info(`Document ${docId} created successfully`);
510
+ this.logger.debug(`Document ${docId} cached and indexed (lastModified: ${internalDoc.lastModified})`);
412
511
  return this.wrapDocument(internalDoc);
413
512
  }
414
513
  async getDocument(docId) {
@@ -422,7 +521,7 @@ class BaseMindooDB {
422
521
  return this.wrapDocument(internalDoc);
423
522
  }
424
523
  async getDocumentAtTimestamp(docId, timestamp) {
425
- console.log(`[BaseMindooDB] Getting document ${docId} at timestamp ${timestamp}`);
524
+ this.logger.debug(`Getting document ${docId} at timestamp ${timestamp}`);
426
525
  // Get all entry metadata for this document
427
526
  const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
428
527
  // Filter entries up to the timestamp
@@ -439,14 +538,14 @@ class BaseMindooDB {
439
538
  for (const entryData of entries) {
440
539
  // Admin-only mode: only accept entries signed by the admin key
441
540
  if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
442
- console.warn(`[BaseMindooDB] Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
541
+ this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
443
542
  continue;
444
543
  }
445
544
  // Verify signature against the encrypted payload (no decryption needed)
446
545
  // We sign the encrypted payload, so anyone can verify signatures without decryption keys
447
546
  const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
448
547
  if (!isValid) {
449
- console.warn(`[BaseMindooDB] Invalid signature for entry ${entryData.id}, skipping`);
548
+ this.logger.warn(`Invalid signature for entry ${entryData.id}, skipping`);
450
549
  continue;
451
550
  }
452
551
  // Decrypt payload (only after signature verification passes)
@@ -461,24 +560,125 @@ class BaseMindooDB {
461
560
  }
462
561
  // Check if document was deleted at this timestamp
463
562
  // Look for a "doc_delete" type entry in the relevant entries
464
- const hasDeleteEntry = relevantEntries.some(em => em.entryType === "doc_delete" && em.createdAt <= timestamp);
465
- if (hasDeleteEntry) {
466
- return null; // Document was deleted at this time
467
- }
563
+ const deleteEntry = relevantEntries.find(em => em.entryType === "doc_delete" && em.createdAt <= timestamp);
564
+ const isDeleted = deleteEntry !== undefined;
468
565
  // Find the first entry to get createdAt and decryptionKeyId
469
566
  const firstEntry = relevantEntries.length > 0 ? relevantEntries[0] : null;
470
567
  const createdAt = firstEntry ? firstEntry.createdAt : timestamp;
471
568
  const decryptionKeyId = firstEntry ? firstEntry.decryptionKeyId : "default";
569
+ // Use delete entry timestamp as lastModified if deleted, otherwise use the requested timestamp
570
+ const lastModified = isDeleted && deleteEntry ? deleteEntry.createdAt : timestamp;
472
571
  const internalDoc = {
473
572
  id: docId,
474
573
  doc,
475
574
  createdAt,
476
- lastModified: timestamp,
575
+ lastModified,
477
576
  decryptionKeyId,
478
- isDeleted: false,
577
+ isDeleted,
479
578
  };
480
579
  return this.wrapDocument(internalDoc);
481
580
  }
581
+ async *iterateDocumentHistory(docId) {
582
+ this.logger.debug(`Iterating document history for ${docId}`);
583
+ // Get all entry metadata for this document
584
+ const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
585
+ // Filter to only doc_create, doc_change, and doc_delete entries (exclude snapshots)
586
+ const relevantEntries = allEntryMetadata
587
+ .filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete")
588
+ .sort((a, b) => a.createdAt - b.createdAt);
589
+ if (relevantEntries.length === 0) {
590
+ return; // Document has no history
591
+ }
592
+ // Load all entries
593
+ const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
594
+ // Build a map for quick lookup
595
+ const entryMap = new Map(entries.map(e => [e.id, e]));
596
+ // Apply changes in order
597
+ let currentDoc = null;
598
+ let createdAt = null;
599
+ let decryptionKeyId = "default";
600
+ for (const entryMetadata of relevantEntries) {
601
+ const entryData = entryMap.get(entryMetadata.id);
602
+ if (!entryData) {
603
+ this.logger.warn(`Entry ${entryMetadata.id} not found in store, skipping`);
604
+ continue;
605
+ }
606
+ // Admin-only mode: only accept entries signed by the admin key
607
+ if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
608
+ this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
609
+ continue;
610
+ }
611
+ // Verify signature
612
+ const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
613
+ if (!isValid) {
614
+ this.logger.warn(`Invalid signature for entry ${entryData.id}, skipping`);
615
+ continue;
616
+ }
617
+ // Decrypt payload
618
+ const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
619
+ // Initialize document if this is the first entry (doc_create)
620
+ const isFirstEntry = currentDoc === null;
621
+ if (isFirstEntry) {
622
+ if (entryMetadata.entryType !== "doc_create") {
623
+ this.logger.warn(`First entry is not doc_create, skipping`);
624
+ continue;
625
+ }
626
+ currentDoc = Automerge.init();
627
+ createdAt = entryMetadata.createdAt;
628
+ decryptionKeyId = entryMetadata.decryptionKeyId;
629
+ }
630
+ // Check document heads before applying change (for non-first entries)
631
+ const headsBefore = isFirstEntry ? null : (currentDoc ? Automerge.getHeads(currentDoc) : null);
632
+ // Apply change using loadIncremental
633
+ if (currentDoc === null) {
634
+ throw new Error("currentDoc should not be null at this point");
635
+ }
636
+ currentDoc = Automerge.loadIncremental(currentDoc, decryptedPayload);
637
+ // Check if document actually changed by comparing heads
638
+ // For first entry (doc_create), always yield since it's the initial creation
639
+ // For delete entries, always yield
640
+ // For other entries, only yield if heads changed
641
+ let shouldYield = false;
642
+ if (isFirstEntry || entryMetadata.entryType === "doc_delete") {
643
+ shouldYield = true;
644
+ }
645
+ else {
646
+ const headsAfter = Automerge.getHeads(currentDoc);
647
+ const headsChanged = headsBefore !== null && JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
648
+ shouldYield = headsChanged;
649
+ }
650
+ // Register automerge hash mapping
651
+ const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
652
+ if (parsed) {
653
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
654
+ }
655
+ // Only yield if document actually changed (or if this is create/delete entry)
656
+ if (shouldYield) {
657
+ // Clone the document to ensure independence
658
+ const clonedDoc = Automerge.clone(currentDoc);
659
+ // Create internal doc representation
660
+ const internalDoc = {
661
+ id: docId,
662
+ doc: clonedDoc,
663
+ createdAt: createdAt,
664
+ lastModified: entryMetadata.createdAt,
665
+ decryptionKeyId,
666
+ isDeleted: entryMetadata.entryType === "doc_delete",
667
+ };
668
+ // Wrap and yield (including delete entries)
669
+ const wrappedDoc = this.wrapDocument(internalDoc);
670
+ yield {
671
+ doc: wrappedDoc,
672
+ changeCreatedAt: entryMetadata.createdAt,
673
+ changeCreatedByPublicKey: entryMetadata.createdByPublicKey,
674
+ };
675
+ }
676
+ // Stop after delete entry (document is deleted, no more changes)
677
+ if (entryMetadata.entryType === "doc_delete") {
678
+ break;
679
+ }
680
+ }
681
+ }
482
682
  async getAllDocumentIds() {
483
683
  // Return all non-deleted document IDs from index
484
684
  const docIds = [];
@@ -489,6 +689,49 @@ class BaseMindooDB {
489
689
  }
490
690
  return docIds;
491
691
  }
692
+ async getAllDocumentIdsAtTimestamp(timestamp) {
693
+ // Efficiently query for doc_create and doc_delete entries before the timestamp
694
+ const [creates, deletes] = await Promise.all([
695
+ this.store.findEntries("doc_create", null, timestamp),
696
+ this.store.findEntries("doc_delete", null, timestamp)
697
+ ]);
698
+ // Build sets of docIds for efficient lookup
699
+ const createdDocIds = new Set();
700
+ const deletedDocIds = new Set();
701
+ // Track earliest create time for each doc
702
+ const createTimes = new Map();
703
+ for (const entry of creates) {
704
+ const existingTime = createTimes.get(entry.docId);
705
+ if (!existingTime || entry.createdAt < existingTime) {
706
+ createTimes.set(entry.docId, entry.createdAt);
707
+ createdDocIds.add(entry.docId);
708
+ }
709
+ }
710
+ // Track earliest delete time for each doc
711
+ const deleteTimes = new Map();
712
+ for (const entry of deletes) {
713
+ const existingTime = deleteTimes.get(entry.docId);
714
+ if (!existingTime || entry.createdAt < existingTime) {
715
+ deleteTimes.set(entry.docId, entry.createdAt);
716
+ deletedDocIds.add(entry.docId);
717
+ }
718
+ }
719
+ // Find documents that existed at the timestamp
720
+ const docIds = [];
721
+ for (const docId of createdDocIds) {
722
+ const createTime = createTimes.get(docId);
723
+ const deleteTime = deleteTimes.get(docId);
724
+ // Document exists at timestamp if:
725
+ // 1. It was created before the timestamp
726
+ // 2. Either it was never deleted, or it was deleted after the timestamp
727
+ if (createTime < timestamp) {
728
+ if (!deleteTime || deleteTime > timestamp) {
729
+ docIds.push(docId);
730
+ }
731
+ }
732
+ }
733
+ return docIds;
734
+ }
492
735
  async deleteDocument(docId) {
493
736
  return this.deleteDocInternal(docId);
494
737
  }
@@ -502,7 +745,7 @@ class BaseMindooDB {
502
745
  */
503
746
  async deleteDocInternal(docId, signingKeyPair, signingKeyPassword) {
504
747
  const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
505
- console.log(`[BaseMindooDB] Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
748
+ this.logger.debug(`Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
506
749
  // Admin-only validation: only admin key can modify data in admin-only databases
507
750
  if (this._isAdminOnlyDb) {
508
751
  const adminPublicKey = this.getAdminPublicKey();
@@ -550,12 +793,12 @@ class BaseMindooDB {
550
793
  let signature;
551
794
  let createdByPublicKey;
552
795
  if (useCustomSigningKey) {
553
- console.log(`[BaseMindooDB] Signing deletion for document ${docId} with provided key`);
796
+ this.logger.debug(`Signing deletion for document ${docId} with provided key`);
554
797
  signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
555
798
  createdByPublicKey = signingKeyPair.publicKey;
556
799
  }
557
800
  else {
558
- console.log(`[BaseMindooDB] Signing deletion for document ${docId} with current user's key`);
801
+ this.logger.debug(`Signing deletion for document ${docId} with current user's key`);
559
802
  const currentUser = await this.tenant.getCurrentUserId();
560
803
  signature = await this.tenant.signPayload(encryptedPayload);
561
804
  createdByPublicKey = currentUser.userSigningPublicKey;
@@ -588,7 +831,7 @@ class BaseMindooDB {
588
831
  internalDoc.lastModified = Date.now();
589
832
  this.docCache.set(docId, internalDoc);
590
833
  this.updateIndex(docId, internalDoc.lastModified, true);
591
- console.log(`[BaseMindooDB] Document ${docId} deleted successfully`);
834
+ this.logger.info(`Document ${docId} deleted successfully`);
592
835
  }
593
836
  async changeDoc(doc, changeFunc) {
594
837
  return this.changeDocInternal(doc, changeFunc);
@@ -599,7 +842,7 @@ class BaseMindooDB {
599
842
  async changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword) {
600
843
  const docId = doc.getId();
601
844
  const useCustomKey = signingKeyPair && signingKeyPassword;
602
- console.log(`[BaseMindooDB] ===== ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'} called for document ${docId} =====`);
845
+ this.logger.debug(`===== ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'} called for document ${docId} =====`);
603
846
  // Admin-only validation: only admin key can modify data in admin-only databases
604
847
  if (this._isAdminOnlyDb) {
605
848
  const adminPublicKey = this.getAdminPublicKey();
@@ -613,24 +856,24 @@ class BaseMindooDB {
613
856
  // Get internal document from cache or load it
614
857
  let internalDoc = this.docCache.get(docId);
615
858
  if (!internalDoc) {
616
- console.log(`[BaseMindooDB] Document ${docId} not in cache, loading from store`);
859
+ this.logger.debug(`Document ${docId} not in cache, loading from store`);
617
860
  const loadedDoc = await this.loadDocumentInternal(docId);
618
861
  if (!loadedDoc) {
619
862
  throw new Error(`Document ${docId} not found`);
620
863
  }
621
864
  internalDoc = loadedDoc;
622
- console.log(`[BaseMindooDB] Successfully loaded document ${docId} from store for ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'}`);
865
+ this.logger.debug(`Successfully loaded document ${docId} from store for ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'}`);
623
866
  }
624
867
  else {
625
- console.log(`[BaseMindooDB] Document ${docId} found in cache`);
868
+ this.logger.debug(`Document ${docId} found in cache`);
626
869
  }
627
870
  if (internalDoc.isDeleted) {
628
871
  throw new Error(`Document ${docId} has been deleted`);
629
872
  }
630
873
  // Apply the change function
631
874
  const now = Date.now();
632
- console.log(`[BaseMindooDB] Applying change function to document ${docId}`);
633
- console.log(`[BaseMindooDB] Document state before change: heads=${JSON.stringify(Automerge.getHeads(internalDoc.doc))}`);
875
+ this.logger.debug(`Applying change function to document ${docId}`);
876
+ this.logger.debug(`Document state before change: heads=${JSON.stringify(Automerge.getHeads(internalDoc.doc))}`);
634
877
  // For async callbacks, we need to handle document modifications carefully.
635
878
  // Automerge.change() requires synchronous modifications, so we'll:
636
879
  // 1. Execute the async callback to do any async work (like signing)
@@ -844,20 +1087,20 @@ class BaseMindooDB {
844
1087
  // Update lastModified timestamp
845
1088
  automergeDoc._lastModified = now;
846
1089
  });
847
- console.log(`[BaseMindooDB] Successfully applied change function, new document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
1090
+ this.logger.debug(`Successfully applied change function, new document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
848
1091
  }
849
1092
  catch (error) {
850
- console.error(`[BaseMindooDB] Error in Automerge.change for document ${docId}:`, error);
1093
+ this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
851
1094
  throw error;
852
1095
  }
853
1096
  // Get the change bytes from the document
854
- console.log(`[BaseMindooDB] Getting change bytes from document ${docId}`);
1097
+ this.logger.debug(`Getting change bytes from document ${docId}`);
855
1098
  const changeBytes = Automerge.getLastLocalChange(newDoc);
856
1099
  if (!changeBytes) {
857
1100
  //TODO decide if we just exit here or throw an error
858
1101
  throw new Error("Failed to get change bytes from Automerge document");
859
1102
  }
860
- console.log(`[BaseMindooDB] Got change bytes: ${changeBytes.length} bytes`);
1103
+ this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
861
1104
  // Decode the change to get hash and dependencies
862
1105
  const decodedChange = Automerge.decodeChange(changeBytes);
863
1106
  const automergeHash = decodedChange.hash;
@@ -868,7 +1111,7 @@ class BaseMindooDB {
868
1111
  const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
869
1112
  // Generate entry ID with blockchain-like chaining
870
1113
  const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
871
- console.log(`[BaseMindooDB] Generated entry ID for change: ${entryId}`);
1114
+ this.logger.debug(`Generated entry ID for change: ${entryId}`);
872
1115
  // Resolve Automerge dependency hashes to entry IDs
873
1116
  const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
874
1117
  // Sign the encrypted payload - use custom key if provided, otherwise current user's key
@@ -911,12 +1154,12 @@ class BaseMindooDB {
911
1154
  internalDoc.lastModified = Date.now();
912
1155
  this.docCache.set(docId, internalDoc);
913
1156
  this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted);
914
- console.log(`[BaseMindooDB] Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
1157
+ this.logger.info(`Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
915
1158
  }
916
- async processChangesSince(cursor, limit, callback) {
1159
+ async *iterateChangesSince(cursor) {
917
1160
  // Default to initial cursor if null is provided
918
1161
  const actualCursor = cursor ?? { lastModified: 0, docId: "" };
919
- console.log(`[BaseMindooDB] Processing changes since cursor ${JSON.stringify(actualCursor)} (limit: ${limit})`);
1162
+ this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
920
1163
  // Find starting position using binary search
921
1164
  // We want to find the first entry that is greater than the cursor
922
1165
  let startIndex = 0;
@@ -944,77 +1187,190 @@ class BaseMindooDB {
944
1187
  }
945
1188
  }
946
1189
  }
947
- // Process documents in order from the starting position
948
- let processedCount = 0;
949
- let lastCursor = actualCursor;
950
- for (let i = startIndex; i < this.index.length && processedCount < limit; i++) {
1190
+ // Pre-check which documents are in cache to optimize iteration
1191
+ const uncachedDocIds = [];
1192
+ for (let i = startIndex; i < this.index.length; i++) {
951
1193
  const entry = this.index[i];
952
- // Skip deleted documents (they're still in index for tracking, but shouldn't be processed)
953
- if (entry.isDeleted) {
954
- continue;
1194
+ if (!this.docCache.has(entry.docId)) {
1195
+ uncachedDocIds.push(entry.docId);
1196
+ }
1197
+ }
1198
+ // Prefetch uncached documents in batches (if any)
1199
+ if (uncachedDocIds.length > 0) {
1200
+ this.logger.debug(`Prefetching ${uncachedDocIds.length} uncached documents for iteration`);
1201
+ const prefetchBatchSize = 10;
1202
+ for (let i = 0; i < uncachedDocIds.length; i += prefetchBatchSize) {
1203
+ const batch = uncachedDocIds.slice(i, i + prefetchBatchSize);
1204
+ await Promise.all(batch.map(docId => this.loadDocumentInternal(docId).catch(err => {
1205
+ this.logger.warn(`Failed to prefetch document ${docId}:`, err);
1206
+ return null;
1207
+ })));
955
1208
  }
1209
+ }
1210
+ // Iterate through the index and yield documents one at a time
1211
+ // Documents should now be in cache from prefetching
1212
+ for (let i = startIndex; i < this.index.length; i++) {
1213
+ const entry = this.index[i];
956
1214
  try {
957
- console.log(`[BaseMindooDB] Processing document ${entry.docId} from index (lastModified: ${entry.lastModified})`);
958
- const doc = await this.getDocument(entry.docId);
959
- console.log(`[BaseMindooDB] Successfully loaded document ${entry.docId}`);
1215
+ this.logger.debug(`Yielding document ${entry.docId} from index (lastModified: ${entry.lastModified}, isDeleted: ${entry.isDeleted})`);
1216
+ // Check cache first (should be there after prefetching)
1217
+ let internalDoc = this.docCache.get(entry.docId) || null;
1218
+ if (!internalDoc) {
1219
+ // Fallback to loading if not in cache (shouldn't happen after prefetching)
1220
+ this.logger.debug(`Document ${entry.docId} not in cache, loading from store`);
1221
+ internalDoc = await this.loadDocumentInternal(entry.docId);
1222
+ }
1223
+ if (!internalDoc) {
1224
+ this.logger.warn(`Document ${entry.docId} not found, skipping`);
1225
+ continue;
1226
+ }
1227
+ // Wrap the document (works for both deleted and non-deleted documents)
1228
+ const doc = this.wrapDocument(internalDoc);
1229
+ this.logger.debug(`Successfully loaded document ${entry.docId} (isDeleted: ${doc.isDeleted()})`);
960
1230
  // Create cursor for current document
961
1231
  const currentCursor = {
962
1232
  lastModified: entry.lastModified,
963
1233
  docId: entry.docId,
964
1234
  };
965
- const shouldContinue = callback(doc, currentCursor);
966
- // Update cursor to the last successfully processed document
967
- lastCursor = currentCursor;
968
- processedCount++;
969
- // Stop if callback returns false
970
- if (shouldContinue === false) {
971
- console.log(`[BaseMindooDB] Callback requested to stop processing`);
972
- break;
973
- }
1235
+ // Yield immediately - this allows the caller to break early after each document
1236
+ // Deleted documents are included so external indexes can handle deletions
1237
+ yield { doc, cursor: currentCursor };
974
1238
  }
975
1239
  catch (error) {
976
- console.error(`[BaseMindooDB] Error processing document ${entry.docId}:`, error);
1240
+ this.logger.error(`Error processing document ${entry.docId}:`, error);
977
1241
  // Stop processing on error
978
1242
  throw error;
979
1243
  }
980
1244
  }
981
- console.log(`[BaseMindooDB] Processed ${processedCount} changes, last cursor: ${JSON.stringify(lastCursor)}`);
982
- return lastCursor;
1245
+ this.logger.debug(`Iteration completed`);
983
1246
  }
984
- async *iterateChangesSince(cursor, pageSize = 100) {
985
- console.log(`[BaseMindooDB] Starting iteration from cursor ${JSON.stringify(cursor)} (pageSize: ${pageSize})`);
986
- let currentCursor = cursor;
987
- while (true) {
988
- // Collect documents from this page
989
- const pageResults = [];
990
- let documentsProcessed = 0;
991
- // Process one page of changes
992
- const returnedCursor = await this.processChangesSince(currentCursor, pageSize, (doc, docCursor) => {
993
- documentsProcessed++;
994
- pageResults.push({ doc, cursor: docCursor });
995
- // Always continue processing the page
996
- return true;
997
- });
998
- // Yield all documents from this page
999
- for (const result of pageResults) {
1000
- yield result;
1247
+ /**
1248
+ * Incrementally update a cached document with new entries.
1249
+ * Returns the updated document, or null if document wasn't actually changed.
1250
+ */
1251
+ async applyNewEntriesToCachedDocument(cachedDoc, newEntryMetadata) {
1252
+ const docId = cachedDoc.id;
1253
+ if (newEntryMetadata.length === 0) {
1254
+ this.logger.debug(`No new entries for cached document ${docId}`);
1255
+ return null; // No changes
1256
+ }
1257
+ this.logger.debug(`Applying ${newEntryMetadata.length} new entries to cached document ${docId}`);
1258
+ // Get current document heads to check if it changes
1259
+ const headsBefore = Automerge.getHeads(cachedDoc.doc);
1260
+ // Filter entries to only include change entries (exclude snapshots)
1261
+ const entriesToApply = newEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
1262
+ if (entriesToApply.length === 0) {
1263
+ this.logger.debug(`No change entries to apply for document ${docId}`);
1264
+ return null; // No changes
1265
+ }
1266
+ // Sort entries by timestamp
1267
+ entriesToApply.sort((a, b) => a.createdAt - b.createdAt);
1268
+ // Load entries from store
1269
+ const entries = await this.store.getEntries(entriesToApply.map(em => em.id));
1270
+ // Filter entries for admin-only mode first
1271
+ const validEntries = entries.filter(entryData => {
1272
+ if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
1273
+ this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
1274
+ return false;
1001
1275
  }
1002
- // If we processed fewer documents than the page size, we've reached the end
1003
- if (documentsProcessed < pageSize) {
1004
- console.log(`[BaseMindooDB] Reached end of documents, processed ${documentsProcessed} in last page`);
1005
- break;
1276
+ return true;
1277
+ });
1278
+ if (validEntries.length === 0) {
1279
+ this.logger.debug(`No valid entries to process for cached document ${docId}`);
1280
+ return null;
1281
+ }
1282
+ // Batch signature verification with key caching
1283
+ // Group entries by public key to import each key only once
1284
+ const entriesByPublicKey = new Map();
1285
+ for (const entryData of validEntries) {
1286
+ if (!entriesByPublicKey.has(entryData.createdByPublicKey)) {
1287
+ entriesByPublicKey.set(entryData.createdByPublicKey, []);
1006
1288
  }
1007
- // If the cursor didn't advance, we've reached the end
1008
- if (currentCursor !== null &&
1009
- returnedCursor.lastModified === currentCursor.lastModified &&
1010
- returnedCursor.docId === currentCursor.docId) {
1011
- console.log(`[BaseMindooDB] Cursor did not advance, reached end of documents`);
1012
- break;
1289
+ entriesByPublicKey.get(entryData.createdByPublicKey).push(entryData);
1290
+ }
1291
+ // Import all unique public keys in parallel (with caching)
1292
+ const keyImportResults = await Promise.all(Array.from(entriesByPublicKey.keys()).map(async (publicKey) => {
1293
+ const cryptoKey = await this.getOrImportPublicKey(publicKey);
1294
+ return { publicKey, cryptoKey };
1295
+ }));
1296
+ // Create a map of public key -> CryptoKey for quick lookup
1297
+ const keyMap = new Map();
1298
+ for (const { publicKey, cryptoKey } of keyImportResults) {
1299
+ if (cryptoKey) {
1300
+ keyMap.set(publicKey, cryptoKey);
1301
+ }
1302
+ }
1303
+ // Verify all signatures in parallel using cached keys
1304
+ const signatureVerificationResults = await Promise.all(validEntries.map(async (entryData) => {
1305
+ const cryptoKey = keyMap.get(entryData.createdByPublicKey);
1306
+ if (!cryptoKey) {
1307
+ // Key was not trusted or failed to import
1308
+ return { entryData, isValid: false };
1309
+ }
1310
+ const isValid = await this.verifySignatureWithKey(cryptoKey, entryData.encryptedData, entryData.signature);
1311
+ return { entryData, isValid };
1312
+ }));
1313
+ // Filter out entries with invalid signatures
1314
+ const verifiedEntries = signatureVerificationResults
1315
+ .filter(({ isValid }) => isValid)
1316
+ .map(({ entryData }) => entryData);
1317
+ if (verifiedEntries.length === 0) {
1318
+ this.logger.debug(`No entries with valid signatures for cached document ${docId}`);
1319
+ return null;
1320
+ }
1321
+ // Parallel decryption - decrypt all entries concurrently
1322
+ const decryptionResults = await Promise.all(verifiedEntries.map(async (entryData) => {
1323
+ const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
1324
+ return { entryData, decryptedPayload };
1325
+ }));
1326
+ // Collect change bytes and register automerge hash mappings
1327
+ const changeBytes = [];
1328
+ for (const { entryData, decryptedPayload } of decryptionResults) {
1329
+ changeBytes.push(decryptedPayload);
1330
+ // Register automerge hash -> entry ID mapping
1331
+ const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
1332
+ if (parsed) {
1333
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
1013
1334
  }
1014
- // Continue with the next page using the returned cursor
1015
- currentCursor = returnedCursor;
1016
1335
  }
1017
- console.log(`[BaseMindooDB] Iteration completed`);
1336
+ if (changeBytes.length === 0) {
1337
+ this.logger.debug(`No valid change bytes to apply for document ${docId}`);
1338
+ return null; // No changes
1339
+ }
1340
+ // Clone the cached document to avoid "outdated document" error
1341
+ // This happens when the document has been wrapped and returned to the user
1342
+ // Automerge marks documents as outdated when they're accessed, preventing direct mutation
1343
+ const clonedDoc = Automerge.clone(cachedDoc.doc);
1344
+ // Apply all changes at once (Automerge handles dependency ordering)
1345
+ const result = Automerge.applyChanges(clonedDoc, changeBytes);
1346
+ const updatedDoc = result[0];
1347
+ // Check if document actually changed
1348
+ const headsAfter = Automerge.getHeads(updatedDoc);
1349
+ const headsChanged = JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
1350
+ if (!headsChanged) {
1351
+ this.logger.debug(`Document ${docId} heads unchanged after applying new entries`);
1352
+ return null; // Document didn't actually change
1353
+ }
1354
+ this.logger.debug(`Document ${docId} changed: heads before=${JSON.stringify(headsBefore)}, after=${JSON.stringify(headsAfter)}`);
1355
+ // Update metadata
1356
+ const payload = updatedDoc;
1357
+ const lastEntry = entries[entries.length - 1];
1358
+ const lastModified = payload._lastModified ||
1359
+ (lastEntry ? lastEntry.createdAt : cachedDoc.lastModified);
1360
+ // Check if document was deleted
1361
+ const hasDeleteEntry = newEntryMetadata.some(em => em.entryType === "doc_delete");
1362
+ const isDeleted = hasDeleteEntry || cachedDoc.isDeleted;
1363
+ const updatedInternalDoc = {
1364
+ id: docId,
1365
+ doc: updatedDoc,
1366
+ createdAt: cachedDoc.createdAt,
1367
+ lastModified,
1368
+ decryptionKeyId: cachedDoc.decryptionKeyId,
1369
+ isDeleted,
1370
+ };
1371
+ // Update cache
1372
+ this.docCache.set(docId, updatedInternalDoc);
1373
+ return updatedInternalDoc;
1018
1374
  }
1019
1375
  /**
1020
1376
  * Internal method to load a document from the content-addressed store
@@ -1022,25 +1378,25 @@ class BaseMindooDB {
1022
1378
  async loadDocumentInternal(docId) {
1023
1379
  // Check cache first
1024
1380
  if (this.docCache.has(docId)) {
1025
- console.log(`[BaseMindooDB] Document ${docId} found in cache, returning cached version`);
1381
+ this.logger.debug(`Document ${docId} found in cache, returning cached version`);
1026
1382
  return this.docCache.get(docId);
1027
1383
  }
1028
- console.log(`[BaseMindooDB] ===== Starting to load document ${docId} from store =====`);
1384
+ this.logger.debug(`===== Starting to load document ${docId} from store =====`);
1029
1385
  // Get all entry metadata for this document
1030
1386
  // TODO: Implement loading from last snapshot if available
1031
- console.log(`[BaseMindooDB] Getting all entry hashes for document ${docId}`);
1387
+ this.logger.debug(`Getting all entry hashes for document ${docId}`);
1032
1388
  const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
1033
- console.log(`[BaseMindooDB] Found ${allEntryMetadata.length} total entry hashes for document ${docId}`);
1389
+ this.logger.debug(`Found ${allEntryMetadata.length} total entry hashes for document ${docId}`);
1034
1390
  if (allEntryMetadata.length === 0) {
1035
- console.log(`[BaseMindooDB] No entry hashes found for document ${docId}, returning null`);
1391
+ this.logger.debug(`No entry hashes found for document ${docId}, returning null`);
1036
1392
  return null;
1037
1393
  }
1038
1394
  // Log all entry types
1039
1395
  const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
1040
- console.log(`[BaseMindooDB] Entry types for ${docId}: ${entryTypes}`);
1396
+ this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
1041
1397
  // Find the most recent snapshot (if any)
1042
1398
  const snapshots = allEntryMetadata.filter(em => em.entryType === "doc_snapshot");
1043
- console.log(`[BaseMindooDB] Found ${snapshots.length} snapshot(s) for document ${docId}`);
1399
+ this.logger.debug(`Found ${snapshots.length} snapshot(s) for document ${docId}`);
1044
1400
  let startFromSnapshot = false;
1045
1401
  let snapshotMeta = null;
1046
1402
  if (snapshots.length > 0) {
@@ -1048,29 +1404,29 @@ class BaseMindooDB {
1048
1404
  snapshots.sort((a, b) => b.createdAt - a.createdAt);
1049
1405
  snapshotMeta = snapshots[0];
1050
1406
  startFromSnapshot = true;
1051
- console.log(`[BaseMindooDB] Will start from snapshot ${snapshotMeta.id} created at ${snapshotMeta.createdAt}`);
1407
+ this.logger.debug(`Will start from snapshot ${snapshotMeta.id} created at ${snapshotMeta.createdAt}`);
1052
1408
  }
1053
1409
  else {
1054
- console.log(`[BaseMindooDB] No snapshot found, will start from scratch`);
1410
+ this.logger.debug(`No snapshot found, will start from scratch`);
1055
1411
  }
1056
1412
  // Get all entries (excluding snapshot entries - we'll handle delete separately)
1057
1413
  // Include "doc_create", "doc_change", and "doc_delete" types as they all contain Automerge changes to apply
1058
1414
  const entriesToLoad = startFromSnapshot
1059
1415
  ? allEntryMetadata.filter(em => (em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete") && em.createdAt > snapshotMeta.createdAt)
1060
1416
  : allEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
1061
- console.log(`[BaseMindooDB] Will load ${entriesToLoad.length} entries for document ${docId} (after snapshot filter)`);
1417
+ this.logger.debug(`Will load ${entriesToLoad.length} entries for document ${docId} (after snapshot filter)`);
1062
1418
  // Load the snapshot first if we have one
1063
1419
  let doc = undefined;
1064
1420
  if (startFromSnapshot && snapshotMeta) {
1065
- console.log(`[BaseMindooDB] Loading snapshot for document ${docId}`);
1421
+ this.logger.debug(`Loading snapshot for document ${docId}`);
1066
1422
  const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
1067
- console.log(`[BaseMindooDB] Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
1423
+ this.logger.debug(`Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
1068
1424
  if (snapshotEntries.length > 0) {
1069
1425
  const snapshotData = snapshotEntries[0];
1070
1426
  // Admin-only mode: only accept snapshots signed by admin
1071
1427
  let isValid = false;
1072
1428
  if (this._isAdminOnlyDb && snapshotData.createdByPublicKey !== this.getAdminPublicKey()) {
1073
- console.warn(`[BaseMindooDB] Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
1429
+ this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
1074
1430
  }
1075
1431
  else {
1076
1432
  // Verify signature against the encrypted snapshot (no decryption needed)
@@ -1078,21 +1434,21 @@ class BaseMindooDB {
1078
1434
  isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
1079
1435
  }
1080
1436
  if (!isValid) {
1081
- console.warn(`[BaseMindooDB] Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
1437
+ this.logger.warn(`Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
1082
1438
  // Fall back to loading from scratch
1083
1439
  startFromSnapshot = false;
1084
1440
  }
1085
1441
  else {
1086
- console.log(`[BaseMindooDB] Snapshot signature valid, decrypting snapshot`);
1442
+ this.logger.debug(`Snapshot signature valid, decrypting snapshot`);
1087
1443
  // Decrypt snapshot (only after signature verification passes)
1088
1444
  const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
1089
- console.log(`[BaseMindooDB] Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
1445
+ this.logger.debug(`Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
1090
1446
  // Load snapshot using Automerge.load()
1091
1447
  // This deserializes a full document snapshot from binary data
1092
1448
  // According to Automerge docs: load() is equivalent to init() followed by loadIncremental()
1093
- console.log(`[BaseMindooDB] Loading snapshot into Automerge document`);
1449
+ this.logger.debug(`Loading snapshot into Automerge document`);
1094
1450
  doc = Automerge.load(decryptedSnapshot);
1095
- console.log(`[BaseMindooDB] Successfully loaded snapshot, document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
1451
+ this.logger.debug(`Successfully loaded snapshot, document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
1096
1452
  // Register the snapshot's automerge hash -> entry ID mapping
1097
1453
  const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
1098
1454
  if (parsed) {
@@ -1103,96 +1459,119 @@ class BaseMindooDB {
1103
1459
  }
1104
1460
  // If we don't have a snapshot, start from scratch
1105
1461
  if (!doc) {
1106
- console.log(`[BaseMindooDB] Initializing new Automerge document for ${docId}`);
1462
+ this.logger.debug(`Initializing new Automerge document for ${docId}`);
1107
1463
  doc = Automerge.init();
1108
- console.log(`[BaseMindooDB] Initialized empty document, heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
1464
+ this.logger.debug(`Initialized empty document, heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
1109
1465
  }
1110
1466
  // Sort entries by timestamp
1111
1467
  entriesToLoad.sort((a, b) => a.createdAt - b.createdAt);
1112
- console.log(`[BaseMindooDB] Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
1468
+ this.logger.debug(`Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
1113
1469
  // Load and apply all entries
1114
- console.log(`[BaseMindooDB] Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
1470
+ this.logger.debug(`Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
1115
1471
  const entries = await this.store.getEntries(entriesToLoad.map(em => em.id));
1116
- console.log(`[BaseMindooDB] Retrieved ${entries.length} entries from store for document ${docId}`);
1117
- console.log(`[BaseMindooDB] Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? 'starting from snapshot' : 'starting from scratch'})`);
1472
+ this.logger.debug(`Retrieved ${entries.length} entries from store for document ${docId}`);
1473
+ this.logger.debug(`Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? 'starting from snapshot' : 'starting from scratch'})`);
1118
1474
  // Log current document state before applying entries
1119
- console.log(`[BaseMindooDB] Document state before applying entries: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
1120
- // Verify signatures, decrypt, and apply entries one at a time
1121
- // This ensures dependencies are handled correctly by Automerge
1122
- for (let i = 0; i < entries.length; i++) {
1123
- const entryData = entries[i];
1124
- console.log(`[BaseMindooDB] ===== Processing entry ${i + 1}/${entries.length} for document ${docId} =====`);
1125
- console.log(`[BaseMindooDB] Entry id: ${entryData.id}`);
1126
- console.log(`[BaseMindooDB] Entry type: ${entryData.entryType}`);
1127
- console.log(`[BaseMindooDB] Entry createdAt: ${entryData.createdAt}`);
1128
- console.log(`[BaseMindooDB] Entry dependencies: ${JSON.stringify(entryData.dependencyIds || [])}`);
1129
- console.log(`[BaseMindooDB] Entry payload size: ${entryData.encryptedData.length} bytes`);
1130
- // Admin-only mode: only accept entries signed by the admin key
1131
- // This prevents recursion and ensures security for the directory database
1475
+ this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
1476
+ // Filter entries for admin-only mode first
1477
+ const validEntries = entries.filter(entryData => {
1132
1478
  if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
1133
- console.warn(`[BaseMindooDB] Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
1134
- continue;
1479
+ this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
1480
+ return false;
1135
1481
  }
1136
- // Verify signature against the encrypted payload (no decryption needed)
1137
- // We sign the encrypted payload, so anyone can verify signatures without decryption keys
1138
- console.log(`[BaseMindooDB] Verifying signature for entry ${entryData.id}`);
1139
- const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
1140
- if (!isValid) {
1141
- console.warn(`[BaseMindooDB] Invalid signature for entry ${entryData.id}, skipping`);
1142
- continue;
1482
+ return true;
1483
+ });
1484
+ if (validEntries.length === 0) {
1485
+ this.logger.debug(`No valid entries to process for document ${docId}`);
1486
+ }
1487
+ else {
1488
+ // Batch signature verification with key caching
1489
+ // Group entries by public key to import each key only once
1490
+ this.logger.debug(`Verifying ${validEntries.length} signatures in parallel with key caching`);
1491
+ const entriesByPublicKey = new Map();
1492
+ for (const entryData of validEntries) {
1493
+ if (!entriesByPublicKey.has(entryData.createdByPublicKey)) {
1494
+ entriesByPublicKey.set(entryData.createdByPublicKey, []);
1495
+ }
1496
+ entriesByPublicKey.get(entryData.createdByPublicKey).push(entryData);
1143
1497
  }
1144
- console.log(`[BaseMindooDB] Signature valid for entry ${entryData.id}`);
1145
- // Decrypt payload (only after signature verification passes)
1146
- console.log(`[BaseMindooDB] Decrypting payload for entry ${entryData.id} with key ${entryData.decryptionKeyId}`);
1147
- const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
1148
- console.log(`[BaseMindooDB] Decrypted payload: ${entryData.encryptedData.length} -> ${decryptedPayload.length} bytes`);
1149
- // Apply change using applyChanges with raw change bytes
1150
- // applyChanges expects an array of Uint8Array (raw change bytes)
1151
- try {
1152
- console.log(`[BaseMindooDB] Document state before applying entry ${i + 1}: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
1153
- console.log(`[BaseMindooDB] Calling Automerge.applyChanges for entry ${i + 1}/${entries.length} on document ${docId}`);
1154
- console.log(`[BaseMindooDB] Decrypted payload length: ${decryptedPayload.length} bytes`);
1155
- const currentDoc = doc;
1156
- const result = Automerge.applyChanges(currentDoc, [decryptedPayload]);
1157
- doc = result[0];
1158
- console.log(`[BaseMindooDB] Successfully applied entry ${i + 1}/${entries.length}`);
1159
- console.log(`[BaseMindooDB] Document state after applying entry ${i + 1}: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
1160
- // Register the automerge hash -> entry ID mapping for future dependency resolution
1161
- const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
1162
- if (parsed) {
1163
- this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
1498
+ // Import all unique public keys in parallel (with caching)
1499
+ const keyImportResults = await Promise.all(Array.from(entriesByPublicKey.keys()).map(async (publicKey) => {
1500
+ const cryptoKey = await this.getOrImportPublicKey(publicKey);
1501
+ return { publicKey, cryptoKey };
1502
+ }));
1503
+ // Create a map of public key -> CryptoKey for quick lookup
1504
+ const keyMap = new Map();
1505
+ for (const { publicKey, cryptoKey } of keyImportResults) {
1506
+ if (cryptoKey) {
1507
+ keyMap.set(publicKey, cryptoKey);
1164
1508
  }
1165
1509
  }
1166
- catch (error) {
1167
- console.error(`[BaseMindooDB] Error applying entry ${entryData.id} to document ${docId}:`, error);
1168
- console.error(`[BaseMindooDB] Entry index: ${i + 1}/${entries.length}`);
1169
- console.error(`[BaseMindooDB] Entry dependencies:`, entryData.dependencyIds);
1170
- console.error(`[BaseMindooDB] Entry type:`, entryData.entryType);
1171
- console.error(`[BaseMindooDB] Entry createdAt:`, entryData.createdAt);
1172
- console.error(`[BaseMindooDB] Document state before entry:`, {
1173
- hasDoc: !!doc,
1174
- docHeads: doc ? Automerge.getHeads(doc) : null,
1175
- });
1176
- // Log previous entry for debugging dependency issues
1177
- if (i > 0) {
1178
- const prevEntry = entries[i - 1];
1179
- console.error(`[BaseMindooDB] Previous entry:`, {
1180
- id: prevEntry.id,
1181
- deps: prevEntry.dependencyIds,
1182
- type: prevEntry.entryType,
1183
- });
1510
+ // Verify all signatures in parallel using cached keys
1511
+ const signatureVerificationResults = await Promise.all(validEntries.map(async (entryData) => {
1512
+ const cryptoKey = keyMap.get(entryData.createdByPublicKey);
1513
+ if (!cryptoKey) {
1514
+ // Key was not trusted or failed to import
1515
+ return { entryData, isValid: false };
1516
+ }
1517
+ const isValid = await this.verifySignatureWithKey(cryptoKey, entryData.encryptedData, entryData.signature);
1518
+ return { entryData, isValid };
1519
+ }));
1520
+ // Filter out entries with invalid signatures
1521
+ const verifiedEntries = signatureVerificationResults
1522
+ .filter(({ isValid }) => {
1523
+ if (!isValid) {
1524
+ this.logger.warn(`Invalid signature for entry, skipping`);
1525
+ }
1526
+ return isValid;
1527
+ })
1528
+ .map(({ entryData }) => entryData);
1529
+ if (verifiedEntries.length === 0) {
1530
+ this.logger.debug(`No entries with valid signatures for document ${docId}`);
1531
+ }
1532
+ else {
1533
+ // Parallel decryption - decrypt all entries concurrently
1534
+ // Automerge handles dependency buffering internally, so we can decrypt all in parallel
1535
+ this.logger.debug(`Decrypting ${verifiedEntries.length} entries in parallel`);
1536
+ const decryptionResults = await Promise.all(verifiedEntries.map(async (entryData) => {
1537
+ const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
1538
+ return { entryData, decryptedPayload };
1539
+ }));
1540
+ // Collect change bytes and register automerge hash mappings
1541
+ const changeBytes = [];
1542
+ for (const { entryData, decryptedPayload } of decryptionResults) {
1543
+ changeBytes.push(decryptedPayload);
1544
+ // Register the automerge hash -> entry ID mapping for future dependency resolution
1545
+ const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
1546
+ if (parsed) {
1547
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
1548
+ }
1549
+ }
1550
+ // Batch apply all changes at once - Automerge handles dependency ordering
1551
+ if (changeBytes.length > 0) {
1552
+ this.logger.debug(`Applying ${changeBytes.length} changes to document ${docId} using batch applyChanges`);
1553
+ try {
1554
+ const result = Automerge.applyChanges(doc, changeBytes);
1555
+ doc = result[0];
1556
+ this.logger.debug(`Successfully applied ${changeBytes.length} changes to document ${docId}`);
1557
+ this.logger.debug(`Document state after applying changes: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
1558
+ }
1559
+ catch (error) {
1560
+ this.logger.error(`Error applying changes to document ${docId}:`, error);
1561
+ this.logger.error(`Number of changes: ${changeBytes.length}`);
1562
+ throw error;
1563
+ }
1184
1564
  }
1185
- throw error;
1186
1565
  }
1187
1566
  }
1188
1567
  // Extract metadata from document (doc is guaranteed to be defined at this point)
1189
- console.log(`[BaseMindooDB] All entries applied successfully for document ${docId}`);
1190
- console.log(`[BaseMindooDB] Final document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
1568
+ this.logger.debug(`All entries applied successfully for document ${docId}`);
1569
+ this.logger.debug(`Final document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
1191
1570
  const payload = doc;
1192
1571
  // Check if document was deleted by looking for a "doc_delete" type entry
1193
1572
  const hasDeleteEntry = allEntryMetadata.some(em => em.entryType === "doc_delete");
1194
1573
  const isDeleted = hasDeleteEntry;
1195
- console.log(`[BaseMindooDB] Document ${docId} isDeleted: ${isDeleted}`);
1574
+ this.logger.debug(`Document ${docId} isDeleted: ${isDeleted}`);
1196
1575
  const decryptionKeyId = payload._decryptionKeyId || "default";
1197
1576
  // Get lastModified from payload, or use the timestamp of the last entry
1198
1577
  const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
@@ -1201,7 +1580,7 @@ class BaseMindooDB {
1201
1580
  // Get createdAt from the first entry
1202
1581
  const firstEntry = allEntryMetadata.length > 0 ? allEntryMetadata[0] : null;
1203
1582
  const createdAt = firstEntry ? firstEntry.createdAt : lastModified;
1204
- console.log(`[BaseMindooDB] Document ${docId} metadata: createdAt=${createdAt}, lastModified=${lastModified}, decryptionKeyId=${decryptionKeyId}`);
1583
+ this.logger.debug(`Document ${docId} metadata: createdAt=${createdAt}, lastModified=${lastModified}, decryptionKeyId=${decryptionKeyId}`);
1205
1584
  const internalDoc = {
1206
1585
  id: docId,
1207
1586
  doc: doc, // doc is guaranteed to be defined at this point
@@ -1212,7 +1591,7 @@ class BaseMindooDB {
1212
1591
  };
1213
1592
  // Update cache
1214
1593
  this.docCache.set(docId, internalDoc);
1215
- console.log(`[BaseMindooDB] ===== Successfully loaded document ${docId} and cached it =====`);
1594
+ this.logger.debug(`===== Successfully loaded document ${docId} and cached it =====`);
1216
1595
  return internalDoc;
1217
1596
  }
1218
1597
  /**
@@ -1299,12 +1678,12 @@ class BaseMindooDB {
1299
1678
  * Internal method to fetch and concatenate all chunks for an attachment.
1300
1679
  */
1301
1680
  async getAttachmentInternal(docId, attachmentId) {
1302
- console.log(`[BaseMindooDB] Getting attachment ${attachmentId} from document ${docId}`);
1681
+ this.logger.debug(`Getting attachment ${attachmentId} from document ${docId}`);
1303
1682
  const ref = this.getAttachmentRefInternal(docId, attachmentId);
1304
1683
  const store = this.getEffectiveAttachmentStore();
1305
1684
  // Resolve dependency chain to get all chunk IDs in order (oldest first)
1306
1685
  const chunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
1307
- console.log(`[BaseMindooDB] Resolved ${chunkIds.length} chunks for attachment ${attachmentId}`);
1686
+ this.logger.debug(`Resolved ${chunkIds.length} chunks for attachment ${attachmentId}`);
1308
1687
  // Fetch all chunks
1309
1688
  const chunks = await store.getEntries(chunkIds);
1310
1689
  // Verify signatures, decrypt, and collect plaintext chunks
@@ -1332,14 +1711,14 @@ class BaseMindooDB {
1332
1711
  result.set(chunk, offset);
1333
1712
  offset += chunk.length;
1334
1713
  }
1335
- console.log(`[BaseMindooDB] Retrieved attachment ${attachmentId}: ${result.length} bytes`);
1714
+ this.logger.debug(`Retrieved attachment ${attachmentId}: ${result.length} bytes`);
1336
1715
  return result;
1337
1716
  }
1338
1717
  /**
1339
1718
  * Internal method to get a byte range from an attachment.
1340
1719
  */
1341
1720
  async getAttachmentRangeInternal(docId, attachmentId, startByte, endByte) {
1342
- console.log(`[BaseMindooDB] Getting attachment ${attachmentId} range [${startByte}, ${endByte}) from document ${docId}`);
1721
+ this.logger.debug(`Getting attachment ${attachmentId} range [${startByte}, ${endByte}) from document ${docId}`);
1343
1722
  if (startByte < 0 || endByte <= startByte) {
1344
1723
  throw new Error(`Invalid byte range: [${startByte}, ${endByte})`);
1345
1724
  }
@@ -1388,14 +1767,14 @@ class BaseMindooDB {
1388
1767
  resultOffset += bytesToCopy;
1389
1768
  bytesRemaining -= bytesToCopy;
1390
1769
  }
1391
- console.log(`[BaseMindooDB] Retrieved attachment ${attachmentId} range: ${result.length} bytes`);
1770
+ this.logger.debug(`Retrieved attachment ${attachmentId} range: ${result.length} bytes`);
1392
1771
  return result;
1393
1772
  }
1394
1773
  /**
1395
1774
  * Internal async generator to stream attachment data.
1396
1775
  */
1397
1776
  async *streamAttachmentInternal(docId, attachmentId, startOffset) {
1398
- console.log(`[BaseMindooDB] Streaming attachment ${attachmentId} from offset ${startOffset}`);
1777
+ this.logger.debug(`Streaming attachment ${attachmentId} from offset ${startOffset}`);
1399
1778
  const ref = this.getAttachmentRefInternal(docId, attachmentId);
1400
1779
  const store = this.getEffectiveAttachmentStore();
1401
1780
  const chunkSize = this.chunkSizeBytes;
@@ -1426,7 +1805,7 @@ class BaseMindooDB {
1426
1805
  yield plaintext;
1427
1806
  }
1428
1807
  }
1429
- console.log(`[BaseMindooDB] Finished streaming attachment ${attachmentId}`);
1808
+ this.logger.debug(`Finished streaming attachment ${attachmentId}`);
1430
1809
  }
1431
1810
  /**
1432
1811
  * Get the effective attachment store (attachment store if configured, otherwise doc store).
@@ -1438,7 +1817,7 @@ class BaseMindooDB {
1438
1817
  * Internal method to add an attachment by chunking the file and storing chunks.
1439
1818
  */
1440
1819
  async addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, createdAt) {
1441
- console.log(`[BaseMindooDB] Adding attachment to document ${docId}: ${fileName} (${fileData.length} bytes)`);
1820
+ this.logger.debug(`Adding attachment to document ${docId}: ${fileName} (${fileData.length} bytes)`);
1442
1821
  const store = this.getEffectiveAttachmentStore();
1443
1822
  const currentUser = await this.tenant.getCurrentUserId();
1444
1823
  const attachmentId = (0, idGeneration_1.generateFileUuid7)();
@@ -1477,7 +1856,7 @@ class BaseMindooDB {
1477
1856
  }
1478
1857
  // Store all chunks
1479
1858
  await store.putEntries(chunks);
1480
- console.log(`[BaseMindooDB] Stored ${chunks.length} chunks for attachment ${attachmentId}`);
1859
+ this.logger.debug(`Stored ${chunks.length} chunks for attachment ${attachmentId}`);
1481
1860
  // Create attachment reference
1482
1861
  const ref = {
1483
1862
  attachmentId,
@@ -1496,7 +1875,7 @@ class BaseMindooDB {
1496
1875
  * Memory efficient - processes data chunk by chunk without loading entire file into memory.
1497
1876
  */
1498
1877
  async addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, createdAt) {
1499
- console.log(`[BaseMindooDB] Adding streaming attachment to document ${docId}: ${fileName}`);
1878
+ this.logger.debug(`Adding streaming attachment to document ${docId}: ${fileName}`);
1500
1879
  const store = this.getEffectiveAttachmentStore();
1501
1880
  const currentUser = await this.tenant.getCurrentUserId();
1502
1881
  const attachmentId = (0, idGeneration_1.generateFileUuid7)();
@@ -1561,7 +1940,7 @@ class BaseMindooDB {
1561
1940
  lastChunkId = await storeChunk(buffer);
1562
1941
  totalSize += buffer.length;
1563
1942
  }
1564
- console.log(`[BaseMindooDB] Stored ${chunkCount} chunks for streaming attachment ${attachmentId} (${totalSize} bytes)`);
1943
+ this.logger.debug(`Stored ${chunkCount} chunks for streaming attachment ${attachmentId} (${totalSize} bytes)`);
1565
1944
  // Create attachment reference
1566
1945
  const ref = {
1567
1946
  attachmentId,
@@ -1579,7 +1958,7 @@ class BaseMindooDB {
1579
1958
  * Internal method to append data to an existing attachment.
1580
1959
  */
1581
1960
  async appendToAttachmentInternal(docId, attachmentId, decryptionKeyId, prevLastChunkId, data, createdAt) {
1582
- console.log(`[BaseMindooDB] Appending ${data.length} bytes to attachment ${attachmentId}`);
1961
+ this.logger.debug(`Appending ${data.length} bytes to attachment ${attachmentId}`);
1583
1962
  const store = this.getEffectiveAttachmentStore();
1584
1963
  const currentUser = await this.tenant.getCurrentUserId();
1585
1964
  // Chunk the data
@@ -1617,7 +1996,7 @@ class BaseMindooDB {
1617
1996
  }
1618
1997
  // Store all chunks
1619
1998
  await store.putEntries(chunks);
1620
- console.log(`[BaseMindooDB] Appended ${chunks.length} chunks to attachment ${attachmentId}`);
1999
+ this.logger.debug(`Appended ${chunks.length} chunks to attachment ${attachmentId}`);
1621
2000
  return {
1622
2001
  lastChunkId,
1623
2002
  sizeIncrease: data.length,
@@ -1639,28 +2018,28 @@ class BaseMindooDB {
1639
2018
  if (this.store.getId() !== remoteStore.getId()) {
1640
2019
  throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
1641
2020
  }
1642
- console.log(`[BaseMindooDB] Pulling entries from remote store ${remoteStore.getId()}`);
2021
+ this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}`);
1643
2022
  // Get all entry IDs we already have in our local store
1644
2023
  const localEntryIds = await this.store.getAllIds();
1645
- console.log(`[BaseMindooDB] Local store has ${localEntryIds.length} entry IDs`);
2024
+ this.logger.debug(`Local store has ${localEntryIds.length} entry IDs`);
1646
2025
  // Find entries in the remote store that we don't have
1647
2026
  const newEntryMetadata = await remoteStore.findNewEntries(localEntryIds);
1648
- console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries in remote store`);
2027
+ this.logger.debug(`Found ${newEntryMetadata.length} new entries in remote store`);
1649
2028
  if (newEntryMetadata.length === 0) {
1650
- console.log(`[BaseMindooDB] No new entries to pull`);
2029
+ this.logger.debug(`No new entries to pull`);
1651
2030
  return;
1652
2031
  }
1653
2032
  // Get the full entries from the remote store
1654
2033
  const newEntries = await remoteStore.getEntries(newEntryMetadata.map(em => em.id));
1655
- console.log(`[BaseMindooDB] Retrieved ${newEntries.length} entries from remote store`);
2034
+ this.logger.debug(`Retrieved ${newEntries.length} entries from remote store`);
1656
2035
  // Store all entries in our local store
1657
2036
  // The putEntries method handles deduplication (no-op if entry already exists)
1658
2037
  await this.store.putEntries(newEntries);
1659
- console.log(`[BaseMindooDB] Stored ${newEntries.length} entries in local store`);
2038
+ this.logger.debug(`Stored ${newEntries.length} entries in local store`);
1660
2039
  // Sync the local store to process the new entries
1661
2040
  // This will update the index, cache, and processedEntryIds
1662
2041
  await this.syncStoreChanges();
1663
- console.log(`[BaseMindooDB] Pull complete, synced ${newEntries.length} entries`);
2042
+ this.logger.info(`Pull complete, synced ${newEntries.length} entries`);
1664
2043
  }
1665
2044
  /**
1666
2045
  * Push changes to a remote content-addressed store.
@@ -1677,24 +2056,24 @@ class BaseMindooDB {
1677
2056
  if (this.store.getId() !== remoteStore.getId()) {
1678
2057
  throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
1679
2058
  }
1680
- console.log(`[BaseMindooDB] Pushing entries to remote store ${remoteStore.getId()}`);
2059
+ this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}`);
1681
2060
  // Get all entry IDs the remote store already has
1682
2061
  const remoteEntryIds = await remoteStore.getAllIds();
1683
- console.log(`[BaseMindooDB] Remote store has ${remoteEntryIds.length} entry IDs`);
2062
+ this.logger.debug(`Remote store has ${remoteEntryIds.length} entry IDs`);
1684
2063
  // Find entries in our local store that the remote doesn't have
1685
2064
  const newEntryMetadata = await this.store.findNewEntries(remoteEntryIds);
1686
- console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries to push`);
2065
+ this.logger.debug(`Found ${newEntryMetadata.length} new entries to push`);
1687
2066
  if (newEntryMetadata.length === 0) {
1688
- console.log(`[BaseMindooDB] No new entries to push`);
2067
+ this.logger.debug(`No new entries to push`);
1689
2068
  return;
1690
2069
  }
1691
2070
  // Get the full entries from our local store
1692
2071
  const newEntries = await this.store.getEntries(newEntryMetadata.map(em => em.id));
1693
- console.log(`[BaseMindooDB] Retrieved ${newEntries.length} entries from local store`);
2072
+ this.logger.debug(`Retrieved ${newEntries.length} entries from local store`);
1694
2073
  // Store all entries in the remote store
1695
2074
  // The putEntries method handles deduplication (no-op if entry already exists)
1696
2075
  await remoteStore.putEntries(newEntries);
1697
- console.log(`[BaseMindooDB] Pushed ${newEntries.length} entries to remote store`);
2076
+ this.logger.info(`Pushed ${newEntries.length} entries to remote store`);
1698
2077
  }
1699
2078
  }
1700
2079
  exports.BaseMindooDB = BaseMindooDB;