mindoodb 0.0.1 → 0.0.3

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