mindoodb 0.0.1

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 (170) hide show
  1. package/README.md +263 -0
  2. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts +5 -0
  3. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -0
  4. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +17 -0
  5. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -0
  6. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +111 -0
  7. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -0
  8. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +237 -0
  9. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -0
  10. package/dist/appendonlystores/network/HttpTransport.d.ts +78 -0
  11. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -0
  12. package/dist/appendonlystores/network/HttpTransport.js +356 -0
  13. package/dist/appendonlystores/network/HttpTransport.js.map +1 -0
  14. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +131 -0
  15. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -0
  16. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +248 -0
  17. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -0
  18. package/dist/appendonlystores/network/index.d.ts +8 -0
  19. package/dist/appendonlystores/network/index.d.ts.map +1 -0
  20. package/dist/appendonlystores/network/index.js +21 -0
  21. package/dist/appendonlystores/network/index.js.map +1 -0
  22. package/dist/browser/crypto/BrowserCryptoAdapter.d.ts +14 -0
  23. package/dist/browser/crypto/BrowserCryptoAdapter.d.ts.map +1 -0
  24. package/dist/browser/crypto/BrowserCryptoAdapter.js +30 -0
  25. package/dist/browser/crypto/BrowserCryptoAdapter.js.map +1 -0
  26. package/dist/browser/index.d.ts +3 -0
  27. package/dist/browser/index.d.ts.map +1 -0
  28. package/dist/browser/index.js +26 -0
  29. package/dist/browser/index.js.map +1 -0
  30. package/dist/core/BaseMindooDB.d.ts +182 -0
  31. package/dist/core/BaseMindooDB.d.ts.map +1 -0
  32. package/dist/core/BaseMindooDB.js +1701 -0
  33. package/dist/core/BaseMindooDB.js.map +1 -0
  34. package/dist/core/BaseMindooTenant.d.ts +155 -0
  35. package/dist/core/BaseMindooTenant.d.ts.map +1 -0
  36. package/dist/core/BaseMindooTenant.js +673 -0
  37. package/dist/core/BaseMindooTenant.js.map +1 -0
  38. package/dist/core/BaseMindooTenantDirectory.d.ts +99 -0
  39. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -0
  40. package/dist/core/BaseMindooTenantDirectory.js +776 -0
  41. package/dist/core/BaseMindooTenantDirectory.js.map +1 -0
  42. package/dist/core/BaseMindooTenantFactory.d.ts +91 -0
  43. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -0
  44. package/dist/core/BaseMindooTenantFactory.js +297 -0
  45. package/dist/core/BaseMindooTenantFactory.js.map +1 -0
  46. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +111 -0
  47. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -0
  48. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +293 -0
  49. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -0
  50. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +78 -0
  51. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -0
  52. package/dist/core/appendonlystores/network/AuthenticationService.js +297 -0
  53. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -0
  54. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +174 -0
  55. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -0
  56. package/dist/core/appendonlystores/network/NetworkTransport.js +3 -0
  57. package/dist/core/appendonlystores/network/NetworkTransport.js.map +1 -0
  58. package/dist/core/appendonlystores/network/types.d.ts +135 -0
  59. package/dist/core/appendonlystores/network/types.d.ts.map +1 -0
  60. package/dist/core/appendonlystores/network/types.js +35 -0
  61. package/dist/core/appendonlystores/network/types.js.map +1 -0
  62. package/dist/core/appendonlystores/types.d.ts +157 -0
  63. package/dist/core/appendonlystores/types.d.ts.map +1 -0
  64. package/dist/core/appendonlystores/types.js +3 -0
  65. package/dist/core/appendonlystores/types.js.map +1 -0
  66. package/dist/core/crypto/CryptoAdapter.d.ts +19 -0
  67. package/dist/core/crypto/CryptoAdapter.d.ts.map +1 -0
  68. package/dist/core/crypto/CryptoAdapter.js +3 -0
  69. package/dist/core/crypto/CryptoAdapter.js.map +1 -0
  70. package/dist/core/crypto/MindooDocSigner.d.ts +106 -0
  71. package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -0
  72. package/dist/core/crypto/MindooDocSigner.js +192 -0
  73. package/dist/core/crypto/MindooDocSigner.js.map +1 -0
  74. package/dist/core/crypto/RSAEncryption.d.ts +62 -0
  75. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -0
  76. package/dist/core/crypto/RSAEncryption.js +167 -0
  77. package/dist/core/crypto/RSAEncryption.js.map +1 -0
  78. package/dist/core/errors.d.ts +10 -0
  79. package/dist/core/errors.d.ts.map +1 -0
  80. package/dist/core/errors.js +17 -0
  81. package/dist/core/errors.js.map +1 -0
  82. package/dist/core/index.d.ts +14 -0
  83. package/dist/core/index.d.ts.map +1 -0
  84. package/dist/core/index.js +53 -0
  85. package/dist/core/index.js.map +1 -0
  86. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts +33 -0
  87. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts.map +1 -0
  88. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js +27 -0
  89. package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js.map +1 -0
  90. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts +31 -0
  91. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts.map +1 -0
  92. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js +3 -0
  93. package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js.map +1 -0
  94. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +52 -0
  95. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -0
  96. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +106 -0
  97. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -0
  98. package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts +44 -0
  99. package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts.map +1 -0
  100. package/dist/core/indexing/virtualviews/ViewEntrySortKey.js +107 -0
  101. package/dist/core/indexing/virtualviews/ViewEntrySortKey.js.map +1 -0
  102. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +28 -0
  103. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -0
  104. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +133 -0
  105. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -0
  106. package/dist/core/indexing/virtualviews/VirtualView.d.ts +109 -0
  107. package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -0
  108. package/dist/core/indexing/virtualviews/VirtualView.js +504 -0
  109. package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -0
  110. package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts +59 -0
  111. package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts.map +1 -0
  112. package/dist/core/indexing/virtualviews/VirtualViewColumn.js +70 -0
  113. package/dist/core/indexing/virtualviews/VirtualViewColumn.js.map +1 -0
  114. package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts +49 -0
  115. package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts.map +1 -0
  116. package/dist/core/indexing/virtualviews/VirtualViewDataChange.js +69 -0
  117. package/dist/core/indexing/virtualviews/VirtualViewDataChange.js.map +1 -0
  118. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +121 -0
  119. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -0
  120. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +365 -0
  121. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -0
  122. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +131 -0
  123. package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -0
  124. package/dist/core/indexing/virtualviews/VirtualViewFactory.js +236 -0
  125. package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -0
  126. package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts +330 -0
  127. package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts.map +1 -0
  128. package/dist/core/indexing/virtualviews/VirtualViewNavigator.js +1010 -0
  129. package/dist/core/indexing/virtualviews/VirtualViewNavigator.js.map +1 -0
  130. package/dist/core/indexing/virtualviews/index.d.ts +53 -0
  131. package/dist/core/indexing/virtualviews/index.d.ts.map +1 -0
  132. package/dist/core/indexing/virtualviews/index.js +89 -0
  133. package/dist/core/indexing/virtualviews/index.js.map +1 -0
  134. package/dist/core/indexing/virtualviews/types.d.ts +115 -0
  135. package/dist/core/indexing/virtualviews/types.d.ts.map +1 -0
  136. package/dist/core/indexing/virtualviews/types.js +175 -0
  137. package/dist/core/indexing/virtualviews/types.js.map +1 -0
  138. package/dist/core/keys/KeyBag.d.ts +122 -0
  139. package/dist/core/keys/KeyBag.d.ts.map +1 -0
  140. package/dist/core/keys/KeyBag.js +401 -0
  141. package/dist/core/keys/KeyBag.js.map +1 -0
  142. package/dist/core/types.d.ts +1004 -0
  143. package/dist/core/types.d.ts.map +1 -0
  144. package/dist/core/types.js +10 -0
  145. package/dist/core/types.js.map +1 -0
  146. package/dist/core/userid/index.d.ts +53 -0
  147. package/dist/core/userid/index.d.ts.map +1 -0
  148. package/dist/core/userid/index.js +3 -0
  149. package/dist/core/userid/index.js.map +1 -0
  150. package/dist/core/utils/idGeneration.d.ts +109 -0
  151. package/dist/core/utils/idGeneration.d.ts.map +1 -0
  152. package/dist/core/utils/idGeneration.js +182 -0
  153. package/dist/core/utils/idGeneration.js.map +1 -0
  154. package/dist/core/utils/index.d.ts +5 -0
  155. package/dist/core/utils/index.d.ts.map +1 -0
  156. package/dist/core/utils/index.js +19 -0
  157. package/dist/core/utils/index.js.map +1 -0
  158. package/dist/index.d.ts +7 -0
  159. package/dist/index.d.ts.map +1 -0
  160. package/dist/index.js +40 -0
  161. package/dist/index.js.map +1 -0
  162. package/dist/node/crypto/NodeCryptoAdapter.d.ts +16 -0
  163. package/dist/node/crypto/NodeCryptoAdapter.d.ts.map +1 -0
  164. package/dist/node/crypto/NodeCryptoAdapter.js +53 -0
  165. package/dist/node/crypto/NodeCryptoAdapter.js.map +1 -0
  166. package/dist/node/index.d.ts +3 -0
  167. package/dist/node/index.d.ts.map +1 -0
  168. package/dist/node/index.js +26 -0
  169. package/dist/node/index.js.map +1 -0
  170. package/package.json +58 -0
@@ -0,0 +1,1701 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BaseMindooDB = void 0;
37
+ const Automerge = __importStar(require("@automerge/automerge"));
38
+ const uuid_1 = require("uuid");
39
+ const idGeneration_1 = require("./utils/idGeneration");
40
+ const errors_1 = require("./errors");
41
+ /**
42
+ * Default chunk size for attachments: 256KB
43
+ */
44
+ const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
45
+ /**
46
+ * BaseMindooDB is a platform-agnostic implementation of MindooDB
47
+ * that works in both browser and server environments.
48
+ *
49
+ * It receives MindooTenant and ContentAddressedStore in the constructor,
50
+ * allowing platform-specific implementations of those interfaces.
51
+ *
52
+ * Dependencies:
53
+ * - @automerge/automerge: For CRDT document management
54
+ * - Web Crypto API: Available in both browser (window.crypto) and Node.js (crypto)
55
+ * - Ed25519 signing/verification (Node.js 15+, Chrome 92+)
56
+ * - AES-256-GCM encryption (widely supported)
57
+ *
58
+ * TODO: Verify Automerge 2.x API methods:
59
+ * - Automerge.getChangeHash(change) - verify method name
60
+ * - Automerge.getHeads(doc) - verify method name
61
+ * - Automerge.load(bytes) - verify method name for snapshots
62
+ * - Automerge.applyChanges(doc, changes) - verify method signature
63
+ */
64
+ class BaseMindooDB {
65
+ constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false) {
66
+ // Internal index: sorted array of document entries, maintained in order by (lastModified, docId)
67
+ // This allows efficient incremental processing without sorting on each call
68
+ this.index = [];
69
+ // Lookup map for O(1) access to index entries by docId
70
+ this.indexLookup = new Map(); // Map<docId, arrayIndex>
71
+ // Cache of loaded documents: Map<docId, InternalDoc>
72
+ this.docCache = new Map();
73
+ // Track which entry IDs we've already processed
74
+ this.processedEntryIds = [];
75
+ // Index: automergeHash -> entryId for each document
76
+ // Used for resolving Automerge dependency hashes to entry IDs
77
+ this.automergeHashToEntryId = new Map(); // Map<docId, Map<automergeHash, entryId>>
78
+ this.tenant = tenant;
79
+ this.store = store;
80
+ this.attachmentStore = attachmentStore;
81
+ this.chunkSizeBytes = attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
82
+ this._isAdminOnlyDb = adminOnlyDb;
83
+ }
84
+ /**
85
+ * Get the admin public key from the tenant.
86
+ * Only used when adminOnlyDb is true.
87
+ */
88
+ getAdminPublicKey() {
89
+ return this.tenant.getAdministrationPublicKey();
90
+ }
91
+ isAdminOnlyDb() {
92
+ return this._isAdminOnlyDb;
93
+ }
94
+ /**
95
+ * Compare two index entries for sorting.
96
+ * Returns negative if a < b, positive if a > b, 0 if equal.
97
+ * Sorts by lastModified first, then by docId for uniqueness.
98
+ */
99
+ compareIndexEntries(a, b) {
100
+ if (a.lastModified !== b.lastModified) {
101
+ return a.lastModified - b.lastModified;
102
+ }
103
+ return a.docId.localeCompare(b.docId);
104
+ }
105
+ /**
106
+ * Update the index entry for a document.
107
+ * When a document changes, it's removed from its current position and inserted
108
+ * at the correct sorted position to maintain order by (lastModified, docId).
109
+ *
110
+ * @param docId The document ID
111
+ * @param lastModified The new last modified timestamp
112
+ * @param isDeleted Whether the document is deleted
113
+ */
114
+ updateIndex(docId, lastModified, isDeleted) {
115
+ // Remove existing entry if present
116
+ const existingIndex = this.indexLookup.get(docId);
117
+ if (existingIndex !== undefined) {
118
+ // Remove from array
119
+ 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++) {
122
+ this.indexLookup.set(this.index[i].docId, i);
123
+ }
124
+ // Remove the entry from lookup
125
+ this.indexLookup.delete(docId);
126
+ }
127
+ // Find insertion point using binary search to maintain sorted order
128
+ const newEntry = { docId, lastModified, isDeleted };
129
+ let insertIndex = this.index.length; // Default to end
130
+ // Binary search for insertion point
131
+ let left = 0;
132
+ let right = this.index.length - 1;
133
+ while (left <= right) {
134
+ const mid = Math.floor((left + right) / 2);
135
+ const cmp = this.compareIndexEntries(newEntry, this.index[mid]);
136
+ if (cmp < 0) {
137
+ right = mid - 1;
138
+ insertIndex = mid;
139
+ }
140
+ else {
141
+ left = mid + 1;
142
+ insertIndex = mid + 1;
143
+ }
144
+ }
145
+ // Insert at the correct position
146
+ this.index.splice(insertIndex, 0, newEntry);
147
+ // Update lookup map for all entries from insertion point onwards
148
+ for (let i = insertIndex; i < this.index.length; i++) {
149
+ this.indexLookup.set(this.index[i].docId, i);
150
+ }
151
+ }
152
+ /**
153
+ * Register an automerge hash -> entry ID mapping for a document.
154
+ * This is used to resolve Automerge dependency hashes to entry IDs.
155
+ */
156
+ registerAutomergeHashMapping(docId, automergeHash, entryId) {
157
+ if (!this.automergeHashToEntryId.has(docId)) {
158
+ this.automergeHashToEntryId.set(docId, new Map());
159
+ }
160
+ this.automergeHashToEntryId.get(docId).set(automergeHash, entryId);
161
+ }
162
+ /**
163
+ * Get the entry ID for an automerge hash within a document.
164
+ * Returns null if not found.
165
+ */
166
+ getEntryIdForAutomergeHash(docId, automergeHash) {
167
+ const docIndex = this.automergeHashToEntryId.get(docId);
168
+ if (docIndex) {
169
+ return docIndex.get(automergeHash) || null;
170
+ }
171
+ return null;
172
+ }
173
+ /**
174
+ * Resolve Automerge dependency hashes to entry IDs for a document.
175
+ * Returns the entry IDs for the given automerge hashes.
176
+ */
177
+ resolveAutomergeDepsToEntryIds(docId, automergeHashes) {
178
+ const entryIds = [];
179
+ for (const hash of automergeHashes) {
180
+ const entryId = this.getEntryIdForAutomergeHash(docId, hash);
181
+ if (entryId) {
182
+ entryIds.push(entryId);
183
+ }
184
+ else {
185
+ console.warn(`[BaseMindooDB] Could not resolve automerge hash ${hash} to entry ID for doc ${docId}`);
186
+ }
187
+ }
188
+ return entryIds;
189
+ }
190
+ /**
191
+ * Get the SubtleCrypto instance from the tenant's crypto adapter.
192
+ */
193
+ getSubtle() {
194
+ return this.tenant.getCryptoAdapter().getSubtle();
195
+ }
196
+ /**
197
+ * Initialize the database instance.
198
+ */
199
+ async initialize() {
200
+ console.log(`[BaseMindooDB] Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
201
+ await this.syncStoreChanges();
202
+ }
203
+ /**
204
+ * Sync changes from the content-addressed store by finding new entries and processing them.
205
+ * This method can be called multiple times to incrementally sync new entries.
206
+ * On first call (when processedEntryIds is empty), it will process all entries.
207
+ */
208
+ 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`);
211
+ // 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`);
214
+ if (newEntryMetadata.length === 0) {
215
+ console.log(`[BaseMindooDB] No new entries to process`);
216
+ return;
217
+ }
218
+ // Group new entries by document ID
219
+ const entriesByDoc = new Map();
220
+ for (const entryMeta of newEntryMetadata) {
221
+ if (!entriesByDoc.has(entryMeta.docId)) {
222
+ entriesByDoc.set(entryMeta.docId, []);
223
+ }
224
+ entriesByDoc.get(entryMeta.docId).push(entryMeta);
225
+ }
226
+ // 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) {
230
+ 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})`);
243
+ }
244
+ else {
245
+ console.warn(`[BaseMindooDB] Document ${docId} returned null from loadDocumentInternal`);
246
+ }
247
+ }
248
+ catch (error) {
249
+ // Handle missing symmetric key gracefully - skip documents we can't decrypt
250
+ if (error instanceof errors_1.SymmetricKeyNotFoundError) {
251
+ console.log(`[BaseMindooDB] Skipping document ${docId} - missing key: ${error.keyId}`);
252
+ // Mark entry IDs as processed so we don't retry them
253
+ 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}`);
261
+ }
262
+ // 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
+ throw error;
265
+ }
266
+ }
267
+ // Append new entry IDs to our processed list
268
+ this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
269
+ console.log(`[BaseMindooDB] Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
270
+ }
271
+ getStore() {
272
+ return this.store;
273
+ }
274
+ getAttachmentStore() {
275
+ return this.attachmentStore;
276
+ }
277
+ getTenant() {
278
+ return this.tenant;
279
+ }
280
+ async createDocument() {
281
+ return this.createEncryptedDocument("default");
282
+ }
283
+ async createEncryptedDocument(decryptionKeyId) {
284
+ return this.createDocumentInternal(decryptionKeyId || "default");
285
+ }
286
+ async createDocumentWithSigningKey(signingKeyPair, signingKeyPassword, decryptionKeyId) {
287
+ return this.createDocumentInternal(decryptionKeyId || "default", signingKeyPair, signingKeyPassword);
288
+ }
289
+ /**
290
+ * Internal method to create a new document.
291
+ * Handles both regular document creation (signed by current user) and
292
+ * document creation with a custom signing key (e.g., for directory operations).
293
+ */
294
+ async createDocumentInternal(decryptionKeyId, signingKeyPair, signingKeyPassword) {
295
+ const keyId = decryptionKeyId;
296
+ const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
297
+ // Admin-only validation: only admin key can modify data in admin-only databases
298
+ if (this._isAdminOnlyDb) {
299
+ const adminPublicKey = this.getAdminPublicKey();
300
+ const signerPublicKey = useCustomSigningKey
301
+ ? signingKeyPair.publicKey
302
+ : (await this.tenant.getCurrentUserId()).userSigningPublicKey;
303
+ if (signerPublicKey !== adminPublicKey) {
304
+ throw new Error("Admin-only database: only the admin key can modify data");
305
+ }
306
+ }
307
+ // Generate UUID7 for document ID
308
+ const docId = (0, uuid_1.v7)();
309
+ console.log(`[BaseMindooDB] Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
310
+ // Create initial Automerge document
311
+ const initialDoc = Automerge.init();
312
+ // Create the first change
313
+ const now = Date.now();
314
+ console.log(`[BaseMindooDB] Creating initial Automerge change for document ${docId}`);
315
+ let newDoc;
316
+ try {
317
+ newDoc = Automerge.change(initialDoc, (doc) => {
318
+ // Store metadata in the document payload
319
+ // We need to modify the document to ensure a change is created
320
+ doc._attachments = [];
321
+ });
322
+ console.log(`[BaseMindooDB] Successfully created Automerge change, document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
323
+ }
324
+ catch (error) {
325
+ console.error(`[BaseMindooDB] Error in Automerge.change for document ${docId}:`, error);
326
+ throw error;
327
+ }
328
+ // Get the change bytes from the document
329
+ console.log(`[BaseMindooDB] Getting change bytes from document ${docId}`);
330
+ const changeBytes = Automerge.getLastLocalChange(newDoc);
331
+ if (!changeBytes) {
332
+ throw new Error("Failed to get change bytes from Automerge document");
333
+ }
334
+ console.log(`[BaseMindooDB] Got change bytes: ${changeBytes.length} bytes`);
335
+ // Decode the change to get hash and dependencies
336
+ console.log(`[BaseMindooDB] Decoding change to get hash and dependencies`);
337
+ let decodedChange;
338
+ try {
339
+ decodedChange = Automerge.decodeChange(changeBytes);
340
+ console.log(`[BaseMindooDB] Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
341
+ }
342
+ catch (error) {
343
+ console.error(`[BaseMindooDB] Error decoding change for document ${docId}:`, error);
344
+ throw error;
345
+ }
346
+ const automergeHash = decodedChange.hash;
347
+ const automergeDepHashes = decodedChange.deps || []; // First change has no dependencies
348
+ // Encrypt the change payload first
349
+ console.log(`[BaseMindooDB] Encrypting change payload for document ${docId}`);
350
+ const encryptedPayload = await this.tenant.encryptPayload(changeBytes, keyId);
351
+ console.log(`[BaseMindooDB] Encrypted payload: ${changeBytes.length} -> ${encryptedPayload.length} bytes`);
352
+ // Compute content hash from encrypted data
353
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
354
+ console.log(`[BaseMindooDB] Computed content hash: ${contentHash.substring(0, 16)}...`);
355
+ // Generate entry ID with blockchain-like chaining
356
+ const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
357
+ console.log(`[BaseMindooDB] Generated entry ID: ${entryId}`);
358
+ // Resolve Automerge dependency hashes to entry IDs (empty for first change)
359
+ const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
360
+ // Sign the encrypted payload - either with custom key or current user's key
361
+ let signature;
362
+ let createdByPublicKey;
363
+ if (useCustomSigningKey) {
364
+ console.log(`[BaseMindooDB] Signing encrypted payload for document ${docId} with provided key`);
365
+ signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
366
+ createdByPublicKey = signingKeyPair.publicKey;
367
+ }
368
+ else {
369
+ console.log(`[BaseMindooDB] Signing encrypted payload for document ${docId}`);
370
+ const currentUser = await this.tenant.getCurrentUserId();
371
+ signature = await this.tenant.signPayload(encryptedPayload);
372
+ createdByPublicKey = currentUser.userSigningPublicKey;
373
+ }
374
+ console.log(`[BaseMindooDB] Signed payload, signature length: ${signature.length} bytes`);
375
+ // Create entry metadata
376
+ const entryMetadata = {
377
+ entryType: "doc_create",
378
+ id: entryId,
379
+ contentHash,
380
+ docId,
381
+ dependencyIds,
382
+ createdAt: now,
383
+ createdByPublicKey,
384
+ decryptionKeyId: keyId,
385
+ signature,
386
+ originalSize: changeBytes.length,
387
+ encryptedSize: encryptedPayload.length,
388
+ };
389
+ // Create full entry object
390
+ const fullEntry = {
391
+ ...entryMetadata,
392
+ encryptedData: encryptedPayload,
393
+ };
394
+ // Store entry
395
+ await this.store.putEntries([fullEntry]);
396
+ // Register automerge hash -> entry ID mapping
397
+ this.registerAutomergeHashMapping(docId, automergeHash, entryId);
398
+ // Create internal document representation
399
+ const internalDoc = {
400
+ id: docId,
401
+ doc: newDoc,
402
+ createdAt: now,
403
+ lastModified: now,
404
+ decryptionKeyId: keyId,
405
+ isDeleted: false,
406
+ };
407
+ // Update cache and index
408
+ this.docCache.set(docId, internalDoc);
409
+ 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})`);
412
+ return this.wrapDocument(internalDoc);
413
+ }
414
+ async getDocument(docId) {
415
+ const internalDoc = await this.loadDocumentInternal(docId);
416
+ if (!internalDoc) {
417
+ throw new Error(`Document ${docId} not found`);
418
+ }
419
+ if (internalDoc.isDeleted) {
420
+ throw new Error(`Document ${docId} has been deleted`);
421
+ }
422
+ return this.wrapDocument(internalDoc);
423
+ }
424
+ async getDocumentAtTimestamp(docId, timestamp) {
425
+ console.log(`[BaseMindooDB] Getting document ${docId} at timestamp ${timestamp}`);
426
+ // Get all entry metadata for this document
427
+ const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
428
+ // Filter entries up to the timestamp
429
+ const relevantEntries = allEntryMetadata
430
+ .filter(em => em.createdAt <= timestamp)
431
+ .sort((a, b) => a.createdAt - b.createdAt);
432
+ if (relevantEntries.length === 0) {
433
+ return null; // Document didn't exist at that time
434
+ }
435
+ // Load entries
436
+ const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
437
+ // Apply changes in order
438
+ let doc = Automerge.init();
439
+ for (const entryData of entries) {
440
+ // Admin-only mode: only accept entries signed by the admin key
441
+ if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
442
+ console.warn(`[BaseMindooDB] Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
443
+ continue;
444
+ }
445
+ // Verify signature against the encrypted payload (no decryption needed)
446
+ // We sign the encrypted payload, so anyone can verify signatures without decryption keys
447
+ const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
448
+ if (!isValid) {
449
+ console.warn(`[BaseMindooDB] Invalid signature for entry ${entryData.id}, skipping`);
450
+ continue;
451
+ }
452
+ // Decrypt payload (only after signature verification passes)
453
+ const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
454
+ // Apply change using loadIncremental - this is the recommended way for binary change data
455
+ doc = Automerge.loadIncremental(doc, decryptedPayload);
456
+ // Register the automerge hash -> entry ID mapping for future dependency resolution
457
+ const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
458
+ if (parsed) {
459
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
460
+ }
461
+ }
462
+ // Check if document was deleted at this timestamp
463
+ // 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
+ }
468
+ // Find the first entry to get createdAt and decryptionKeyId
469
+ const firstEntry = relevantEntries.length > 0 ? relevantEntries[0] : null;
470
+ const createdAt = firstEntry ? firstEntry.createdAt : timestamp;
471
+ const decryptionKeyId = firstEntry ? firstEntry.decryptionKeyId : "default";
472
+ const internalDoc = {
473
+ id: docId,
474
+ doc,
475
+ createdAt,
476
+ lastModified: timestamp,
477
+ decryptionKeyId,
478
+ isDeleted: false,
479
+ };
480
+ return this.wrapDocument(internalDoc);
481
+ }
482
+ async getAllDocumentIds() {
483
+ // Return all non-deleted document IDs from index
484
+ const docIds = [];
485
+ for (const entry of this.index) {
486
+ if (!entry.isDeleted) {
487
+ docIds.push(entry.docId);
488
+ }
489
+ }
490
+ return docIds;
491
+ }
492
+ async deleteDocument(docId) {
493
+ return this.deleteDocInternal(docId);
494
+ }
495
+ async deleteDocumentWithSigningKey(docId, signingKeyPair, signingKeyPassword) {
496
+ return this.deleteDocInternal(docId, signingKeyPair, signingKeyPassword);
497
+ }
498
+ /**
499
+ * Internal method to delete a document.
500
+ * Handles both regular deletion (signed by current user) and
501
+ * deletion with a custom signing key (e.g., for directory operations).
502
+ */
503
+ async deleteDocInternal(docId, signingKeyPair, signingKeyPassword) {
504
+ const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
505
+ console.log(`[BaseMindooDB] Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
506
+ // Admin-only validation: only admin key can modify data in admin-only databases
507
+ if (this._isAdminOnlyDb) {
508
+ const adminPublicKey = this.getAdminPublicKey();
509
+ const signerPublicKey = useCustomSigningKey
510
+ ? signingKeyPair.publicKey
511
+ : (await this.tenant.getCurrentUserId()).userSigningPublicKey;
512
+ if (signerPublicKey !== adminPublicKey) {
513
+ throw new Error("Admin-only database: only the admin key can modify data");
514
+ }
515
+ }
516
+ // Get current document
517
+ const internalDoc = await this.loadDocumentInternal(docId);
518
+ if (!internalDoc || internalDoc.isDeleted) {
519
+ throw new Error(`Document ${docId} not found or already deleted`);
520
+ }
521
+ // Create deletion change by clearing all fields from the document
522
+ // This ensures Automerge produces actual change bytes
523
+ // The deletion is also tracked via the "doc_delete" type entry in the append-only store
524
+ const newDoc = Automerge.change(internalDoc.doc, (doc) => {
525
+ // Remove all fields from the document
526
+ for (const key of Object.keys(doc)) {
527
+ delete doc[key];
528
+ }
529
+ // Mark as deleted for clarity
530
+ doc._deleted = true;
531
+ });
532
+ // Get the change bytes from the document
533
+ const changeBytes = Automerge.getLastLocalChange(newDoc);
534
+ if (!changeBytes) {
535
+ throw new Error("Failed to get change bytes from Automerge document");
536
+ }
537
+ // Decode the change to get hash and dependencies
538
+ const decodedChange = Automerge.decodeChange(changeBytes);
539
+ const automergeHash = decodedChange.hash;
540
+ const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
541
+ // Encrypt the change payload first
542
+ const encryptedPayload = await this.tenant.encryptPayload(changeBytes, internalDoc.decryptionKeyId);
543
+ // Compute content hash from encrypted data
544
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
545
+ // Generate entry ID with blockchain-like chaining
546
+ const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
547
+ // Resolve Automerge dependency hashes to entry IDs
548
+ const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
549
+ // Sign the encrypted payload - either with custom key or current user's key
550
+ let signature;
551
+ let createdByPublicKey;
552
+ if (useCustomSigningKey) {
553
+ console.log(`[BaseMindooDB] Signing deletion for document ${docId} with provided key`);
554
+ signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
555
+ createdByPublicKey = signingKeyPair.publicKey;
556
+ }
557
+ else {
558
+ console.log(`[BaseMindooDB] Signing deletion for document ${docId} with current user's key`);
559
+ const currentUser = await this.tenant.getCurrentUserId();
560
+ signature = await this.tenant.signPayload(encryptedPayload);
561
+ createdByPublicKey = currentUser.userSigningPublicKey;
562
+ }
563
+ // Create entry metadata with type "doc_delete" to mark this as a deletion entry in the store
564
+ const entryMetadata = {
565
+ entryType: "doc_delete",
566
+ id: entryId,
567
+ contentHash,
568
+ docId,
569
+ dependencyIds,
570
+ createdAt: Date.now(),
571
+ createdByPublicKey,
572
+ decryptionKeyId: internalDoc.decryptionKeyId,
573
+ signature,
574
+ originalSize: changeBytes.length,
575
+ encryptedSize: encryptedPayload.length,
576
+ };
577
+ // Create full entry object
578
+ const fullEntry = {
579
+ ...entryMetadata,
580
+ encryptedData: encryptedPayload,
581
+ };
582
+ // Store entry
583
+ await this.store.putEntries([fullEntry]);
584
+ // Register automerge hash -> entry ID mapping
585
+ this.registerAutomergeHashMapping(docId, automergeHash, entryId);
586
+ // Update cache and index
587
+ internalDoc.isDeleted = true;
588
+ internalDoc.lastModified = Date.now();
589
+ this.docCache.set(docId, internalDoc);
590
+ this.updateIndex(docId, internalDoc.lastModified, true);
591
+ console.log(`[BaseMindooDB] Document ${docId} deleted successfully`);
592
+ }
593
+ async changeDoc(doc, changeFunc) {
594
+ return this.changeDocInternal(doc, changeFunc);
595
+ }
596
+ async changeDocWithSigningKey(doc, changeFunc, signingKeyPair, signingKeyPassword) {
597
+ return this.changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword);
598
+ }
599
+ async changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword) {
600
+ const docId = doc.getId();
601
+ const useCustomKey = signingKeyPair && signingKeyPassword;
602
+ console.log(`[BaseMindooDB] ===== ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'} called for document ${docId} =====`);
603
+ // Admin-only validation: only admin key can modify data in admin-only databases
604
+ if (this._isAdminOnlyDb) {
605
+ const adminPublicKey = this.getAdminPublicKey();
606
+ const signerPublicKey = useCustomKey
607
+ ? signingKeyPair.publicKey
608
+ : (await this.tenant.getCurrentUserId()).userSigningPublicKey;
609
+ if (signerPublicKey !== adminPublicKey) {
610
+ throw new Error("Admin-only database: only the admin key can modify data");
611
+ }
612
+ }
613
+ // Get internal document from cache or load it
614
+ let internalDoc = this.docCache.get(docId);
615
+ if (!internalDoc) {
616
+ console.log(`[BaseMindooDB] Document ${docId} not in cache, loading from store`);
617
+ const loadedDoc = await this.loadDocumentInternal(docId);
618
+ if (!loadedDoc) {
619
+ throw new Error(`Document ${docId} not found`);
620
+ }
621
+ internalDoc = loadedDoc;
622
+ console.log(`[BaseMindooDB] Successfully loaded document ${docId} from store for ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'}`);
623
+ }
624
+ else {
625
+ console.log(`[BaseMindooDB] Document ${docId} found in cache`);
626
+ }
627
+ if (internalDoc.isDeleted) {
628
+ throw new Error(`Document ${docId} has been deleted`);
629
+ }
630
+ // Apply the change function
631
+ 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))}`);
634
+ // For async callbacks, we need to handle document modifications carefully.
635
+ // Automerge.change() requires synchronous modifications, so we'll:
636
+ // 1. Execute the async callback to do any async work (like signing)
637
+ // 2. Apply document modifications synchronously within Automerge.change()
638
+ //
639
+ // We use a two-phase approach: the callback can do async work and collect
640
+ // what needs to be changed, then we apply those changes in Automerge.change()
641
+ const pendingChanges = new Map();
642
+ const pendingDeletions = new Set();
643
+ // Track pending attachment operations
644
+ const pendingAttachmentAdditions = [];
645
+ const pendingAttachmentRemovals = new Set();
646
+ // Map of attachmentId -> {lastChunkId, sizeIncrease} for appends
647
+ const pendingAttachmentAppends = new Map();
648
+ // Reference to db for closures
649
+ const db = this;
650
+ // Guard flag to prevent operations after callback completes
651
+ // This ensures changes can only be made during the callback execution
652
+ let isCallbackActive = true;
653
+ const throwIfCallbackInactive = (methodName) => {
654
+ if (!isCallbackActive) {
655
+ throw new Error(`${methodName}() cannot be called after changeDoc() callback has completed. Document changes can only be made within the callback.`);
656
+ }
657
+ };
658
+ // Create a document wrapper that collects changes
659
+ const collectingDoc = {
660
+ getDatabase: () => this,
661
+ getId: () => docId,
662
+ getCreatedAt: () => internalDoc.createdAt,
663
+ getLastModified: () => internalDoc.lastModified,
664
+ isDeleted: () => false,
665
+ getData: () => {
666
+ // Return a proxy that collects property assignments and deletions
667
+ const currentData = internalDoc.doc;
668
+ return new Proxy(currentData, {
669
+ set: (target, prop, value) => {
670
+ throwIfCallbackInactive('set property');
671
+ if (typeof prop === 'string') {
672
+ // If this property was marked for deletion, remove it from deletions
673
+ pendingDeletions.delete(prop);
674
+ // Track the change
675
+ pendingChanges.set(prop, value);
676
+ // Also set on the target for immediate access
677
+ target[prop] = value;
678
+ }
679
+ return true;
680
+ },
681
+ deleteProperty: (target, prop) => {
682
+ throwIfCallbackInactive('delete property');
683
+ if (typeof prop === 'string') {
684
+ // Mark for deletion
685
+ pendingDeletions.add(prop);
686
+ // Remove from pending changes if it was there
687
+ pendingChanges.delete(prop);
688
+ // Also delete from the target for immediate access
689
+ delete target[prop];
690
+ }
691
+ return true;
692
+ },
693
+ get: (target, prop) => {
694
+ // If marked for deletion, return undefined
695
+ if (typeof prop === 'string' && pendingDeletions.has(prop)) {
696
+ return undefined;
697
+ }
698
+ // Check pending changes first, then target
699
+ if (typeof prop === 'string' && pendingChanges.has(prop)) {
700
+ return pendingChanges.get(prop);
701
+ }
702
+ return target[prop];
703
+ },
704
+ has: (target, prop) => {
705
+ // If marked for deletion, it doesn't exist
706
+ if (typeof prop === 'string' && pendingDeletions.has(prop)) {
707
+ return false;
708
+ }
709
+ // Check pending changes first, then target
710
+ if (typeof prop === 'string' && pendingChanges.has(prop)) {
711
+ return true;
712
+ }
713
+ return prop in target;
714
+ }
715
+ });
716
+ },
717
+ // ========== Attachment Write Methods (work in changeDoc context) ==========
718
+ addAttachment: async (fileData, fileName, mimeType, keyId) => {
719
+ throwIfCallbackInactive('addAttachment');
720
+ const decryptionKeyId = keyId || internalDoc.decryptionKeyId;
721
+ const ref = await db.addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, now);
722
+ pendingAttachmentAdditions.push(ref);
723
+ return ref;
724
+ },
725
+ addAttachmentStream: async (dataStream, fileName, mimeType, keyId) => {
726
+ throwIfCallbackInactive('addAttachmentStream');
727
+ const decryptionKeyId = keyId || internalDoc.decryptionKeyId;
728
+ const ref = await db.addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, now);
729
+ pendingAttachmentAdditions.push(ref);
730
+ return ref;
731
+ },
732
+ removeAttachment: async (attachmentId) => {
733
+ throwIfCallbackInactive('removeAttachment');
734
+ // Check if attachment exists (either in current doc or pending additions)
735
+ const payload = internalDoc.doc;
736
+ const existingAttachments = payload._attachments || [];
737
+ const existsInDoc = existingAttachments.some(a => a.attachmentId === attachmentId);
738
+ const existsInPending = pendingAttachmentAdditions.some(a => a.attachmentId === attachmentId);
739
+ if (!existsInDoc && !existsInPending) {
740
+ throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
741
+ }
742
+ // If it was added in this same changeDoc call, just remove from pending
743
+ const pendingIndex = pendingAttachmentAdditions.findIndex(a => a.attachmentId === attachmentId);
744
+ if (pendingIndex >= 0) {
745
+ pendingAttachmentAdditions.splice(pendingIndex, 1);
746
+ }
747
+ else {
748
+ // Mark for removal from existing attachments
749
+ pendingAttachmentRemovals.add(attachmentId);
750
+ }
751
+ },
752
+ appendToAttachment: async (attachmentId, data) => {
753
+ throwIfCallbackInactive('appendToAttachment');
754
+ // Find the attachment reference
755
+ const payload = internalDoc.doc;
756
+ const existingAttachments = payload._attachments || [];
757
+ let ref = existingAttachments.find(a => a.attachmentId === attachmentId);
758
+ // Also check pending additions
759
+ if (!ref) {
760
+ ref = pendingAttachmentAdditions.find(a => a.attachmentId === attachmentId);
761
+ }
762
+ if (!ref) {
763
+ throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
764
+ }
765
+ // Determine the previous lastChunkId (might have been updated by previous append in this changeDoc)
766
+ let prevLastChunkId = ref.lastChunkId;
767
+ const prevAppend = pendingAttachmentAppends.get(attachmentId);
768
+ if (prevAppend) {
769
+ prevLastChunkId = prevAppend.lastChunkId;
770
+ }
771
+ // Append the data by creating new chunks
772
+ const { lastChunkId, sizeIncrease } = await db.appendToAttachmentInternal(docId, attachmentId, ref.decryptionKeyId, prevLastChunkId, data, now);
773
+ // Track the append
774
+ const existingAppend = pendingAttachmentAppends.get(attachmentId);
775
+ if (existingAppend) {
776
+ existingAppend.lastChunkId = lastChunkId;
777
+ existingAppend.sizeIncrease += sizeIncrease;
778
+ }
779
+ else {
780
+ pendingAttachmentAppends.set(attachmentId, { lastChunkId, sizeIncrease });
781
+ }
782
+ },
783
+ // ========== Attachment Read Methods (also work in changeDoc context) ==========
784
+ getAttachments: () => {
785
+ const payload = internalDoc.doc;
786
+ const existing = payload._attachments || [];
787
+ // Filter out removals and add pending additions
788
+ const filtered = existing.filter(a => !pendingAttachmentRemovals.has(a.attachmentId));
789
+ return [...filtered, ...pendingAttachmentAdditions];
790
+ },
791
+ getAttachment: async (attachmentId) => {
792
+ return db.getAttachmentInternal(docId, attachmentId);
793
+ },
794
+ getAttachmentRange: async (attachmentId, startByte, endByte) => {
795
+ return db.getAttachmentRangeInternal(docId, attachmentId, startByte, endByte);
796
+ },
797
+ streamAttachment: (attachmentId, startOffset = 0) => {
798
+ return db.streamAttachmentInternal(docId, attachmentId, startOffset);
799
+ },
800
+ };
801
+ // Execute the async callback (this may do async operations like signing)
802
+ await changeFunc(collectingDoc);
803
+ // Deactivate the callback guard - no more changes can be made via collectingDoc
804
+ isCallbackActive = false;
805
+ // Now apply the collected changes synchronously in Automerge.change()
806
+ let newDoc;
807
+ try {
808
+ newDoc = Automerge.change(internalDoc.doc, (automergeDoc) => {
809
+ // Apply all pending changes (sets/updates)
810
+ for (const [key, value] of pendingChanges) {
811
+ automergeDoc[key] = value;
812
+ }
813
+ // Apply all pending deletions
814
+ for (const key of pendingDeletions) {
815
+ delete automergeDoc[key];
816
+ }
817
+ // Apply pending attachment changes
818
+ if (pendingAttachmentAdditions.length > 0 || pendingAttachmentRemovals.size > 0 || pendingAttachmentAppends.size > 0) {
819
+ // Initialize _attachments array if needed
820
+ if (!automergeDoc._attachments) {
821
+ automergeDoc._attachments = [];
822
+ }
823
+ const attachments = automergeDoc._attachments;
824
+ // Remove attachments marked for removal
825
+ for (const attachmentId of pendingAttachmentRemovals) {
826
+ const index = attachments.findIndex(a => a.attachmentId === attachmentId);
827
+ if (index >= 0) {
828
+ attachments.splice(index, 1);
829
+ }
830
+ }
831
+ // Apply appends (update lastChunkId and size)
832
+ for (const [attachmentId, { lastChunkId, sizeIncrease }] of pendingAttachmentAppends) {
833
+ const attachment = attachments.find(a => a.attachmentId === attachmentId);
834
+ if (attachment) {
835
+ attachment.lastChunkId = lastChunkId;
836
+ attachment.size += sizeIncrease;
837
+ }
838
+ }
839
+ // Add new attachments
840
+ for (const ref of pendingAttachmentAdditions) {
841
+ attachments.push(ref);
842
+ }
843
+ }
844
+ // Update lastModified timestamp
845
+ automergeDoc._lastModified = now;
846
+ });
847
+ console.log(`[BaseMindooDB] Successfully applied change function, new document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
848
+ }
849
+ catch (error) {
850
+ console.error(`[BaseMindooDB] Error in Automerge.change for document ${docId}:`, error);
851
+ throw error;
852
+ }
853
+ // Get the change bytes from the document
854
+ console.log(`[BaseMindooDB] Getting change bytes from document ${docId}`);
855
+ const changeBytes = Automerge.getLastLocalChange(newDoc);
856
+ if (!changeBytes) {
857
+ //TODO decide if we just exit here or throw an error
858
+ throw new Error("Failed to get change bytes from Automerge document");
859
+ }
860
+ console.log(`[BaseMindooDB] Got change bytes: ${changeBytes.length} bytes`);
861
+ // Decode the change to get hash and dependencies
862
+ const decodedChange = Automerge.decodeChange(changeBytes);
863
+ const automergeHash = decodedChange.hash;
864
+ const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
865
+ // Encrypt the change payload first
866
+ const encryptedPayload = await this.tenant.encryptPayload(changeBytes, internalDoc.decryptionKeyId);
867
+ // Compute content hash from encrypted data
868
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
869
+ // Generate entry ID with blockchain-like chaining
870
+ const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
871
+ console.log(`[BaseMindooDB] Generated entry ID for change: ${entryId}`);
872
+ // Resolve Automerge dependency hashes to entry IDs
873
+ const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
874
+ // Sign the encrypted payload - use custom key if provided, otherwise current user's key
875
+ let signature;
876
+ let createdByPublicKey;
877
+ if (useCustomKey) {
878
+ signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
879
+ createdByPublicKey = signingKeyPair.publicKey;
880
+ }
881
+ else {
882
+ const currentUser = await this.tenant.getCurrentUserId();
883
+ signature = await this.tenant.signPayload(encryptedPayload);
884
+ createdByPublicKey = currentUser.userSigningPublicKey;
885
+ }
886
+ // Create entry metadata
887
+ const entryMetadata = {
888
+ entryType: "doc_change",
889
+ id: entryId,
890
+ contentHash,
891
+ docId,
892
+ dependencyIds,
893
+ createdAt: Date.now(),
894
+ createdByPublicKey,
895
+ decryptionKeyId: internalDoc.decryptionKeyId,
896
+ signature,
897
+ originalSize: changeBytes.length,
898
+ encryptedSize: encryptedPayload.length,
899
+ };
900
+ // Create full entry object
901
+ const fullEntry = {
902
+ ...entryMetadata,
903
+ encryptedData: encryptedPayload,
904
+ };
905
+ // Store entry
906
+ await this.store.putEntries([fullEntry]);
907
+ // Register automerge hash -> entry ID mapping
908
+ this.registerAutomergeHashMapping(docId, automergeHash, entryId);
909
+ // Update cache and index
910
+ internalDoc.doc = newDoc;
911
+ internalDoc.lastModified = Date.now();
912
+ this.docCache.set(docId, internalDoc);
913
+ this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted);
914
+ console.log(`[BaseMindooDB] Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
915
+ }
916
+ async processChangesSince(cursor, limit, callback) {
917
+ // Default to initial cursor if null is provided
918
+ const actualCursor = cursor ?? { lastModified: 0, docId: "" };
919
+ console.log(`[BaseMindooDB] Processing changes since cursor ${JSON.stringify(actualCursor)} (limit: ${limit})`);
920
+ // Find starting position using binary search
921
+ // We want to find the first entry that is greater than the cursor
922
+ let startIndex = 0;
923
+ if (this.index.length > 0) {
924
+ let left = 0;
925
+ let right = this.index.length - 1;
926
+ while (left <= right) {
927
+ const mid = Math.floor((left + right) / 2);
928
+ const entry = this.index[mid];
929
+ const cmp = this.compareIndexEntries({ docId: actualCursor.docId, lastModified: actualCursor.lastModified }, entry);
930
+ if (cmp < 0) {
931
+ right = mid - 1;
932
+ startIndex = mid;
933
+ }
934
+ else {
935
+ left = mid + 1;
936
+ startIndex = mid + 1;
937
+ }
938
+ }
939
+ // If we found an exact match, start from the next entry
940
+ if (startIndex < this.index.length) {
941
+ const entry = this.index[startIndex];
942
+ if (entry.lastModified === actualCursor.lastModified && entry.docId === actualCursor.docId) {
943
+ startIndex++;
944
+ }
945
+ }
946
+ }
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++) {
951
+ 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;
955
+ }
956
+ 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}`);
960
+ // Create cursor for current document
961
+ const currentCursor = {
962
+ lastModified: entry.lastModified,
963
+ docId: entry.docId,
964
+ };
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
+ }
974
+ }
975
+ catch (error) {
976
+ console.error(`[BaseMindooDB] Error processing document ${entry.docId}:`, error);
977
+ // Stop processing on error
978
+ throw error;
979
+ }
980
+ }
981
+ console.log(`[BaseMindooDB] Processed ${processedCount} changes, last cursor: ${JSON.stringify(lastCursor)}`);
982
+ return lastCursor;
983
+ }
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;
1001
+ }
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;
1006
+ }
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;
1013
+ }
1014
+ // Continue with the next page using the returned cursor
1015
+ currentCursor = returnedCursor;
1016
+ }
1017
+ console.log(`[BaseMindooDB] Iteration completed`);
1018
+ }
1019
+ /**
1020
+ * Internal method to load a document from the content-addressed store
1021
+ */
1022
+ async loadDocumentInternal(docId) {
1023
+ // Check cache first
1024
+ if (this.docCache.has(docId)) {
1025
+ console.log(`[BaseMindooDB] Document ${docId} found in cache, returning cached version`);
1026
+ return this.docCache.get(docId);
1027
+ }
1028
+ console.log(`[BaseMindooDB] ===== Starting to load document ${docId} from store =====`);
1029
+ // Get all entry metadata for this document
1030
+ // 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}`);
1034
+ if (allEntryMetadata.length === 0) {
1035
+ console.log(`[BaseMindooDB] No entry hashes found for document ${docId}, returning null`);
1036
+ return null;
1037
+ }
1038
+ // Log all entry types
1039
+ const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
1040
+ console.log(`[BaseMindooDB] Entry types for ${docId}: ${entryTypes}`);
1041
+ // Find the most recent snapshot (if any)
1042
+ const snapshots = allEntryMetadata.filter(em => em.entryType === "doc_snapshot");
1043
+ console.log(`[BaseMindooDB] Found ${snapshots.length} snapshot(s) for document ${docId}`);
1044
+ let startFromSnapshot = false;
1045
+ let snapshotMeta = null;
1046
+ if (snapshots.length > 0) {
1047
+ // Use the most recent snapshot
1048
+ snapshots.sort((a, b) => b.createdAt - a.createdAt);
1049
+ snapshotMeta = snapshots[0];
1050
+ startFromSnapshot = true;
1051
+ console.log(`[BaseMindooDB] Will start from snapshot ${snapshotMeta.id} created at ${snapshotMeta.createdAt}`);
1052
+ }
1053
+ else {
1054
+ console.log(`[BaseMindooDB] No snapshot found, will start from scratch`);
1055
+ }
1056
+ // Get all entries (excluding snapshot entries - we'll handle delete separately)
1057
+ // Include "doc_create", "doc_change", and "doc_delete" types as they all contain Automerge changes to apply
1058
+ const entriesToLoad = startFromSnapshot
1059
+ ? allEntryMetadata.filter(em => (em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete") && em.createdAt > snapshotMeta.createdAt)
1060
+ : 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)`);
1062
+ // Load the snapshot first if we have one
1063
+ let doc = undefined;
1064
+ if (startFromSnapshot && snapshotMeta) {
1065
+ console.log(`[BaseMindooDB] Loading snapshot for document ${docId}`);
1066
+ const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
1067
+ console.log(`[BaseMindooDB] Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
1068
+ if (snapshotEntries.length > 0) {
1069
+ const snapshotData = snapshotEntries[0];
1070
+ // Admin-only mode: only accept snapshots signed by admin
1071
+ let isValid = false;
1072
+ if (this._isAdminOnlyDb && snapshotData.createdByPublicKey !== this.getAdminPublicKey()) {
1073
+ console.warn(`[BaseMindooDB] Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
1074
+ }
1075
+ else {
1076
+ // Verify signature against the encrypted snapshot (no decryption needed)
1077
+ // We sign the encrypted payload, so anyone can verify signatures without decryption keys
1078
+ isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
1079
+ }
1080
+ if (!isValid) {
1081
+ console.warn(`[BaseMindooDB] Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
1082
+ // Fall back to loading from scratch
1083
+ startFromSnapshot = false;
1084
+ }
1085
+ else {
1086
+ console.log(`[BaseMindooDB] Snapshot signature valid, decrypting snapshot`);
1087
+ // Decrypt snapshot (only after signature verification passes)
1088
+ const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
1089
+ console.log(`[BaseMindooDB] Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
1090
+ // Load snapshot using Automerge.load()
1091
+ // This deserializes a full document snapshot from binary data
1092
+ // 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))}`);
1096
+ // Register the snapshot's automerge hash -> entry ID mapping
1097
+ const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
1098
+ if (parsed) {
1099
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, snapshotData.id);
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ // If we don't have a snapshot, start from scratch
1105
+ 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))}`);
1109
+ }
1110
+ // Sort entries by timestamp
1111
+ entriesToLoad.sort((a, b) => a.createdAt - b.createdAt);
1112
+ console.log(`[BaseMindooDB] Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
1113
+ // Load and apply all entries
1114
+ console.log(`[BaseMindooDB] Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
1115
+ 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'})`);
1118
+ // 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
1132
+ 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;
1135
+ }
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;
1143
+ }
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);
1164
+ }
1165
+ }
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
+ });
1184
+ }
1185
+ throw error;
1186
+ }
1187
+ }
1188
+ // 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))}`);
1191
+ const payload = doc;
1192
+ // Check if document was deleted by looking for a "doc_delete" type entry
1193
+ const hasDeleteEntry = allEntryMetadata.some(em => em.entryType === "doc_delete");
1194
+ const isDeleted = hasDeleteEntry;
1195
+ console.log(`[BaseMindooDB] Document ${docId} isDeleted: ${isDeleted}`);
1196
+ const decryptionKeyId = payload._decryptionKeyId || "default";
1197
+ // Get lastModified from payload, or use the timestamp of the last entry
1198
+ const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
1199
+ const lastModified = payload._lastModified ||
1200
+ (lastEntry ? lastEntry.createdAt : Date.now());
1201
+ // Get createdAt from the first entry
1202
+ const firstEntry = allEntryMetadata.length > 0 ? allEntryMetadata[0] : null;
1203
+ const createdAt = firstEntry ? firstEntry.createdAt : lastModified;
1204
+ console.log(`[BaseMindooDB] Document ${docId} metadata: createdAt=${createdAt}, lastModified=${lastModified}, decryptionKeyId=${decryptionKeyId}`);
1205
+ const internalDoc = {
1206
+ id: docId,
1207
+ doc: doc, // doc is guaranteed to be defined at this point
1208
+ createdAt,
1209
+ lastModified,
1210
+ decryptionKeyId,
1211
+ isDeleted,
1212
+ };
1213
+ // Update cache
1214
+ this.docCache.set(docId, internalDoc);
1215
+ console.log(`[BaseMindooDB] ===== Successfully loaded document ${docId} and cached it =====`);
1216
+ return internalDoc;
1217
+ }
1218
+ /**
1219
+ * Wrap an internal document in the MindooDoc interface.
1220
+ * This is the read-only wrapper - write methods throw errors.
1221
+ */
1222
+ wrapDocument(internalDoc) {
1223
+ const db = this;
1224
+ const docId = internalDoc.id;
1225
+ // Create a read-only proxy that throws on any modification attempts
1226
+ const createReadOnlyProxy = (target) => {
1227
+ return new Proxy(target, {
1228
+ set: (_target, prop) => {
1229
+ throw new Error(`Cannot modify property '${String(prop)}' on read-only document. Use changeDoc() to modify documents.`);
1230
+ },
1231
+ deleteProperty: (_target, prop) => {
1232
+ throw new Error(`Cannot delete property '${String(prop)}' on read-only document. Use changeDoc() to modify documents.`);
1233
+ },
1234
+ get: (target, prop) => {
1235
+ const value = target[prop];
1236
+ // Recursively wrap nested objects (but not arrays or special types)
1237
+ if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Uint8Array)) {
1238
+ return createReadOnlyProxy(value);
1239
+ }
1240
+ return value;
1241
+ },
1242
+ });
1243
+ };
1244
+ return {
1245
+ getDatabase: () => db,
1246
+ getId: () => docId,
1247
+ getCreatedAt: () => internalDoc.createdAt,
1248
+ getLastModified: () => internalDoc.lastModified,
1249
+ isDeleted: () => internalDoc.isDeleted,
1250
+ getData: () => createReadOnlyProxy(internalDoc.doc),
1251
+ // ========== Attachment Write Methods ==========
1252
+ // These throw errors in the read-only wrapper
1253
+ addAttachment: async () => {
1254
+ throw new Error("addAttachment() can only be called within changeDoc() callback");
1255
+ },
1256
+ addAttachmentStream: async () => {
1257
+ throw new Error("addAttachmentStream() can only be called within changeDoc() callback");
1258
+ },
1259
+ removeAttachment: async () => {
1260
+ throw new Error("removeAttachment() can only be called within changeDoc() callback");
1261
+ },
1262
+ appendToAttachment: async () => {
1263
+ throw new Error("appendToAttachment() can only be called within changeDoc() callback");
1264
+ },
1265
+ // ========== Attachment Read Methods ==========
1266
+ // These work in the read-only wrapper
1267
+ getAttachments: () => {
1268
+ const payload = internalDoc.doc;
1269
+ return payload._attachments || [];
1270
+ },
1271
+ getAttachment: async (attachmentId) => {
1272
+ return db.getAttachmentInternal(docId, attachmentId);
1273
+ },
1274
+ getAttachmentRange: async (attachmentId, startByte, endByte) => {
1275
+ return db.getAttachmentRangeInternal(docId, attachmentId, startByte, endByte);
1276
+ },
1277
+ streamAttachment: (attachmentId, startOffset = 0) => {
1278
+ return db.streamAttachmentInternal(docId, attachmentId, startOffset);
1279
+ },
1280
+ };
1281
+ }
1282
+ /**
1283
+ * Get an attachment reference by ID from a document's _attachments array.
1284
+ */
1285
+ getAttachmentRefInternal(docId, attachmentId) {
1286
+ const internalDoc = this.docCache.get(docId);
1287
+ if (!internalDoc) {
1288
+ throw new Error(`Document ${docId} not found in cache`);
1289
+ }
1290
+ const payload = internalDoc.doc;
1291
+ const attachments = payload._attachments || [];
1292
+ const ref = attachments.find(a => a.attachmentId === attachmentId);
1293
+ if (!ref) {
1294
+ throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
1295
+ }
1296
+ return ref;
1297
+ }
1298
+ /**
1299
+ * Internal method to fetch and concatenate all chunks for an attachment.
1300
+ */
1301
+ async getAttachmentInternal(docId, attachmentId) {
1302
+ console.log(`[BaseMindooDB] Getting attachment ${attachmentId} from document ${docId}`);
1303
+ const ref = this.getAttachmentRefInternal(docId, attachmentId);
1304
+ const store = this.getEffectiveAttachmentStore();
1305
+ // Resolve dependency chain to get all chunk IDs in order (oldest first)
1306
+ const chunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
1307
+ console.log(`[BaseMindooDB] Resolved ${chunkIds.length} chunks for attachment ${attachmentId}`);
1308
+ // Fetch all chunks
1309
+ const chunks = await store.getEntries(chunkIds);
1310
+ // Verify signatures, decrypt, and collect plaintext chunks
1311
+ const plaintextChunks = [];
1312
+ let totalSize = 0;
1313
+ for (const chunk of chunks) {
1314
+ // Admin-only mode: only accept chunks signed by the admin key
1315
+ if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
1316
+ throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
1317
+ }
1318
+ // Verify signature
1319
+ const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
1320
+ if (!isValid) {
1321
+ throw new Error(`Invalid signature for chunk ${chunk.id}`);
1322
+ }
1323
+ // Decrypt
1324
+ const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
1325
+ plaintextChunks.push(plaintext);
1326
+ totalSize += plaintext.length;
1327
+ }
1328
+ // Concatenate all chunks into final result
1329
+ const result = new Uint8Array(totalSize);
1330
+ let offset = 0;
1331
+ for (const chunk of plaintextChunks) {
1332
+ result.set(chunk, offset);
1333
+ offset += chunk.length;
1334
+ }
1335
+ console.log(`[BaseMindooDB] Retrieved attachment ${attachmentId}: ${result.length} bytes`);
1336
+ return result;
1337
+ }
1338
+ /**
1339
+ * Internal method to get a byte range from an attachment.
1340
+ */
1341
+ async getAttachmentRangeInternal(docId, attachmentId, startByte, endByte) {
1342
+ console.log(`[BaseMindooDB] Getting attachment ${attachmentId} range [${startByte}, ${endByte}) from document ${docId}`);
1343
+ if (startByte < 0 || endByte <= startByte) {
1344
+ throw new Error(`Invalid byte range: [${startByte}, ${endByte})`);
1345
+ }
1346
+ const ref = this.getAttachmentRefInternal(docId, attachmentId);
1347
+ if (endByte > ref.size) {
1348
+ throw new Error(`End byte ${endByte} exceeds attachment size ${ref.size}`);
1349
+ }
1350
+ const store = this.getEffectiveAttachmentStore();
1351
+ const chunkSize = this.chunkSizeBytes;
1352
+ // Calculate which chunks we need
1353
+ const startChunkIndex = Math.floor(startByte / chunkSize);
1354
+ const endChunkIndex = Math.floor((endByte - 1) / chunkSize);
1355
+ // Resolve dependency chain to get all chunk IDs
1356
+ const allChunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
1357
+ // Get only the chunks we need
1358
+ const neededChunkIds = allChunkIds.slice(startChunkIndex, endChunkIndex + 1);
1359
+ const chunks = await store.getEntries(neededChunkIds);
1360
+ // Decrypt needed chunks
1361
+ const plaintextChunks = [];
1362
+ for (const chunk of chunks) {
1363
+ // Admin-only mode: only accept chunks signed by the admin key
1364
+ if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
1365
+ throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
1366
+ }
1367
+ // Verify signature
1368
+ const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
1369
+ if (!isValid) {
1370
+ throw new Error(`Invalid signature for chunk ${chunk.id}`);
1371
+ }
1372
+ // Decrypt
1373
+ const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
1374
+ plaintextChunks.push(plaintext);
1375
+ }
1376
+ // Calculate offsets within the fetched chunks
1377
+ const offsetInFirstChunk = startByte - (startChunkIndex * chunkSize);
1378
+ const totalNeededBytes = endByte - startByte;
1379
+ // Extract the requested range
1380
+ const result = new Uint8Array(totalNeededBytes);
1381
+ let resultOffset = 0;
1382
+ let bytesRemaining = totalNeededBytes;
1383
+ for (let i = 0; i < plaintextChunks.length && bytesRemaining > 0; i++) {
1384
+ const chunk = plaintextChunks[i];
1385
+ const chunkStart = i === 0 ? offsetInFirstChunk : 0;
1386
+ const bytesToCopy = Math.min(chunk.length - chunkStart, bytesRemaining);
1387
+ result.set(chunk.slice(chunkStart, chunkStart + bytesToCopy), resultOffset);
1388
+ resultOffset += bytesToCopy;
1389
+ bytesRemaining -= bytesToCopy;
1390
+ }
1391
+ console.log(`[BaseMindooDB] Retrieved attachment ${attachmentId} range: ${result.length} bytes`);
1392
+ return result;
1393
+ }
1394
+ /**
1395
+ * Internal async generator to stream attachment data.
1396
+ */
1397
+ async *streamAttachmentInternal(docId, attachmentId, startOffset) {
1398
+ console.log(`[BaseMindooDB] Streaming attachment ${attachmentId} from offset ${startOffset}`);
1399
+ const ref = this.getAttachmentRefInternal(docId, attachmentId);
1400
+ const store = this.getEffectiveAttachmentStore();
1401
+ const chunkSize = this.chunkSizeBytes;
1402
+ // Calculate starting chunk
1403
+ const startChunkIndex = Math.floor(startOffset / chunkSize);
1404
+ const offsetInStartChunk = startOffset % chunkSize;
1405
+ // Resolve dependency chain to get all chunk IDs
1406
+ const allChunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
1407
+ // Stream chunks starting from startChunkIndex
1408
+ for (let i = startChunkIndex; i < allChunkIds.length; i++) {
1409
+ const [chunk] = await store.getEntries([allChunkIds[i]]);
1410
+ // Admin-only mode: only accept chunks signed by the admin key
1411
+ if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
1412
+ throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
1413
+ }
1414
+ // Verify signature
1415
+ const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
1416
+ if (!isValid) {
1417
+ throw new Error(`Invalid signature for chunk ${chunk.id}`);
1418
+ }
1419
+ // Decrypt
1420
+ const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
1421
+ // For first chunk, skip bytes before startOffset
1422
+ if (i === startChunkIndex && offsetInStartChunk > 0) {
1423
+ yield plaintext.slice(offsetInStartChunk);
1424
+ }
1425
+ else {
1426
+ yield plaintext;
1427
+ }
1428
+ }
1429
+ console.log(`[BaseMindooDB] Finished streaming attachment ${attachmentId}`);
1430
+ }
1431
+ /**
1432
+ * Get the effective attachment store (attachment store if configured, otherwise doc store).
1433
+ */
1434
+ getEffectiveAttachmentStore() {
1435
+ return this.attachmentStore || this.store;
1436
+ }
1437
+ /**
1438
+ * Internal method to add an attachment by chunking the file and storing chunks.
1439
+ */
1440
+ async addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, createdAt) {
1441
+ console.log(`[BaseMindooDB] Adding attachment to document ${docId}: ${fileName} (${fileData.length} bytes)`);
1442
+ const store = this.getEffectiveAttachmentStore();
1443
+ const currentUser = await this.tenant.getCurrentUserId();
1444
+ const attachmentId = (0, idGeneration_1.generateFileUuid7)();
1445
+ // Chunk the file
1446
+ const chunks = [];
1447
+ let prevChunkId = null;
1448
+ let lastChunkId = "";
1449
+ for (let offset = 0; offset < fileData.length; offset += this.chunkSizeBytes) {
1450
+ const chunkData = fileData.slice(offset, Math.min(offset + this.chunkSizeBytes, fileData.length));
1451
+ // Encrypt chunk
1452
+ const encryptedData = await this.tenant.encryptAttachmentPayload(chunkData, decryptionKeyId);
1453
+ // Compute content hash
1454
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedData, this.getSubtle());
1455
+ // Generate chunk ID
1456
+ const chunkId = (0, idGeneration_1.generateAttachmentChunkId)(docId, attachmentId);
1457
+ lastChunkId = chunkId;
1458
+ // Sign the encrypted chunk
1459
+ const signature = await this.tenant.signPayload(encryptedData);
1460
+ // Create chunk entry
1461
+ const chunkEntry = {
1462
+ entryType: "attachment_chunk",
1463
+ id: chunkId,
1464
+ contentHash,
1465
+ docId,
1466
+ dependencyIds: prevChunkId ? [prevChunkId] : [],
1467
+ createdAt,
1468
+ createdByPublicKey: currentUser.userSigningPublicKey,
1469
+ decryptionKeyId,
1470
+ signature,
1471
+ originalSize: chunkData.length,
1472
+ encryptedSize: encryptedData.length,
1473
+ encryptedData,
1474
+ };
1475
+ chunks.push(chunkEntry);
1476
+ prevChunkId = chunkId;
1477
+ }
1478
+ // Store all chunks
1479
+ await store.putEntries(chunks);
1480
+ console.log(`[BaseMindooDB] Stored ${chunks.length} chunks for attachment ${attachmentId}`);
1481
+ // Create attachment reference
1482
+ const ref = {
1483
+ attachmentId,
1484
+ fileName,
1485
+ mimeType,
1486
+ size: fileData.length,
1487
+ lastChunkId,
1488
+ decryptionKeyId,
1489
+ createdAt,
1490
+ createdBy: currentUser.userSigningPublicKey,
1491
+ };
1492
+ return ref;
1493
+ }
1494
+ /**
1495
+ * Internal method to add an attachment from a streaming data source.
1496
+ * Memory efficient - processes data chunk by chunk without loading entire file into memory.
1497
+ */
1498
+ async addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, createdAt) {
1499
+ console.log(`[BaseMindooDB] Adding streaming attachment to document ${docId}: ${fileName}`);
1500
+ const store = this.getEffectiveAttachmentStore();
1501
+ const currentUser = await this.tenant.getCurrentUserId();
1502
+ const attachmentId = (0, idGeneration_1.generateFileUuid7)();
1503
+ // Buffer to accumulate incoming data until we have a full chunk
1504
+ let buffer = new Uint8Array(0);
1505
+ let totalSize = 0;
1506
+ let prevChunkId = null;
1507
+ let lastChunkId = "";
1508
+ let chunkCount = 0;
1509
+ // Helper to concatenate Uint8Arrays
1510
+ const concatArrays = (a, b) => {
1511
+ const result = new Uint8Array(a.length + b.length);
1512
+ result.set(a, 0);
1513
+ result.set(b, a.length);
1514
+ return result;
1515
+ };
1516
+ // Helper to store a chunk
1517
+ const storeChunk = async (chunkData) => {
1518
+ // Encrypt chunk
1519
+ const encryptedData = await this.tenant.encryptAttachmentPayload(chunkData, decryptionKeyId);
1520
+ // Compute content hash
1521
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedData, this.getSubtle());
1522
+ // Generate chunk ID
1523
+ const chunkId = (0, idGeneration_1.generateAttachmentChunkId)(docId, attachmentId);
1524
+ // Sign the encrypted chunk
1525
+ const signature = await this.tenant.signPayload(encryptedData);
1526
+ // Create chunk entry
1527
+ const chunkEntry = {
1528
+ entryType: "attachment_chunk",
1529
+ id: chunkId,
1530
+ contentHash,
1531
+ docId,
1532
+ dependencyIds: prevChunkId ? [prevChunkId] : [],
1533
+ createdAt,
1534
+ createdByPublicKey: currentUser.userSigningPublicKey,
1535
+ decryptionKeyId,
1536
+ signature,
1537
+ originalSize: chunkData.length,
1538
+ encryptedSize: encryptedData.length,
1539
+ encryptedData,
1540
+ };
1541
+ // Store immediately (streaming - don't buffer chunks in memory)
1542
+ await store.putEntries([chunkEntry]);
1543
+ chunkCount++;
1544
+ return chunkId;
1545
+ };
1546
+ // Process incoming data stream
1547
+ for await (const chunk of dataStream) {
1548
+ // Add incoming data to buffer
1549
+ buffer = concatArrays(buffer, chunk);
1550
+ // Process complete chunks
1551
+ while (buffer.length >= this.chunkSizeBytes) {
1552
+ const chunkData = buffer.slice(0, this.chunkSizeBytes);
1553
+ buffer = buffer.slice(this.chunkSizeBytes);
1554
+ lastChunkId = await storeChunk(chunkData);
1555
+ prevChunkId = lastChunkId;
1556
+ totalSize += chunkData.length;
1557
+ }
1558
+ }
1559
+ // Store remaining data as final chunk (if any)
1560
+ if (buffer.length > 0) {
1561
+ lastChunkId = await storeChunk(buffer);
1562
+ totalSize += buffer.length;
1563
+ }
1564
+ console.log(`[BaseMindooDB] Stored ${chunkCount} chunks for streaming attachment ${attachmentId} (${totalSize} bytes)`);
1565
+ // Create attachment reference
1566
+ const ref = {
1567
+ attachmentId,
1568
+ fileName,
1569
+ mimeType,
1570
+ size: totalSize,
1571
+ lastChunkId,
1572
+ decryptionKeyId,
1573
+ createdAt,
1574
+ createdBy: currentUser.userSigningPublicKey,
1575
+ };
1576
+ return ref;
1577
+ }
1578
+ /**
1579
+ * Internal method to append data to an existing attachment.
1580
+ */
1581
+ async appendToAttachmentInternal(docId, attachmentId, decryptionKeyId, prevLastChunkId, data, createdAt) {
1582
+ console.log(`[BaseMindooDB] Appending ${data.length} bytes to attachment ${attachmentId}`);
1583
+ const store = this.getEffectiveAttachmentStore();
1584
+ const currentUser = await this.tenant.getCurrentUserId();
1585
+ // Chunk the data
1586
+ const chunks = [];
1587
+ let prevChunkId = prevLastChunkId;
1588
+ let lastChunkId = prevLastChunkId;
1589
+ for (let offset = 0; offset < data.length; offset += this.chunkSizeBytes) {
1590
+ const chunkData = data.slice(offset, Math.min(offset + this.chunkSizeBytes, data.length));
1591
+ // Encrypt chunk
1592
+ const encryptedData = await this.tenant.encryptAttachmentPayload(chunkData, decryptionKeyId);
1593
+ // Compute content hash
1594
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedData, this.getSubtle());
1595
+ // Generate chunk ID
1596
+ const chunkId = (0, idGeneration_1.generateAttachmentChunkId)(docId, attachmentId);
1597
+ lastChunkId = chunkId;
1598
+ // Sign the encrypted chunk
1599
+ const signature = await this.tenant.signPayload(encryptedData);
1600
+ // Create chunk entry with dependency on previous chunk
1601
+ const chunkEntry = {
1602
+ entryType: "attachment_chunk",
1603
+ id: chunkId,
1604
+ contentHash,
1605
+ docId,
1606
+ dependencyIds: [prevChunkId],
1607
+ createdAt,
1608
+ createdByPublicKey: currentUser.userSigningPublicKey,
1609
+ decryptionKeyId,
1610
+ signature,
1611
+ originalSize: chunkData.length,
1612
+ encryptedSize: encryptedData.length,
1613
+ encryptedData,
1614
+ };
1615
+ chunks.push(chunkEntry);
1616
+ prevChunkId = chunkId;
1617
+ }
1618
+ // Store all chunks
1619
+ await store.putEntries(chunks);
1620
+ console.log(`[BaseMindooDB] Appended ${chunks.length} chunks to attachment ${attachmentId}`);
1621
+ return {
1622
+ lastChunkId,
1623
+ sizeIncrease: data.length,
1624
+ };
1625
+ }
1626
+ /**
1627
+ * Pull changes from a remote content-addressed store.
1628
+ *
1629
+ * This method:
1630
+ * 1. Finds entries in the remote store that we don't have locally
1631
+ * 2. Retrieves those entries from the remote store
1632
+ * 3. Stores them in our local store
1633
+ * 4. Syncs the local store to process the new entries
1634
+ *
1635
+ * @param remoteStore The remote store to pull entries from
1636
+ * @return A promise that resolves when the pull is complete
1637
+ */
1638
+ async pullChangesFrom(remoteStore) {
1639
+ if (this.store.getId() !== remoteStore.getId()) {
1640
+ throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
1641
+ }
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`);
1651
+ return;
1652
+ }
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
+ // Sync the local store to process the new entries
1661
+ // This will update the index, cache, and processedEntryIds
1662
+ await this.syncStoreChanges();
1663
+ console.log(`[BaseMindooDB] Pull complete, synced ${newEntries.length} entries`);
1664
+ }
1665
+ /**
1666
+ * Push changes to a remote content-addressed store.
1667
+ *
1668
+ * This method:
1669
+ * 1. Finds entries in our local store that the remote doesn't have
1670
+ * 2. Retrieves those entries from our local store
1671
+ * 3. Stores them in the remote store
1672
+ *
1673
+ * @param remoteStore The remote store to push entries to
1674
+ * @return A promise that resolves when the push is complete
1675
+ */
1676
+ async pushChangesTo(remoteStore) {
1677
+ if (this.store.getId() !== remoteStore.getId()) {
1678
+ throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
1679
+ }
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`);
1689
+ return;
1690
+ }
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`);
1698
+ }
1699
+ }
1700
+ exports.BaseMindooDB = BaseMindooDB;
1701
+ //# sourceMappingURL=BaseMindooDB.js.map