mindoodb 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/README-server.md +1001 -0
  2. package/README.md +89 -28
  3. package/dist/__mocks__/expo-standard-web-crypto.d.ts +10 -0
  4. package/dist/__mocks__/expo-standard-web-crypto.d.ts.map +1 -0
  5. package/dist/__mocks__/expo-standard-web-crypto.js +22 -0
  6. package/dist/__mocks__/expo-standard-web-crypto.js.map +1 -0
  7. package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.d.ts +8 -0
  8. package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.d.ts.map +1 -0
  9. package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.js +16 -0
  10. package/dist/appendonlystores/BasicOnDiskContentAddressedStoreFactory.js.map +1 -0
  11. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts +1 -1
  12. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -1
  13. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +2 -2
  14. package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -1
  15. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +30 -1
  16. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  17. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +152 -5
  18. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  19. package/dist/appendonlystores/network/HttpTransport.d.ts +17 -2
  20. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  21. package/dist/appendonlystores/network/HttpTransport.js +129 -0
  22. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  23. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +22 -2
  24. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  25. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +110 -0
  26. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  27. package/dist/appendonlystores/network/index.d.ts +1 -1
  28. package/dist/appendonlystores/network/index.d.ts.map +1 -1
  29. package/dist/appendonlystores/network/index.js.map +1 -1
  30. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +65 -0
  31. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -0
  32. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +741 -0
  33. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -0
  34. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts +25 -0
  35. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts.map +1 -0
  36. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js +41 -0
  37. package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js.map +1 -0
  38. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts +21 -0
  39. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -0
  40. package/dist/browser/cache/IndexedDBLocalCacheStore.js +106 -0
  41. package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -0
  42. package/dist/browser/index.d.ts +8 -0
  43. package/dist/browser/index.d.ts.map +1 -1
  44. package/dist/browser/index.js +21 -1
  45. package/dist/browser/index.js.map +1 -1
  46. package/dist/core/BaseMindooDB.d.ts +115 -13
  47. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  48. package/dist/core/BaseMindooDB.js +1232 -269
  49. package/dist/core/BaseMindooDB.js.map +1 -1
  50. package/dist/core/BaseMindooTenant.d.ts +69 -6
  51. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  52. package/dist/core/BaseMindooTenant.js +302 -109
  53. package/dist/core/BaseMindooTenant.js.map +1 -1
  54. package/dist/core/BaseMindooTenantDirectory.d.ts +13 -10
  55. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  56. package/dist/core/BaseMindooTenantDirectory.js +144 -27
  57. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  58. package/dist/core/BaseMindooTenantFactory.d.ts +27 -29
  59. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  60. package/dist/core/BaseMindooTenantFactory.js +185 -112
  61. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  62. package/dist/core/MindooDBServerAdmin.d.ts +238 -0
  63. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -0
  64. package/dist/core/MindooDBServerAdmin.js +345 -0
  65. package/dist/core/MindooDBServerAdmin.js.map +1 -0
  66. package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +68 -0
  67. package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
  68. package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js +659 -0
  69. package/dist/core/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
  70. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +45 -3
  71. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  72. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +184 -23
  73. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  74. package/dist/core/appendonlystores/MaterializationPlanner.d.ts +49 -0
  75. package/dist/core/appendonlystores/MaterializationPlanner.d.ts.map +1 -0
  76. package/dist/core/appendonlystores/MaterializationPlanner.js +325 -0
  77. package/dist/core/appendonlystores/MaterializationPlanner.js.map +1 -0
  78. package/dist/core/appendonlystores/bloom.d.ts +85 -0
  79. package/dist/core/appendonlystores/bloom.d.ts.map +1 -0
  80. package/dist/core/appendonlystores/bloom.js +228 -0
  81. package/dist/core/appendonlystores/bloom.js.map +1 -0
  82. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +37 -2
  83. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  84. package/dist/core/appendonlystores/network/types.d.ts +12 -0
  85. package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
  86. package/dist/core/appendonlystores/network/types.js.map +1 -1
  87. package/dist/core/appendonlystores/types.d.ts +206 -0
  88. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  89. package/dist/core/automerge-adapter.d.ts +15 -0
  90. package/dist/core/automerge-adapter.d.ts.map +1 -0
  91. package/dist/core/automerge-adapter.js +73 -0
  92. package/dist/core/automerge-adapter.js.map +1 -0
  93. package/dist/core/cache/CacheManager.d.ts +75 -0
  94. package/dist/core/cache/CacheManager.d.ts.map +1 -0
  95. package/dist/core/cache/CacheManager.js +101 -0
  96. package/dist/core/cache/CacheManager.js.map +1 -0
  97. package/dist/core/cache/EncryptedLocalCacheStore.d.ts +41 -0
  98. package/dist/core/cache/EncryptedLocalCacheStore.d.ts.map +1 -0
  99. package/dist/core/cache/EncryptedLocalCacheStore.js +120 -0
  100. package/dist/core/cache/EncryptedLocalCacheStore.js.map +1 -0
  101. package/dist/core/cache/LocalCacheStore.d.ts +50 -0
  102. package/dist/core/cache/LocalCacheStore.d.ts.map +1 -0
  103. package/dist/core/cache/LocalCacheStore.js +39 -0
  104. package/dist/core/cache/LocalCacheStore.js.map +1 -0
  105. package/dist/core/crypto/pbkdf2Iterations.d.ts +12 -0
  106. package/dist/core/crypto/pbkdf2Iterations.d.ts.map +1 -0
  107. package/dist/core/crypto/pbkdf2Iterations.js +55 -0
  108. package/dist/core/crypto/pbkdf2Iterations.js.map +1 -0
  109. package/dist/core/crypto/privateKeyEncryption.d.ts +5 -0
  110. package/dist/core/crypto/privateKeyEncryption.d.ts.map +1 -0
  111. package/dist/core/crypto/privateKeyEncryption.js +87 -0
  112. package/dist/core/crypto/privateKeyEncryption.js.map +1 -0
  113. package/dist/core/index.d.ts +18 -6
  114. package/dist/core/index.d.ts.map +1 -1
  115. package/dist/core/index.js +42 -1
  116. package/dist/core/index.js.map +1 -1
  117. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +4 -1
  118. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
  119. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +39 -11
  120. package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
  121. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +1 -0
  122. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -1
  123. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +3 -0
  124. package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -1
  125. package/dist/core/indexing/virtualviews/VirtualView.d.ts +43 -0
  126. package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -1
  127. package/dist/core/indexing/virtualviews/VirtualView.js +173 -0
  128. package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -1
  129. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +7 -0
  130. package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -1
  131. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +28 -0
  132. package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -1
  133. package/dist/core/keys/KeyBag.d.ts +56 -12
  134. package/dist/core/keys/KeyBag.d.ts.map +1 -1
  135. package/dist/core/keys/KeyBag.js +156 -70
  136. package/dist/core/keys/KeyBag.js.map +1 -1
  137. package/dist/core/keys/KeyContext.d.ts +4 -0
  138. package/dist/core/keys/KeyContext.d.ts.map +1 -0
  139. package/dist/core/keys/KeyContext.js +17 -0
  140. package/dist/core/keys/KeyContext.js.map +1 -0
  141. package/dist/core/types.d.ts +512 -60
  142. package/dist/core/types.d.ts.map +1 -1
  143. package/dist/core/types.js +9 -1
  144. package/dist/core/types.js.map +1 -1
  145. package/dist/core/uri/MindooURI.d.ts +58 -0
  146. package/dist/core/uri/MindooURI.d.ts.map +1 -0
  147. package/dist/core/uri/MindooURI.js +169 -0
  148. package/dist/core/uri/MindooURI.js.map +1 -0
  149. package/dist/core/userid/index.d.ts +0 -10
  150. package/dist/core/userid/index.d.ts.map +1 -1
  151. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  152. package/dist/core/utils/idGeneration.js +37 -3
  153. package/dist/core/utils/idGeneration.js.map +1 -1
  154. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +544 -0
  155. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -0
  156. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +1358 -0
  157. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -0
  158. package/dist/node/attachments/writeAttachmentToFile.d.ts +15 -0
  159. package/dist/node/attachments/writeAttachmentToFile.d.ts.map +1 -0
  160. package/dist/node/attachments/writeAttachmentToFile.js +73 -0
  161. package/dist/node/attachments/writeAttachmentToFile.js.map +1 -0
  162. package/dist/node/cache/FileSystemLocalCacheStore.d.ts +17 -0
  163. package/dist/node/cache/FileSystemLocalCacheStore.d.ts.map +1 -0
  164. package/dist/node/cache/FileSystemLocalCacheStore.js +128 -0
  165. package/dist/node/cache/FileSystemLocalCacheStore.js.map +1 -0
  166. package/dist/node/cli/change-identity-password.d.ts +7 -0
  167. package/dist/node/cli/change-identity-password.d.ts.map +1 -0
  168. package/dist/node/cli/change-identity-password.js +67 -0
  169. package/dist/node/cli/change-identity-password.js.map +1 -0
  170. package/dist/node/cli/cli-utils.d.ts +16 -0
  171. package/dist/node/cli/cli-utils.d.ts.map +1 -0
  172. package/dist/node/cli/cli-utils.js +113 -0
  173. package/dist/node/cli/cli-utils.js.map +1 -0
  174. package/dist/node/cli/identity-export-public.d.ts +4 -0
  175. package/dist/node/cli/identity-export-public.d.ts.map +1 -0
  176. package/dist/node/cli/identity-export-public.js +55 -0
  177. package/dist/node/cli/identity-export-public.js.map +1 -0
  178. package/dist/node/cli/identity-info.d.ts +4 -0
  179. package/dist/node/cli/identity-info.d.ts.map +1 -0
  180. package/dist/node/cli/identity-info.js +57 -0
  181. package/dist/node/cli/identity-info.js.map +1 -0
  182. package/dist/node/index.d.ts +2 -0
  183. package/dist/node/index.d.ts.map +1 -1
  184. package/dist/node/index.js +8 -1
  185. package/dist/node/index.js.map +1 -1
  186. package/dist/node/server/CapabilityMatcher.d.ts +45 -0
  187. package/dist/node/server/CapabilityMatcher.d.ts.map +1 -0
  188. package/dist/node/server/CapabilityMatcher.js +117 -0
  189. package/dist/node/server/CapabilityMatcher.js.map +1 -0
  190. package/dist/node/server/MindooDBServer.d.ts +87 -0
  191. package/dist/node/server/MindooDBServer.d.ts.map +1 -0
  192. package/dist/node/server/MindooDBServer.js +1040 -0
  193. package/dist/node/server/MindooDBServer.js.map +1 -0
  194. package/dist/node/server/ServerSync.d.ts +41 -0
  195. package/dist/node/server/ServerSync.d.ts.map +1 -0
  196. package/dist/node/server/ServerSync.js +187 -0
  197. package/dist/node/server/ServerSync.js.map +1 -0
  198. package/dist/node/server/StoreFactory.d.ts +45 -0
  199. package/dist/node/server/StoreFactory.d.ts.map +1 -0
  200. package/dist/node/server/StoreFactory.js +89 -0
  201. package/dist/node/server/StoreFactory.js.map +1 -0
  202. package/dist/node/server/SystemAdminAuth.d.ts +65 -0
  203. package/dist/node/server/SystemAdminAuth.d.ts.map +1 -0
  204. package/dist/node/server/SystemAdminAuth.js +200 -0
  205. package/dist/node/server/SystemAdminAuth.js.map +1 -0
  206. package/dist/node/server/SystemIpAllowlist.d.ts +21 -0
  207. package/dist/node/server/SystemIpAllowlist.d.ts.map +1 -0
  208. package/dist/node/server/SystemIpAllowlist.js +210 -0
  209. package/dist/node/server/SystemIpAllowlist.js.map +1 -0
  210. package/dist/node/server/TenantManager.d.ts +85 -0
  211. package/dist/node/server/TenantManager.d.ts.map +1 -0
  212. package/dist/node/server/TenantManager.js +493 -0
  213. package/dist/node/server/TenantManager.js.map +1 -0
  214. package/dist/node/server/add-to-network.d.ts +20 -0
  215. package/dist/node/server/add-to-network.d.ts.map +1 -0
  216. package/dist/node/server/add-to-network.js +275 -0
  217. package/dist/node/server/add-to-network.js.map +1 -0
  218. package/dist/node/server/config.d.ts +51 -0
  219. package/dist/node/server/config.d.ts.map +1 -0
  220. package/dist/node/server/config.js +218 -0
  221. package/dist/node/server/config.js.map +1 -0
  222. package/dist/node/server/resolveServerPassword.d.ts +7 -0
  223. package/dist/node/server/resolveServerPassword.d.ts.map +1 -0
  224. package/dist/node/server/resolveServerPassword.js +25 -0
  225. package/dist/node/server/resolveServerPassword.js.map +1 -0
  226. package/dist/node/server/server.d.ts +22 -0
  227. package/dist/node/server/server.d.ts.map +1 -0
  228. package/dist/node/server/server.js +225 -0
  229. package/dist/node/server/server.js.map +1 -0
  230. package/dist/node/server/serverinit.d.ts +21 -0
  231. package/dist/node/server/serverinit.d.ts.map +1 -0
  232. package/dist/node/server/serverinit.js +317 -0
  233. package/dist/node/server/serverinit.js.map +1 -0
  234. package/dist/node/server/types.d.ts +162 -0
  235. package/dist/node/server/types.d.ts.map +1 -0
  236. package/dist/node/server/types.js +24 -0
  237. package/dist/node/server/types.js.map +1 -0
  238. package/dist/node/server/validation.d.ts +47 -0
  239. package/dist/node/server/validation.d.ts.map +1 -0
  240. package/dist/node/server/validation.js +114 -0
  241. package/dist/node/server/validation.js.map +1 -0
  242. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts +48 -0
  243. package/dist/reactnative/cache/MMKVLocalCacheStore.d.ts.map +1 -0
  244. package/dist/reactnative/cache/MMKVLocalCacheStore.js +113 -0
  245. package/dist/reactnative/cache/MMKVLocalCacheStore.js.map +1 -0
  246. package/dist/reactnative/crypto/AESAdapter.d.ts +18 -0
  247. package/dist/reactnative/crypto/AESAdapter.d.ts.map +1 -0
  248. package/dist/reactnative/crypto/AESAdapter.js +112 -0
  249. package/dist/reactnative/crypto/AESAdapter.js.map +1 -0
  250. package/dist/reactnative/crypto/Ed25519Adapter.d.ts +34 -0
  251. package/dist/reactnative/crypto/Ed25519Adapter.d.ts.map +1 -0
  252. package/dist/reactnative/crypto/Ed25519Adapter.js +386 -0
  253. package/dist/reactnative/crypto/Ed25519Adapter.js.map +1 -0
  254. package/dist/reactnative/crypto/ExpoGoCryptoAdapter.d.ts +23 -0
  255. package/dist/reactnative/crypto/ExpoGoCryptoAdapter.d.ts.map +1 -0
  256. package/dist/reactnative/crypto/ExpoGoCryptoAdapter.js +51 -0
  257. package/dist/reactnative/crypto/ExpoGoCryptoAdapter.js.map +1 -0
  258. package/dist/reactnative/crypto/HMACAdapter.d.ts +13 -0
  259. package/dist/reactnative/crypto/HMACAdapter.d.ts.map +1 -0
  260. package/dist/reactnative/crypto/HMACAdapter.js +50 -0
  261. package/dist/reactnative/crypto/HMACAdapter.js.map +1 -0
  262. package/dist/reactnative/crypto/KeyFormatConverter.d.ts +21 -0
  263. package/dist/reactnative/crypto/KeyFormatConverter.d.ts.map +1 -0
  264. package/dist/reactnative/crypto/KeyFormatConverter.js +72 -0
  265. package/dist/reactnative/crypto/KeyFormatConverter.js.map +1 -0
  266. package/dist/reactnative/crypto/PBKDF2Adapter.d.ts +19 -0
  267. package/dist/reactnative/crypto/PBKDF2Adapter.d.ts.map +1 -0
  268. package/dist/reactnative/crypto/PBKDF2Adapter.js +67 -0
  269. package/dist/reactnative/crypto/PBKDF2Adapter.js.map +1 -0
  270. package/dist/reactnative/crypto/QuickCryptoAdapter.d.ts +37 -0
  271. package/dist/reactnative/crypto/QuickCryptoAdapter.d.ts.map +1 -0
  272. package/dist/reactnative/crypto/QuickCryptoAdapter.js +72 -0
  273. package/dist/reactnative/crypto/QuickCryptoAdapter.js.map +1 -0
  274. package/dist/reactnative/crypto/RSAAdapter.d.ts +38 -0
  275. package/dist/reactnative/crypto/RSAAdapter.d.ts.map +1 -0
  276. package/dist/reactnative/crypto/RSAAdapter.js +184 -0
  277. package/dist/reactnative/crypto/RSAAdapter.js.map +1 -0
  278. package/dist/reactnative/crypto/ReactNativeCryptoAdapter.d.ts +33 -0
  279. package/dist/reactnative/crypto/ReactNativeCryptoAdapter.d.ts.map +1 -0
  280. package/dist/reactnative/crypto/ReactNativeCryptoAdapter.js +61 -0
  281. package/dist/reactnative/crypto/ReactNativeCryptoAdapter.js.map +1 -0
  282. package/dist/reactnative/crypto/SHA256Adapter.d.ts +9 -0
  283. package/dist/reactnative/crypto/SHA256Adapter.d.ts.map +1 -0
  284. package/dist/reactnative/crypto/SHA256Adapter.js +29 -0
  285. package/dist/reactnative/crypto/SHA256Adapter.js.map +1 -0
  286. package/dist/reactnative/crypto/SubtleCryptoPolyfill.d.ts +63 -0
  287. package/dist/reactnative/crypto/SubtleCryptoPolyfill.d.ts.map +1 -0
  288. package/dist/reactnative/crypto/SubtleCryptoPolyfill.js +350 -0
  289. package/dist/reactnative/crypto/SubtleCryptoPolyfill.js.map +1 -0
  290. package/dist/reactnative/index.d.ts +9 -0
  291. package/dist/reactnative/index.d.ts.map +1 -0
  292. package/dist/reactnative/index.js +41 -0
  293. package/dist/reactnative/index.js.map +1 -0
  294. package/package.json +63 -7
  295. package/patches/PATCHES.md +111 -0
  296. package/patches/react-native+0.76.9.patch +34 -0
  297. package/patches/react-native-quick-crypto+1.0.7.patch +42 -0
  298. package/scripts/cli.js +21 -0
  299. package/scripts/setup-react-native.js +228 -0
  300. package/templates/reactnative/App.js +252 -0
  301. package/templates/reactnative/app.json +29 -0
  302. package/templates/reactnative/babel.config.js +6 -0
  303. package/templates/reactnative/index.js +26 -0
  304. package/templates/reactnative/metro.config.snippet.js +39 -0
  305. package/templates/reactnative/mindoodb-polyfills.js +116 -0
@@ -1,41 +1,13 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.BaseMindooDB = void 0;
37
- const Automerge = __importStar(require("@automerge/automerge"));
4
+ // Import platform-appropriate Automerge implementation
5
+ // React Native: native Rust (react-native-automerge-generated)
6
+ // Browser/Node.js: WASM (@automerge/automerge/slim)
7
+ const automerge_adapter_1 = require("./automerge-adapter");
38
8
  const uuid_1 = require("uuid");
9
+ const bloom_1 = require("./appendonlystores/bloom");
10
+ const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
39
11
  const idGeneration_1 = require("./utils/idGeneration");
40
12
  const errors_1 = require("./errors");
41
13
  const logging_1 = require("./logging");
@@ -43,6 +15,10 @@ const logging_1 = require("./logging");
43
15
  * Default chunk size for attachments: 256KB
44
16
  */
45
17
  const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
18
+ const DEFAULT_SNAPSHOT_MIN_CHANGES = 100;
19
+ const DEFAULT_SNAPSHOT_COOLDOWN_MS = 10 * 60 * 1000;
20
+ const DEFAULT_MAX_CACHED_DOCS = 128;
21
+ const DEFAULT_ITERATE_PREFETCH_WINDOW_DOCS = 0;
46
22
  /**
47
23
  * BaseMindooDB is a platform-agnostic implementation of MindooDB
48
24
  * that works in both browser and server environments.
@@ -63,27 +39,40 @@ const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
63
39
  * - Automerge.applyChanges(doc, changes) - verify method signature
64
40
  */
65
41
  class BaseMindooDB {
66
- constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false, logger, performanceCallback) {
67
- // Internal index: sorted array of document entries, maintained in order by (lastModified, docId)
68
- // This allows efficient incremental processing without sorting on each call
42
+ constructor(tenant, store, attachmentStore, attachmentConfig, documentCacheConfig, snapshotConfig, adminOnlyDb = false, logger, performanceCallback) {
43
+ // Internal changefeed index: sorted by (changeSeq, docId) for deterministic iteration.
44
+ // lastModified remains available for UX metadata but is not the primary cursor key.
69
45
  this.index = [];
70
46
  // Lookup map for O(1) access to index entries by docId
71
47
  this.indexLookup = new Map(); // Map<docId, arrayIndex>
48
+ this.nextChangeSeq = 1;
72
49
  // Cache of loaded documents: Map<docId, InternalDoc>
73
50
  this.docCache = new Map();
74
51
  // Track which entry IDs we've already processed
75
52
  this.processedEntryIds = [];
53
+ this.processedEntryCursor = null;
76
54
  // Index: automergeHash -> entryId for each document
77
55
  // Used for resolving Automerge dependency hashes to entry IDs
78
56
  this.automergeHashToEntryId = new Map(); // Map<docId, Map<automergeHash, entryId>>
79
57
  // Cache for imported public keys (CryptoKey objects) to avoid re-importing the same key
80
58
  // Map<publicKeyPEM, CryptoKey>
81
59
  this.publicKeyCache = new Map();
60
+ // Local cache support
61
+ this.cacheManager = null;
62
+ this.cachePrefix = null;
63
+ this.dirtyDocIds = new Set();
64
+ this.cacheMetaDirty = false;
82
65
  this.tenant = tenant;
83
66
  this.store = store;
84
67
  this.attachmentStore = attachmentStore;
85
68
  this.chunkSizeBytes = attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
86
69
  this._isAdminOnlyDb = adminOnlyDb;
70
+ this.maxCachedDocs = Math.max(1, Math.floor(documentCacheConfig?.maxEntries ?? DEFAULT_MAX_CACHED_DOCS));
71
+ this.iteratePrefetchWindowDocs = Math.max(0, Math.floor(documentCacheConfig?.iteratePrefetchWindowDocs ??
72
+ DEFAULT_ITERATE_PREFETCH_WINDOW_DOCS));
73
+ this.cacheRestoreLimit = Math.max(1, Math.floor(documentCacheConfig?.restoreLimit ?? this.maxCachedDocs));
74
+ this.snapshotMinChanges = Math.max(1, Math.floor(snapshotConfig?.minChanges ?? DEFAULT_SNAPSHOT_MIN_CHANGES));
75
+ this.snapshotCooldownMs = Math.max(0, Math.floor(snapshotConfig?.cooldownMs ?? DEFAULT_SNAPSHOT_COOLDOWN_MS));
87
76
  // Create logger if not provided (for backward compatibility)
88
77
  this.logger =
89
78
  logger ||
@@ -100,6 +89,207 @@ class BaseMindooDB {
100
89
  isAdminOnlyDb() {
101
90
  return this._isAdminOnlyDb;
102
91
  }
92
+ // ---------------------------------------------------------------------------
93
+ // Local cache support (ICacheable)
94
+ // ---------------------------------------------------------------------------
95
+ /**
96
+ * Attach a CacheManager so this DB participates in periodic cache flushing.
97
+ */
98
+ setCacheManager(cacheManager) {
99
+ this.cacheManager = cacheManager;
100
+ const cacheIdentity = this.store.getCacheIdentity?.() ?? this.store.getId();
101
+ this.cachePrefix = `${this.tenant.getId()}/${cacheIdentity}`;
102
+ cacheManager.register(this);
103
+ }
104
+ getCachePrefix() {
105
+ return this.cachePrefix ?? `${this.tenant.getId()}/${this.store.getId()}`;
106
+ }
107
+ hasDirtyState() {
108
+ return this.dirtyDocIds.size > 0 || this.cacheMetaDirty;
109
+ }
110
+ clearDirty() {
111
+ this.dirtyDocIds.clear();
112
+ this.cacheMetaDirty = false;
113
+ this.evictCachedDocsIfNeeded();
114
+ }
115
+ markDocDirty(docId) {
116
+ this.dirtyDocIds.add(docId);
117
+ this.cacheMetaDirty = true;
118
+ this.cacheManager?.markDirty();
119
+ }
120
+ getCachedDocument(docId) {
121
+ const cached = this.docCache.get(docId) ?? null;
122
+ if (!cached) {
123
+ return null;
124
+ }
125
+ this.touchCachedDocument(docId, cached);
126
+ return cached;
127
+ }
128
+ touchCachedDocument(docId, internalDoc) {
129
+ this.docCache.delete(docId);
130
+ this.docCache.set(docId, internalDoc);
131
+ }
132
+ storeCachedDocument(internalDoc) {
133
+ this.touchCachedDocument(internalDoc.id, internalDoc);
134
+ this.evictCachedDocsIfNeeded(new Set([internalDoc.id]));
135
+ }
136
+ evictCachedDocsIfNeeded(protectedDocIds) {
137
+ if (this.docCache.size <= this.maxCachedDocs) {
138
+ return;
139
+ }
140
+ for (const [docId] of this.docCache) {
141
+ if (this.docCache.size <= this.maxCachedDocs) {
142
+ break;
143
+ }
144
+ if (protectedDocIds?.has(docId)) {
145
+ continue;
146
+ }
147
+ this.docCache.delete(docId);
148
+ this.dirtyDocIds.delete(docId);
149
+ }
150
+ }
151
+ /**
152
+ * Flush dirty documents and metadata to the cache store.
153
+ */
154
+ async flushToCache(store) {
155
+ const prefix = this.getCachePrefix();
156
+ let written = 0;
157
+ for (const docId of this.dirtyDocIds) {
158
+ const internal = this.docCache.get(docId);
159
+ if (!internal)
160
+ continue;
161
+ const amBinary = automerge_adapter_1.Automerge.save(internal.doc);
162
+ const header = JSON.stringify({
163
+ id: internal.id,
164
+ createdAt: internal.createdAt,
165
+ lastModified: internal.lastModified,
166
+ decryptionKeyId: internal.decryptionKeyId,
167
+ isDeleted: internal.isDeleted,
168
+ });
169
+ const headerBytes = new TextEncoder().encode(header);
170
+ // Format: 4-byte header length (big-endian) + header JSON + Automerge binary
171
+ const value = new Uint8Array(4 + headerBytes.length + amBinary.length);
172
+ const view = new DataView(value.buffer);
173
+ view.setUint32(0, headerBytes.length, false);
174
+ value.set(headerBytes, 4);
175
+ value.set(amBinary, 4 + headerBytes.length);
176
+ await store.put("doc", `${prefix}/${docId}`, value);
177
+ written++;
178
+ }
179
+ // Write metadata checkpoint
180
+ const meta = this.exportMetadataCheckpoint();
181
+ await store.put("db-meta", prefix, meta);
182
+ written++;
183
+ return written;
184
+ }
185
+ exportMetadataCheckpoint() {
186
+ const checkpoint = {
187
+ version: 1,
188
+ processedEntryCursor: this.processedEntryCursor,
189
+ index: this.index,
190
+ nextChangeSeq: this.nextChangeSeq,
191
+ };
192
+ // Serialize automergeHashToEntryId: Map<string, Map<string,string>> -> nested object
193
+ const hashMap = {};
194
+ for (const [docId, inner] of this.automergeHashToEntryId) {
195
+ hashMap[docId] = Object.fromEntries(inner);
196
+ }
197
+ checkpoint.automergeHashToEntryId = hashMap;
198
+ // For stores without cursor scan, include processedEntryIds
199
+ if (!this.supportsCursorScan(this.store)) {
200
+ checkpoint.processedEntryIds = this.processedEntryIds;
201
+ }
202
+ return new TextEncoder().encode(JSON.stringify(checkpoint));
203
+ }
204
+ /**
205
+ * Attempt to restore state from cache. Returns true on success.
206
+ */
207
+ async restoreFromCache(store) {
208
+ const prefix = this.getCachePrefix();
209
+ try {
210
+ // 1. Load metadata checkpoint
211
+ const metaBytes = await store.get("db-meta", prefix);
212
+ if (!metaBytes) {
213
+ this.logger.debug("No cache metadata found, will do full rebuild");
214
+ return false;
215
+ }
216
+ const checkpoint = JSON.parse(new TextDecoder().decode(metaBytes));
217
+ if (checkpoint.version !== 1) {
218
+ this.logger.warn(`Unknown cache version ${checkpoint.version}, ignoring cache`);
219
+ return false;
220
+ }
221
+ // 2. Restore metadata
222
+ this.processedEntryCursor = checkpoint.processedEntryCursor ?? null;
223
+ this.index = checkpoint.index ?? [];
224
+ this.nextChangeSeq = checkpoint.nextChangeSeq ?? 1;
225
+ if (checkpoint.processedEntryIds) {
226
+ this.processedEntryIds = checkpoint.processedEntryIds;
227
+ }
228
+ // Rebuild indexLookup from index
229
+ this.indexLookup.clear();
230
+ for (let i = 0; i < this.index.length; i++) {
231
+ if (typeof this.index[i].changeSeq !== "number") {
232
+ this.index[i].changeSeq = i + 1;
233
+ }
234
+ this.indexLookup.set(this.index[i].docId, i);
235
+ }
236
+ if (this.index.length > 0 && this.nextChangeSeq <= this.index.length) {
237
+ this.nextChangeSeq = Math.max(...this.index.map(e => e.changeSeq)) + 1;
238
+ }
239
+ // Restore automergeHashToEntryId
240
+ if (checkpoint.automergeHashToEntryId) {
241
+ this.automergeHashToEntryId.clear();
242
+ for (const [docId, inner] of Object.entries(checkpoint.automergeHashToEntryId)) {
243
+ this.automergeHashToEntryId.set(docId, new Map(Object.entries(inner)));
244
+ }
245
+ }
246
+ // 3. Load cached documents
247
+ const docIds = await store.list("doc");
248
+ const docPrefix = `${prefix}/`;
249
+ let restoredDocs = 0;
250
+ for (const id of docIds) {
251
+ if (restoredDocs >= this.cacheRestoreLimit) {
252
+ break;
253
+ }
254
+ if (!id.startsWith(docPrefix))
255
+ continue;
256
+ const docId = id.slice(docPrefix.length);
257
+ const docBytes = await store.get("doc", id);
258
+ if (!docBytes)
259
+ continue;
260
+ try {
261
+ const internal = this.deserializeDoc(docBytes);
262
+ this.storeCachedDocument(internal);
263
+ restoredDocs++;
264
+ }
265
+ catch (e) {
266
+ this.logger.warn(`Failed to restore cached doc ${docId}, will reload from store: ${e}`);
267
+ }
268
+ }
269
+ this.logger.info(`Restored ${restoredDocs} documents from cache for ${prefix}`);
270
+ return true;
271
+ }
272
+ catch (e) {
273
+ this.logger.warn(`Cache restore failed, will do full rebuild: ${e}`);
274
+ return false;
275
+ }
276
+ }
277
+ deserializeDoc(value) {
278
+ const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
279
+ const headerLen = view.getUint32(0, false);
280
+ const headerBytes = value.slice(4, 4 + headerLen);
281
+ const amBinary = value.slice(4 + headerLen);
282
+ const header = JSON.parse(new TextDecoder().decode(headerBytes));
283
+ const doc = automerge_adapter_1.Automerge.load(amBinary);
284
+ return {
285
+ id: header.id,
286
+ doc,
287
+ createdAt: header.createdAt,
288
+ lastModified: header.lastModified,
289
+ decryptionKeyId: header.decryptionKeyId,
290
+ isDeleted: header.isDeleted,
291
+ };
292
+ }
103
293
  /**
104
294
  * Get or import a CryptoKey for signature verification, with caching.
105
295
  * This avoids re-importing the same public key multiple times.
@@ -141,18 +331,18 @@ class BaseMindooDB {
141
331
  /**
142
332
  * Compare two index entries for sorting.
143
333
  * Returns negative if a < b, positive if a > b, 0 if equal.
144
- * Sorts by lastModified first, then by docId for uniqueness.
334
+ * Sorts by deterministic changeSeq first, then by docId for uniqueness.
145
335
  */
146
336
  compareIndexEntries(a, b) {
147
- if (a.lastModified !== b.lastModified) {
148
- return a.lastModified - b.lastModified;
337
+ if (a.changeSeq !== b.changeSeq) {
338
+ return a.changeSeq - b.changeSeq;
149
339
  }
150
340
  return a.docId.localeCompare(b.docId);
151
341
  }
152
342
  /**
153
343
  * Update the index entry for a document.
154
- * When a document changes, it's removed from its current position and inserted
155
- * at the correct sorted position to maintain order by (lastModified, docId).
344
+ * When a document changes, it gets a new monotonic sequence and is moved to
345
+ * maintain order by (changeSeq, docId).
156
346
  *
157
347
  * Optimized to only update lookup map for entries that actually moved.
158
348
  *
@@ -161,12 +351,14 @@ class BaseMindooDB {
161
351
  * @param isDeleted Whether the document is deleted
162
352
  */
163
353
  updateIndex(docId, lastModified, isDeleted) {
164
- const newEntry = { docId, lastModified, isDeleted };
354
+ const startedAt = Date.now();
355
+ const assignedSeq = this.nextChangeSeq;
356
+ const newEntry = { docId, changeSeq: assignedSeq, lastModified, isDeleted };
165
357
  const existingIndex = this.indexLookup.get(docId);
166
358
  // Check if the entry already exists and hasn't changed position
167
359
  if (existingIndex !== undefined) {
168
360
  const existingEntry = this.index[existingIndex];
169
- // If position hasn't changed (same lastModified and docId), no update needed
361
+ // If only metadata flags stayed identical and caller replays same state, skip.
170
362
  if (existingEntry.lastModified === lastModified && existingEntry.isDeleted === isDeleted) {
171
363
  return; // No change needed
172
364
  }
@@ -199,11 +391,17 @@ class BaseMindooDB {
199
391
  }
200
392
  // Insert at the correct position
201
393
  this.index.splice(insertIndex, 0, newEntry);
394
+ this.nextChangeSeq = assignedSeq + 1;
202
395
  // Update lookup map for entries from insertion point onwards
203
396
  // Only update entries that actually moved (from insertIndex to end)
204
397
  for (let i = insertIndex; i < this.index.length; i++) {
205
398
  this.indexLookup.set(this.index[i].docId, i);
206
399
  }
400
+ this.performanceCallback?.onIndexUpdate?.({
401
+ docId,
402
+ operation: existingIndex === undefined ? "insert" : "update",
403
+ time: Date.now() - startedAt,
404
+ });
207
405
  }
208
406
  /**
209
407
  * Register an automerge hash -> entry ID mapping for a document.
@@ -251,9 +449,20 @@ class BaseMindooDB {
251
449
  }
252
450
  /**
253
451
  * Initialize the database instance.
452
+ * If a cache store is available, attempts to restore from cache first,
453
+ * then processes only the delta. Falls back to full rebuild on cache miss.
254
454
  */
255
455
  async initialize() {
256
456
  this.logger.info(`Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
457
+ const cacheStore = this.cacheManager?.getStore();
458
+ if (cacheStore) {
459
+ const restored = await this.restoreFromCache(cacheStore);
460
+ if (restored) {
461
+ this.logger.info("Restored from cache, processing delta only");
462
+ await this.syncStoreChanges();
463
+ return;
464
+ }
465
+ }
257
466
  await this.syncStoreChanges();
258
467
  }
259
468
  /**
@@ -262,13 +471,22 @@ class BaseMindooDB {
262
471
  * On first call (when processedEntryIds is empty), it will process all entries.
263
472
  */
264
473
  async syncStoreChanges() {
474
+ const syncStartedAt = Date.now();
265
475
  this.logger.debug(`Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
266
476
  this.logger.debug(`Already processed ${this.processedEntryIds.length} entry IDs`);
267
477
  // Find new entries that we haven't processed yet
268
- const newEntryMetadata = await this.store.findNewEntries(this.processedEntryIds);
478
+ const newEntryMetadata = await this.getNewEntryMetadataForSync();
269
479
  this.logger.debug(`Found ${newEntryMetadata.length} new entries`);
270
480
  if (newEntryMetadata.length === 0) {
271
481
  this.logger.debug(`No new entries to process`);
482
+ this.performanceCallback?.onSyncOperation?.({
483
+ operation: "findNewEntries",
484
+ time: Date.now() - syncStartedAt,
485
+ details: {
486
+ newEntryCount: 0,
487
+ processedEntryCount: this.processedEntryIds.length,
488
+ },
489
+ });
272
490
  return;
273
491
  }
274
492
  // Group new entries by document ID
@@ -285,10 +503,11 @@ class BaseMindooDB {
285
503
  this.logger.debug(`Processing ${entriesByDoc.size} documents with new entries`);
286
504
  // Helper function to process a single document
287
505
  const processDocument = async (docId, entryMetadataList) => {
506
+ const processStartedAt = Date.now();
288
507
  try {
289
508
  this.logger.debug(`===== Processing document ${docId} with ${entryMetadataList.length} new entry(s) in syncStoreChanges =====`);
290
509
  // Check if document is cached
291
- const cachedDoc = this.docCache.get(docId);
510
+ const cachedDoc = this.getCachedDocument(docId);
292
511
  let updatedDoc = null;
293
512
  if (cachedDoc) {
294
513
  // Document is cached - try incremental update
@@ -316,16 +535,37 @@ class BaseMindooDB {
316
535
  }
317
536
  }
318
537
  else {
319
- // Document not cached - load from scratch
320
- this.logger.debug(`Document ${docId} not in cache, loading from store`);
321
- updatedDoc = await this.loadDocumentInternal(docId);
322
- if (updatedDoc) {
323
- this.logger.debug(`Successfully loaded document ${docId}, updating index`);
324
- this.updateIndex(docId, updatedDoc.lastModified, updatedDoc.isDeleted);
325
- this.logger.debug(`Updated index for document ${docId} (lastModified: ${updatedDoc.lastModified}, isDeleted: ${updatedDoc.isDeleted})`);
538
+ // Metadata-first startup: avoid full materialization for uncached docs.
539
+ // Index based on doc-lifecycle entries (create/change/delete) plus
540
+ // doc_snapshot which may be the sole entry after a dense sync.
541
+ // Attachment-only batches should not trigger document index updates.
542
+ const docLifecycleEntries = entryMetadataList.filter((e) => e.entryType === "doc_create" ||
543
+ e.entryType === "doc_change" ||
544
+ e.entryType === "doc_delete" ||
545
+ e.entryType === "doc_snapshot");
546
+ if (docLifecycleEntries.length === 0) {
547
+ this.logger.debug(`Skipping metadata-first index for doc ${docId} — no document lifecycle entries (${entryMetadataList.length} attachment/other entries only)`);
326
548
  }
327
549
  else {
328
- this.logger.warn(`Document ${docId} returned null from loadDocumentInternal`);
550
+ // Before indexing, verify the user has the decryption key so that
551
+ // documents the user cannot access do not appear in getAllDocumentIds.
552
+ const representativeEntry = docLifecycleEntries[0];
553
+ const keyAvailable = await this.tenant.hasDecryptionKey(representativeEntry.decryptionKeyId);
554
+ if (!keyAvailable) {
555
+ this.logger.debug(`Skipping metadata-first index for doc ${docId} — decryption key "${representativeEntry.decryptionKeyId}" not available`);
556
+ }
557
+ else {
558
+ const mutationEntries = docLifecycleEntries.filter((e) => e.entryType === "doc_create" ||
559
+ e.entryType === "doc_change" ||
560
+ e.entryType === "doc_delete");
561
+ // Snapshots compact replay history but should not change the user-visible
562
+ // modification time when the original create/change/delete entries still exist.
563
+ const entriesForLastModified = mutationEntries.length > 0 ? mutationEntries : docLifecycleEntries;
564
+ const lastModified = Math.max(...entriesForLastModified.map((e) => e.createdAt));
565
+ const isDeleted = mutationEntries.some((e) => e.entryType === "doc_delete");
566
+ this.updateIndex(docId, lastModified, isDeleted);
567
+ this.logger.debug(`Metadata-first update for uncached doc ${docId} (lastModified: ${lastModified}, isDeleted: ${isDeleted})`);
568
+ }
329
569
  }
330
570
  }
331
571
  }
@@ -341,31 +581,332 @@ class BaseMindooDB {
341
581
  // Re-throw the error so we can see what's happening in the test
342
582
  throw error;
343
583
  }
584
+ finally {
585
+ this.performanceCallback?.onSyncOperation?.({
586
+ operation: "processDocument",
587
+ time: Date.now() - processStartedAt,
588
+ details: {
589
+ docId,
590
+ entryCount: entryMetadataList.length,
591
+ cacheHit: this.docCache.has(docId),
592
+ },
593
+ });
594
+ }
344
595
  };
345
- // Process documents in parallel with concurrency limit (default: 10)
346
- const maxConcurrency = 10;
347
- const documentEntries = Array.from(entriesByDoc.entries());
348
- // Process in batches to limit concurrency
349
- const results = [];
350
- for (let i = 0; i < documentEntries.length; i += maxConcurrency) {
351
- const batch = documentEntries.slice(i, i + maxConcurrency);
352
- const batchResults = await Promise.allSettled(batch.map(([docId, entryMetadataList]) => processDocument(docId, entryMetadataList)));
353
- results.push(...batchResults);
354
- }
355
- // Check for errors (excluding SymmetricKeyNotFoundError which is handled gracefully)
356
- for (let i = 0; i < results.length; i++) {
357
- const result = results[i];
358
- if (result.status === 'rejected') {
359
- const error = result.reason;
360
- if (!(error instanceof errors_1.SymmetricKeyNotFoundError)) {
361
- // Re-throw non-key errors
362
- throw error;
363
- }
596
+ const documentEntries = Array.from(entriesByDoc.entries())
597
+ .sort((a, b) => {
598
+ const aMinCreatedAt = Math.min(...a[1].map((e) => e.createdAt));
599
+ const bMinCreatedAt = Math.min(...b[1].map((e) => e.createdAt));
600
+ if (aMinCreatedAt !== bMinCreatedAt) {
601
+ return aMinCreatedAt - bMinCreatedAt;
364
602
  }
603
+ return a[0].localeCompare(b[0]);
604
+ });
605
+ // Deterministic sequential processing ensures stable changefeed ordering.
606
+ for (const [docId, entryMetadataList] of documentEntries) {
607
+ await processDocument(docId, entryMetadataList);
365
608
  }
366
609
  // Append new entry IDs to our processed list
367
610
  this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
611
+ this.cacheMetaDirty = true;
612
+ this.cacheManager?.markDirty();
368
613
  this.logger.debug(`Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
614
+ this.performanceCallback?.onSyncOperation?.({
615
+ operation: "findNewEntries",
616
+ time: Date.now() - syncStartedAt,
617
+ details: {
618
+ newEntryCount: newEntryMetadata.length,
619
+ documentCount: entriesByDoc.size,
620
+ indexSize: this.index.length,
621
+ },
622
+ });
623
+ }
624
+ supportsCursorScan(store) {
625
+ return typeof store.scanEntriesSince === "function";
626
+ }
627
+ async scanAllMetadata(store, filters) {
628
+ if (!this.supportsCursorScan(store)) {
629
+ if (filters?.docId) {
630
+ return store.findNewEntriesForDoc([], filters.docId);
631
+ }
632
+ return store.findNewEntries([]);
633
+ }
634
+ const all = [];
635
+ let cursor = null;
636
+ while (true) {
637
+ const page = await store.scanEntriesSince(cursor, 1000, filters);
638
+ all.push(...page.entries);
639
+ cursor = page.nextCursor;
640
+ if (!page.hasMore) {
641
+ break;
642
+ }
643
+ }
644
+ return all;
645
+ }
646
+ async getNewEntryMetadataForSync() {
647
+ const startedAt = Date.now();
648
+ if (!this.supportsCursorScan(this.store)) {
649
+ const result = await this.store.findNewEntries(this.processedEntryIds);
650
+ this.performanceCallback?.onSyncOperation?.({
651
+ operation: "findNewEntries",
652
+ time: Date.now() - startedAt,
653
+ details: {
654
+ mode: "knownIds",
655
+ resultCount: result.length,
656
+ processedEntryCount: this.processedEntryIds.length,
657
+ },
658
+ });
659
+ return result;
660
+ }
661
+ const allNew = [];
662
+ let cursor = this.processedEntryCursor;
663
+ while (true) {
664
+ const page = await this.store.scanEntriesSince(cursor, 1000);
665
+ allNew.push(...page.entries);
666
+ cursor = page.nextCursor;
667
+ if (!page.hasMore) {
668
+ break;
669
+ }
670
+ }
671
+ this.processedEntryCursor = cursor;
672
+ this.performanceCallback?.onSyncOperation?.({
673
+ operation: "findNewEntries",
674
+ time: Date.now() - startedAt,
675
+ details: {
676
+ mode: "cursorScan",
677
+ resultCount: allNew.length,
678
+ cursor,
679
+ },
680
+ });
681
+ return allNew;
682
+ }
683
+ /**
684
+ * Fetch the target store's bloom filter summary, returning null if
685
+ * unsupported or on error (callers fall back to exact checks).
686
+ */
687
+ async getTargetBloomSummary(targetStore) {
688
+ if (typeof targetStore.getIdBloomSummary !== "function") {
689
+ return null;
690
+ }
691
+ try {
692
+ return await targetStore.getIdBloomSummary();
693
+ }
694
+ catch (error) {
695
+ this.logger.warn("Failed to get bloom summary from target store, falling back to exact checks", error);
696
+ return null;
697
+ }
698
+ }
699
+ /**
700
+ * From a list of candidate IDs, return only those the target store is
701
+ * missing. Uses bloom-filter pre-screening when available, then falls
702
+ * back to exact `hasEntries` for the uncertain set.
703
+ */
704
+ async filterMissingIds(targetStore, candidateIds, bloom) {
705
+ let definitelyMissing = [];
706
+ let maybeExisting = candidateIds;
707
+ if (bloom) {
708
+ definitelyMissing = [];
709
+ maybeExisting = [];
710
+ for (const id of candidateIds) {
711
+ if ((0, bloom_1.bloomMightContainId)(bloom, id)) {
712
+ maybeExisting.push(id);
713
+ }
714
+ else {
715
+ definitelyMissing.push(id);
716
+ }
717
+ }
718
+ }
719
+ let missingIds = definitelyMissing;
720
+ if (maybeExisting.length > 0) {
721
+ const existing = await targetStore.hasEntries(maybeExisting);
722
+ const existingSet = new Set(existing);
723
+ missingIds = missingIds.concat(maybeExisting.filter((id) => !existingSet.has(id)));
724
+ }
725
+ return missingIds;
726
+ }
727
+ async syncEntriesFromStore(sourceStore, targetStore, options) {
728
+ if (options?.mode === "dense") {
729
+ return this.syncEntriesFromStoreDense(sourceStore, targetStore, options);
730
+ }
731
+ let transferred = 0;
732
+ let scanned = 0;
733
+ const onProgress = options?.onProgress;
734
+ const pageSize = options?.pageSize ?? 1000;
735
+ const targetBloom = await this.getTargetBloomSummary(targetStore);
736
+ const totalSourceEstimate = targetBloom?.totalIds;
737
+ if (this.supportsCursorScan(sourceStore)) {
738
+ onProgress?.({
739
+ phase: 'preparing',
740
+ message: 'Preparing to sync entries...',
741
+ transferredEntries: 0,
742
+ scannedEntries: 0,
743
+ totalSourceEntries: totalSourceEstimate,
744
+ });
745
+ let cursor = null;
746
+ let currentPage = 0;
747
+ while (true) {
748
+ if (options?.signal?.aborted) {
749
+ return { transferred, cancelled: true };
750
+ }
751
+ const page = await sourceStore.scanEntriesSince(cursor, pageSize);
752
+ currentPage++;
753
+ scanned += page.entries.length;
754
+ if (page.entries.length > 0) {
755
+ const ids = page.entries.map((m) => m.id);
756
+ const missingIds = await this.filterMissingIds(targetStore, ids, targetBloom);
757
+ if (missingIds.length > 0) {
758
+ const missingEntries = await sourceStore.getEntries(missingIds);
759
+ await targetStore.putEntries(missingEntries);
760
+ transferred += missingEntries.length;
761
+ }
762
+ }
763
+ onProgress?.({
764
+ phase: 'transferring',
765
+ message: `Transferred ${transferred} entries (page ${currentPage}, scanned ${scanned})`,
766
+ transferredEntries: transferred,
767
+ scannedEntries: scanned,
768
+ totalSourceEntries: totalSourceEstimate,
769
+ currentPage,
770
+ });
771
+ cursor = page.nextCursor;
772
+ if (!page.hasMore) {
773
+ break;
774
+ }
775
+ }
776
+ return { transferred, cancelled: false };
777
+ }
778
+ onProgress?.({
779
+ phase: 'preparing',
780
+ message: 'Finding new entries...',
781
+ transferredEntries: 0,
782
+ scannedEntries: 0,
783
+ });
784
+ const targetIds = await targetStore.getAllIds();
785
+ const sourceNewMetadata = await sourceStore.findNewEntries(targetIds);
786
+ if (sourceNewMetadata.length === 0) {
787
+ return { transferred: 0, cancelled: false };
788
+ }
789
+ if (options?.signal?.aborted) {
790
+ return { transferred: 0, cancelled: true };
791
+ }
792
+ onProgress?.({
793
+ phase: 'transferring',
794
+ message: `Transferring ${sourceNewMetadata.length} entries...`,
795
+ transferredEntries: 0,
796
+ scannedEntries: sourceNewMetadata.length,
797
+ totalSourceEntries: sourceNewMetadata.length,
798
+ });
799
+ const sourceEntries = await sourceStore.getEntries(sourceNewMetadata.map((m) => m.id));
800
+ await targetStore.putEntries(sourceEntries);
801
+ onProgress?.({
802
+ phase: 'transferring',
803
+ message: `Transferred ${sourceEntries.length} entries`,
804
+ transferredEntries: sourceEntries.length,
805
+ scannedEntries: sourceNewMetadata.length,
806
+ totalSourceEntries: sourceNewMetadata.length,
807
+ });
808
+ return { transferred: sourceEntries.length, cancelled: false };
809
+ }
810
+ /**
811
+ * Dense sync: transfer only the entries required to reconstruct the latest
812
+ * state of each document, using the batch materialization planner to skip
813
+ * historical entries already superseded by snapshots.
814
+ *
815
+ * Algorithm:
816
+ * 1. Discover all documents on the source via `doc_create` metadata.
817
+ * 2. Also fetch `doc_delete` metadata so deletion markers are transferred.
818
+ * 3. Ask the source's batch planner for the optimal replay set per document.
819
+ * 4. Merge all entry IDs: doc_create + doc_delete + snapshot + uncovered changes.
820
+ * 5. Filter out IDs the target already has (bloom + exact check).
821
+ * 6. Transfer only the missing entries.
822
+ *
823
+ * Attachment chunks are intentionally skipped — they are not part of the
824
+ * Automerge document DAG and can be fetched on demand when accessed.
825
+ */
826
+ async syncEntriesFromStoreDense(sourceStore, targetStore, options) {
827
+ const onProgress = options?.onProgress;
828
+ onProgress?.({
829
+ phase: "preparing",
830
+ message: "Dense sync: discovering documents on source...",
831
+ transferredEntries: 0,
832
+ scannedEntries: 0,
833
+ });
834
+ if (options?.signal?.aborted) {
835
+ return { transferred: 0, cancelled: true };
836
+ }
837
+ // ── Phase 1: discover documents ──────────────────────────────────
838
+ const docCreateEntries = await sourceStore.findEntries("doc_create", null, null);
839
+ const docDeleteEntries = await sourceStore.findEntries("doc_delete", null, null);
840
+ const docIds = [...new Set(docCreateEntries.map((e) => e.docId))];
841
+ this.logger.info(`Dense sync: found ${docIds.length} documents on source`);
842
+ if (docIds.length === 0) {
843
+ return { transferred: 0, cancelled: false };
844
+ }
845
+ if (options?.signal?.aborted) {
846
+ return { transferred: 0, cancelled: true };
847
+ }
848
+ // ── Phase 2: batch plan on source ────────────────────────────────
849
+ onProgress?.({
850
+ phase: "planning",
851
+ message: `Dense sync: computing materialization plans for ${docIds.length} documents...`,
852
+ transferredEntries: 0,
853
+ scannedEntries: docIds.length,
854
+ });
855
+ const batchPlan = await sourceStore.planDocumentMaterializationBatch(docIds);
856
+ // ── Phase 3: collect needed entry IDs ────────────────────────────
857
+ const neededIds = new Set();
858
+ for (const entry of docCreateEntries) {
859
+ neededIds.add(entry.id);
860
+ }
861
+ for (const entry of docDeleteEntries) {
862
+ neededIds.add(entry.id);
863
+ }
864
+ for (const plan of batchPlan.plans) {
865
+ if (plan.snapshotEntryId) {
866
+ neededIds.add(plan.snapshotEntryId);
867
+ }
868
+ for (const id of plan.entryIdsToApply) {
869
+ neededIds.add(id);
870
+ }
871
+ }
872
+ this.logger.info(`Dense sync: planner identified ${neededIds.size} required entries ` +
873
+ `(from ${docIds.length} documents)`);
874
+ if (options?.signal?.aborted) {
875
+ return { transferred: 0, cancelled: true };
876
+ }
877
+ // ── Phase 4: filter out entries the target already has ───────────
878
+ const allNeededArray = Array.from(neededIds);
879
+ const targetBloom = await this.getTargetBloomSummary(targetStore);
880
+ const missingIds = await this.filterMissingIds(targetStore, allNeededArray, targetBloom);
881
+ this.logger.info(`Dense sync: ${missingIds.length} entries to transfer ` +
882
+ `(${allNeededArray.length - missingIds.length} already present)`);
883
+ if (missingIds.length === 0) {
884
+ return { transferred: 0, cancelled: false };
885
+ }
886
+ if (options?.signal?.aborted) {
887
+ return { transferred: 0, cancelled: true };
888
+ }
889
+ // ── Phase 5: transfer missing entries in pages ───────────────────
890
+ const pageSize = options?.pageSize ?? 500;
891
+ let transferred = 0;
892
+ for (let offset = 0; offset < missingIds.length; offset += pageSize) {
893
+ if (options?.signal?.aborted) {
894
+ return { transferred, cancelled: true };
895
+ }
896
+ const batch = missingIds.slice(offset, offset + pageSize);
897
+ const entries = await sourceStore.getEntries(batch);
898
+ await targetStore.putEntries(entries);
899
+ transferred += entries.length;
900
+ onProgress?.({
901
+ phase: "transferring",
902
+ message: `Dense sync: transferred ${transferred}/${missingIds.length} entries`,
903
+ transferredEntries: transferred,
904
+ scannedEntries: allNeededArray.length,
905
+ totalSourceEntries: allNeededArray.length,
906
+ });
907
+ }
908
+ this.logger.info(`Dense sync complete: transferred ${transferred} entries`);
909
+ return { transferred, cancelled: false };
369
910
  }
370
911
  getStore() {
371
912
  return this.store;
@@ -407,18 +948,18 @@ class BaseMindooDB {
407
948
  const docId = (0, uuid_1.v7)();
408
949
  this.logger.debug(`Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
409
950
  // Create initial Automerge document
410
- const initialDoc = Automerge.init();
951
+ const initialDoc = automerge_adapter_1.Automerge.init();
411
952
  // Create the first change
412
953
  const now = Date.now();
413
954
  this.logger.debug(`Creating initial Automerge change for document ${docId}`);
414
955
  let newDoc;
415
956
  try {
416
- newDoc = Automerge.change(initialDoc, (doc) => {
957
+ newDoc = automerge_adapter_1.Automerge.change(initialDoc, (doc) => {
417
958
  // Store metadata in the document payload
418
959
  // We need to modify the document to ensure a change is created
419
960
  doc._attachments = [];
420
961
  });
421
- this.logger.debug(`Successfully created Automerge change, document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
962
+ this.logger.debug(`Successfully created Automerge change, document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
422
963
  }
423
964
  catch (error) {
424
965
  this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
@@ -426,7 +967,7 @@ class BaseMindooDB {
426
967
  }
427
968
  // Get the change bytes from the document
428
969
  this.logger.debug(`Getting change bytes from document ${docId}`);
429
- const changeBytes = Automerge.getLastLocalChange(newDoc);
970
+ const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
430
971
  if (!changeBytes) {
431
972
  throw new Error("Failed to get change bytes from Automerge document");
432
973
  }
@@ -435,7 +976,7 @@ class BaseMindooDB {
435
976
  this.logger.debug(`Decoding change to get hash and dependencies`);
436
977
  let decodedChange;
437
978
  try {
438
- decodedChange = Automerge.decodeChange(changeBytes);
979
+ decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
439
980
  this.logger.debug(`Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
440
981
  }
441
982
  catch (error) {
@@ -504,8 +1045,9 @@ class BaseMindooDB {
504
1045
  isDeleted: false,
505
1046
  };
506
1047
  // Update cache and index
507
- this.docCache.set(docId, internalDoc);
1048
+ this.storeCachedDocument(internalDoc);
508
1049
  this.updateIndex(docId, internalDoc.lastModified, false);
1050
+ this.markDocDirty(docId);
509
1051
  this.logger.info(`Document ${docId} created successfully`);
510
1052
  this.logger.debug(`Document ${docId} cached and indexed (lastModified: ${internalDoc.lastModified})`);
511
1053
  return this.wrapDocument(internalDoc);
@@ -521,20 +1063,28 @@ class BaseMindooDB {
521
1063
  return this.wrapDocument(internalDoc);
522
1064
  }
523
1065
  async getDocumentAtTimestamp(docId, timestamp) {
1066
+ const startedAt = Date.now();
524
1067
  this.logger.debug(`Getting document ${docId} at timestamp ${timestamp}`);
525
1068
  // Get all entry metadata for this document
526
- const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
1069
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
527
1070
  // Filter entries up to the timestamp
528
1071
  const relevantEntries = allEntryMetadata
529
1072
  .filter(em => em.createdAt <= timestamp)
530
1073
  .sort((a, b) => a.createdAt - b.createdAt);
531
1074
  if (relevantEntries.length === 0) {
1075
+ this.performanceCallback?.onHistoryOperation?.({
1076
+ operation: "getDocumentAtTimestamp",
1077
+ docId,
1078
+ time: Date.now() - startedAt,
1079
+ scannedEntries: 0,
1080
+ bounded: false,
1081
+ });
532
1082
  return null; // Document didn't exist at that time
533
1083
  }
534
1084
  // Load entries
535
1085
  const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
536
1086
  // Apply changes in order
537
- let doc = Automerge.init();
1087
+ let doc = automerge_adapter_1.Automerge.init();
538
1088
  for (const entryData of entries) {
539
1089
  // Admin-only mode: only accept entries signed by the admin key
540
1090
  if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
@@ -551,7 +1101,7 @@ class BaseMindooDB {
551
1101
  // Decrypt payload (only after signature verification passes)
552
1102
  const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
553
1103
  // Apply change using loadIncremental - this is the recommended way for binary change data
554
- doc = Automerge.loadIncremental(doc, decryptedPayload);
1104
+ doc = automerge_adapter_1.Automerge.loadIncremental(doc, decryptedPayload);
555
1105
  // Register the automerge hash -> entry ID mapping for future dependency resolution
556
1106
  const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
557
1107
  if (parsed) {
@@ -576,12 +1126,20 @@ class BaseMindooDB {
576
1126
  decryptionKeyId,
577
1127
  isDeleted,
578
1128
  };
1129
+ this.performanceCallback?.onHistoryOperation?.({
1130
+ operation: "getDocumentAtTimestamp",
1131
+ docId,
1132
+ time: Date.now() - startedAt,
1133
+ scannedEntries: relevantEntries.length,
1134
+ returnedEntries: 1,
1135
+ bounded: false,
1136
+ });
579
1137
  return this.wrapDocument(internalDoc);
580
1138
  }
581
1139
  async *iterateDocumentHistory(docId) {
582
1140
  this.logger.debug(`Iterating document history for ${docId}`);
583
1141
  // Get all entry metadata for this document
584
- const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
1142
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
585
1143
  // Filter to only doc_create, doc_change, and doc_delete entries (exclude snapshots)
586
1144
  const relevantEntries = allEntryMetadata
587
1145
  .filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete")
@@ -623,17 +1181,17 @@ class BaseMindooDB {
623
1181
  this.logger.warn(`First entry is not doc_create, skipping`);
624
1182
  continue;
625
1183
  }
626
- currentDoc = Automerge.init();
1184
+ currentDoc = automerge_adapter_1.Automerge.init();
627
1185
  createdAt = entryMetadata.createdAt;
628
1186
  decryptionKeyId = entryMetadata.decryptionKeyId;
629
1187
  }
630
1188
  // Check document heads before applying change (for non-first entries)
631
- const headsBefore = isFirstEntry ? null : (currentDoc ? Automerge.getHeads(currentDoc) : null);
1189
+ const headsBefore = isFirstEntry ? null : (currentDoc ? automerge_adapter_1.Automerge.getHeads(currentDoc) : null);
632
1190
  // Apply change using loadIncremental
633
1191
  if (currentDoc === null) {
634
1192
  throw new Error("currentDoc should not be null at this point");
635
1193
  }
636
- currentDoc = Automerge.loadIncremental(currentDoc, decryptedPayload);
1194
+ currentDoc = automerge_adapter_1.Automerge.loadIncremental(currentDoc, decryptedPayload);
637
1195
  // Check if document actually changed by comparing heads
638
1196
  // For first entry (doc_create), always yield since it's the initial creation
639
1197
  // For delete entries, always yield
@@ -643,7 +1201,7 @@ class BaseMindooDB {
643
1201
  shouldYield = true;
644
1202
  }
645
1203
  else {
646
- const headsAfter = Automerge.getHeads(currentDoc);
1204
+ const headsAfter = automerge_adapter_1.Automerge.getHeads(currentDoc);
647
1205
  const headsChanged = headsBefore !== null && JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
648
1206
  shouldYield = headsChanged;
649
1207
  }
@@ -655,7 +1213,7 @@ class BaseMindooDB {
655
1213
  // Only yield if document actually changed (or if this is create/delete entry)
656
1214
  if (shouldYield) {
657
1215
  // Clone the document to ensure independence
658
- const clonedDoc = Automerge.clone(currentDoc);
1216
+ const clonedDoc = automerge_adapter_1.Automerge.clone(currentDoc);
659
1217
  // Create internal doc representation
660
1218
  const internalDoc = {
661
1219
  id: docId,
@@ -679,6 +1237,46 @@ class BaseMindooDB {
679
1237
  }
680
1238
  }
681
1239
  }
1240
+ async getDocumentHistoryPage(docId, options) {
1241
+ const startedAt = Date.now();
1242
+ const limit = Math.max(1, Math.floor(options?.limit ?? 100));
1243
+ const offset = Math.max(0, Math.floor(options?.cursor?.offset ?? 0));
1244
+ // This API stays metadata-only so large history views can page cheaply
1245
+ // without reconstructing every historical Automerge document state.
1246
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
1247
+ const relevantEntries = allEntryMetadata
1248
+ .filter((em) => em.entryType === "doc_create" ||
1249
+ em.entryType === "doc_change" ||
1250
+ em.entryType === "doc_delete")
1251
+ .sort((a, b) => a.createdAt - b.createdAt);
1252
+ // Cursor paging is offset-based because the result is a bounded timeline view
1253
+ // over the doc's sorted change metadata, not a resumable store scan cursor.
1254
+ const slice = relevantEntries.slice(offset, offset + limit);
1255
+ const entries = slice.map((entry) => ({
1256
+ entryId: entry.id,
1257
+ entryType: entry.entryType,
1258
+ changeCreatedAt: entry.createdAt,
1259
+ changeCreatedByPublicKey: entry.createdByPublicKey,
1260
+ dependencyIds: [...entry.dependencyIds],
1261
+ isDeleted: entry.entryType === "doc_delete",
1262
+ }));
1263
+ const nextOffset = offset + entries.length;
1264
+ const hasMore = nextOffset < relevantEntries.length;
1265
+ const result = {
1266
+ entries,
1267
+ nextCursor: hasMore ? { offset: nextOffset } : null,
1268
+ hasMore,
1269
+ };
1270
+ this.performanceCallback?.onHistoryOperation?.({
1271
+ operation: "getDocumentHistoryPage",
1272
+ docId,
1273
+ time: Date.now() - startedAt,
1274
+ scannedEntries: relevantEntries.length,
1275
+ returnedEntries: entries.length,
1276
+ bounded: true,
1277
+ });
1278
+ return result;
1279
+ }
682
1280
  async getAllDocumentIds() {
683
1281
  // Return all non-deleted document IDs from index
684
1282
  const docIds = [];
@@ -690,10 +1288,14 @@ class BaseMindooDB {
690
1288
  return docIds;
691
1289
  }
692
1290
  async getAllDocumentIdsAtTimestamp(timestamp) {
1291
+ // findEntries() uses an exclusive upper bound (createdAt < creationDateUntil).
1292
+ // Query up to timestamp + 1 so entries created exactly at `timestamp` are included
1293
+ // for the strict checks below (createTime < timestamp, deleteTime > timestamp).
1294
+ const upperBoundExclusive = timestamp === Number.MAX_SAFE_INTEGER ? timestamp : timestamp + 1;
693
1295
  // Efficiently query for doc_create and doc_delete entries before the timestamp
694
1296
  const [creates, deletes] = await Promise.all([
695
- this.store.findEntries("doc_create", null, timestamp),
696
- this.store.findEntries("doc_delete", null, timestamp)
1297
+ this.store.findEntries("doc_create", null, upperBoundExclusive),
1298
+ this.store.findEntries("doc_delete", null, upperBoundExclusive)
697
1299
  ]);
698
1300
  // Build sets of docIds for efficient lookup
699
1301
  const createdDocIds = new Set();
@@ -722,9 +1324,9 @@ class BaseMindooDB {
722
1324
  const createTime = createTimes.get(docId);
723
1325
  const deleteTime = deleteTimes.get(docId);
724
1326
  // Document exists at timestamp if:
725
- // 1. It was created before the timestamp
1327
+ // 1. It was created at or before the timestamp
726
1328
  // 2. Either it was never deleted, or it was deleted after the timestamp
727
- if (createTime < timestamp) {
1329
+ if (createTime <= timestamp) {
728
1330
  if (!deleteTime || deleteTime > timestamp) {
729
1331
  docIds.push(docId);
730
1332
  }
@@ -761,24 +1363,26 @@ class BaseMindooDB {
761
1363
  if (!internalDoc || internalDoc.isDeleted) {
762
1364
  throw new Error(`Document ${docId} not found or already deleted`);
763
1365
  }
1366
+ const now = Date.now();
764
1367
  // Create deletion change by clearing all fields from the document
765
1368
  // This ensures Automerge produces actual change bytes
766
1369
  // The deletion is also tracked via the "doc_delete" type entry in the append-only store
767
- const newDoc = Automerge.change(internalDoc.doc, (doc) => {
1370
+ const newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (doc) => {
768
1371
  // Remove all fields from the document
769
1372
  for (const key of Object.keys(doc)) {
770
1373
  delete doc[key];
771
1374
  }
772
1375
  // Mark as deleted for clarity
773
1376
  doc._deleted = true;
1377
+ doc._lastModified = now;
774
1378
  });
775
1379
  // Get the change bytes from the document
776
- const changeBytes = Automerge.getLastLocalChange(newDoc);
1380
+ const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
777
1381
  if (!changeBytes) {
778
1382
  throw new Error("Failed to get change bytes from Automerge document");
779
1383
  }
780
1384
  // Decode the change to get hash and dependencies
781
- const decodedChange = Automerge.decodeChange(changeBytes);
1385
+ const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
782
1386
  const automergeHash = decodedChange.hash;
783
1387
  const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
784
1388
  // Encrypt the change payload first
@@ -810,7 +1414,7 @@ class BaseMindooDB {
810
1414
  contentHash,
811
1415
  docId,
812
1416
  dependencyIds,
813
- createdAt: Date.now(),
1417
+ createdAt: now,
814
1418
  createdByPublicKey,
815
1419
  decryptionKeyId: internalDoc.decryptionKeyId,
816
1420
  signature,
@@ -828,9 +1432,10 @@ class BaseMindooDB {
828
1432
  this.registerAutomergeHashMapping(docId, automergeHash, entryId);
829
1433
  // Update cache and index
830
1434
  internalDoc.isDeleted = true;
831
- internalDoc.lastModified = Date.now();
832
- this.docCache.set(docId, internalDoc);
1435
+ internalDoc.lastModified = now;
1436
+ this.storeCachedDocument(internalDoc);
833
1437
  this.updateIndex(docId, internalDoc.lastModified, true);
1438
+ this.markDocDirty(docId);
834
1439
  this.logger.info(`Document ${docId} deleted successfully`);
835
1440
  }
836
1441
  async changeDoc(doc, changeFunc) {
@@ -854,7 +1459,7 @@ class BaseMindooDB {
854
1459
  }
855
1460
  }
856
1461
  // Get internal document from cache or load it
857
- let internalDoc = this.docCache.get(docId);
1462
+ let internalDoc = this.getCachedDocument(docId);
858
1463
  if (!internalDoc) {
859
1464
  this.logger.debug(`Document ${docId} not in cache, loading from store`);
860
1465
  const loadedDoc = await this.loadDocumentInternal(docId);
@@ -873,7 +1478,7 @@ class BaseMindooDB {
873
1478
  // Apply the change function
874
1479
  const now = Date.now();
875
1480
  this.logger.debug(`Applying change function to document ${docId}`);
876
- this.logger.debug(`Document state before change: heads=${JSON.stringify(Automerge.getHeads(internalDoc.doc))}`);
1481
+ this.logger.debug(`Document state before change: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(internalDoc.doc))}`);
877
1482
  // For async callbacks, we need to handle document modifications carefully.
878
1483
  // Automerge.change() requires synchronous modifications, so we'll:
879
1484
  // 1. Execute the async callback to do any async work (like signing)
@@ -916,8 +1521,8 @@ class BaseMindooDB {
916
1521
  pendingDeletions.delete(prop);
917
1522
  // Track the change
918
1523
  pendingChanges.set(prop, value);
919
- // Also set on the target for immediate access
920
- target[prop] = value;
1524
+ // NOTE: Don't set on target immediately - Automerge requires changes
1525
+ // to be made inside Automerge.change(). Pending changes are applied later.
921
1526
  }
922
1527
  return true;
923
1528
  },
@@ -928,8 +1533,8 @@ class BaseMindooDB {
928
1533
  pendingDeletions.add(prop);
929
1534
  // Remove from pending changes if it was there
930
1535
  pendingChanges.delete(prop);
931
- // Also delete from the target for immediate access
932
- delete target[prop];
1536
+ // NOTE: Don't delete from target immediately - Automerge requires changes
1537
+ // to be made inside Automerge.change(). Pending deletions are applied later.
933
1538
  }
934
1539
  return true;
935
1540
  },
@@ -1048,7 +1653,7 @@ class BaseMindooDB {
1048
1653
  // Now apply the collected changes synchronously in Automerge.change()
1049
1654
  let newDoc;
1050
1655
  try {
1051
- newDoc = Automerge.change(internalDoc.doc, (automergeDoc) => {
1656
+ newDoc = automerge_adapter_1.Automerge.change(internalDoc.doc, (automergeDoc) => {
1052
1657
  // Apply all pending changes (sets/updates)
1053
1658
  for (const [key, value] of pendingChanges) {
1054
1659
  automergeDoc[key] = value;
@@ -1087,7 +1692,7 @@ class BaseMindooDB {
1087
1692
  // Update lastModified timestamp
1088
1693
  automergeDoc._lastModified = now;
1089
1694
  });
1090
- this.logger.debug(`Successfully applied change function, new document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
1695
+ this.logger.debug(`Successfully applied change function, new document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(newDoc))}`);
1091
1696
  }
1092
1697
  catch (error) {
1093
1698
  this.logger.error(`Error in Automerge.change for document ${docId}:`, error);
@@ -1095,14 +1700,14 @@ class BaseMindooDB {
1095
1700
  }
1096
1701
  // Get the change bytes from the document
1097
1702
  this.logger.debug(`Getting change bytes from document ${docId}`);
1098
- const changeBytes = Automerge.getLastLocalChange(newDoc);
1703
+ const changeBytes = automerge_adapter_1.Automerge.getLastLocalChange(newDoc);
1099
1704
  if (!changeBytes) {
1100
1705
  //TODO decide if we just exit here or throw an error
1101
1706
  throw new Error("Failed to get change bytes from Automerge document");
1102
1707
  }
1103
1708
  this.logger.debug(`Got change bytes: ${changeBytes.length} bytes`);
1104
1709
  // Decode the change to get hash and dependencies
1105
- const decodedChange = Automerge.decodeChange(changeBytes);
1710
+ const decodedChange = automerge_adapter_1.Automerge.decodeChange(changeBytes);
1106
1711
  const automergeHash = decodedChange.hash;
1107
1712
  const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
1108
1713
  // Encrypt the change payload first
@@ -1133,7 +1738,7 @@ class BaseMindooDB {
1133
1738
  contentHash,
1134
1739
  docId,
1135
1740
  dependencyIds,
1136
- createdAt: Date.now(),
1741
+ createdAt: now,
1137
1742
  createdByPublicKey,
1138
1743
  decryptionKeyId: internalDoc.decryptionKeyId,
1139
1744
  signature,
@@ -1151,98 +1756,242 @@ class BaseMindooDB {
1151
1756
  this.registerAutomergeHashMapping(docId, automergeHash, entryId);
1152
1757
  // Update cache and index
1153
1758
  internalDoc.doc = newDoc;
1154
- internalDoc.lastModified = Date.now();
1155
- this.docCache.set(docId, internalDoc);
1759
+ internalDoc.lastModified = now;
1760
+ this.storeCachedDocument(internalDoc);
1156
1761
  this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted);
1762
+ this.markDocDirty(docId);
1157
1763
  this.logger.info(`Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
1764
+ // Snapshot creation runs as a best-effort background optimization.
1765
+ await this.maybeWriteSnapshotForDocument(internalDoc, useCustomKey
1766
+ ? { signingKeyPair, signingKeyPassword, createdByPublicKey }
1767
+ : { createdByPublicKey });
1158
1768
  }
1159
- async *iterateChangesSince(cursor) {
1160
- // Default to initial cursor if null is provided
1161
- const actualCursor = cursor ?? { lastModified: 0, docId: "" };
1162
- this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
1163
- // Find starting position using binary search
1164
- // We want to find the first entry that is greater than the cursor
1165
- let startIndex = 0;
1166
- if (this.index.length > 0) {
1167
- let left = 0;
1168
- let right = this.index.length - 1;
1169
- while (left <= right) {
1170
- const mid = Math.floor((left + right) / 2);
1171
- const entry = this.index[mid];
1172
- const cmp = this.compareIndexEntries({ docId: actualCursor.docId, lastModified: actualCursor.lastModified }, entry);
1173
- if (cmp < 0) {
1174
- right = mid - 1;
1175
- startIndex = mid;
1176
- }
1177
- else {
1178
- left = mid + 1;
1179
- startIndex = mid + 1;
1180
- }
1769
+ /**
1770
+ * Best-effort snapshot scheduling and writing.
1771
+ * A snapshot is written only when enough replay history has accumulated and
1772
+ * a cooldown window has elapsed, to avoid snapshot churn on hot documents.
1773
+ */
1774
+ async maybeWriteSnapshotForDocument(internalDoc, options) {
1775
+ const docId = internalDoc.id;
1776
+ try {
1777
+ const allMetadata = await this.scanAllMetadata(this.store, { docId });
1778
+ const replayEntries = allMetadata.filter((em) => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
1779
+ const snapshots = allMetadata
1780
+ .filter((em) => em.entryType === "doc_snapshot")
1781
+ .sort((a, b) => b.createdAt - a.createdAt);
1782
+ const latestSnapshot = snapshots[0] || null;
1783
+ const latestSnapshotAt = latestSnapshot?.createdAt ?? 0;
1784
+ const changesSinceSnapshot = replayEntries.filter((em) => em.createdAt > latestSnapshotAt).length;
1785
+ if (changesSinceSnapshot < this.snapshotMinChanges) {
1786
+ return;
1181
1787
  }
1182
- // If we found an exact match, start from the next entry
1183
- if (startIndex < this.index.length) {
1184
- const entry = this.index[startIndex];
1185
- if (entry.lastModified === actualCursor.lastModified && entry.docId === actualCursor.docId) {
1186
- startIndex++;
1187
- }
1788
+ if (latestSnapshot &&
1789
+ Date.now() - latestSnapshot.createdAt < this.snapshotCooldownMs) {
1790
+ return;
1791
+ }
1792
+ const headHashes = automerge_adapter_1.Automerge.getHeads(internalDoc.doc);
1793
+ const headEntryIds = this.resolveAutomergeDepsToEntryIds(docId, headHashes);
1794
+ const snapshotBytes = automerge_adapter_1.Automerge.save(internalDoc.doc);
1795
+ const encryptedPayload = await this.tenant.encryptPayload(snapshotBytes, internalDoc.decryptionKeyId);
1796
+ const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
1797
+ const pseudoSnapshotHash = `snapshot-${(0, uuid_1.v7)()}`;
1798
+ const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, pseudoSnapshotHash, headHashes, this.getSubtle());
1799
+ let signature;
1800
+ if (options.signingKeyPair && options.signingKeyPassword) {
1801
+ signature = await this.tenant.signPayloadWithKey(encryptedPayload, options.signingKeyPair, options.signingKeyPassword);
1188
1802
  }
1803
+ else {
1804
+ signature = await this.tenant.signPayload(encryptedPayload);
1805
+ }
1806
+ const snapshotEntry = {
1807
+ entryType: "doc_snapshot",
1808
+ id: entryId,
1809
+ contentHash,
1810
+ docId,
1811
+ dependencyIds: headEntryIds,
1812
+ createdAt: Date.now(),
1813
+ createdByPublicKey: options.createdByPublicKey,
1814
+ decryptionKeyId: internalDoc.decryptionKeyId,
1815
+ snapshotHeadHashes: headHashes,
1816
+ snapshotHeadEntryIds: headEntryIds,
1817
+ signature,
1818
+ originalSize: snapshotBytes.length,
1819
+ encryptedSize: encryptedPayload.length,
1820
+ encryptedData: encryptedPayload,
1821
+ };
1822
+ await this.store.putEntries([snapshotEntry]);
1823
+ this.logger.debug(`Created snapshot for document ${docId} with ${headHashes.length} heads and ${changesSinceSnapshot} changes since previous snapshot`);
1824
+ }
1825
+ catch (error) {
1826
+ this.logger.warn(`Failed to create snapshot for document ${docId}, continuing without snapshot`, error);
1827
+ }
1828
+ }
1829
+ async prefetchIterationWindow(indexSnapshot, startIndex) {
1830
+ if (this.iteratePrefetchWindowDocs <= 0) {
1831
+ return 0;
1189
1832
  }
1190
- // Pre-check which documents are in cache to optimize iteration
1191
- const uncachedDocIds = [];
1192
- for (let i = startIndex; i < this.index.length; i++) {
1193
- const entry = this.index[i];
1194
- if (!this.docCache.has(entry.docId)) {
1195
- uncachedDocIds.push(entry.docId);
1833
+ const docIds = [];
1834
+ const seen = new Set();
1835
+ // Only look ahead a bounded number of uncached docs so iteration does not
1836
+ // materialize the entire remaining changefeed tail up front.
1837
+ for (let i = startIndex; i < indexSnapshot.length && docIds.length < this.iteratePrefetchWindowDocs; i++) {
1838
+ const docId = indexSnapshot[i].docId;
1839
+ if (seen.has(docId) || this.docCache.has(docId)) {
1840
+ continue;
1196
1841
  }
1842
+ seen.add(docId);
1843
+ docIds.push(docId);
1197
1844
  }
1198
- // Prefetch uncached documents in batches (if any)
1199
- if (uncachedDocIds.length > 0) {
1200
- this.logger.debug(`Prefetching ${uncachedDocIds.length} uncached documents for iteration`);
1201
- const prefetchBatchSize = 10;
1202
- for (let i = 0; i < uncachedDocIds.length; i += prefetchBatchSize) {
1203
- const batch = uncachedDocIds.slice(i, i + prefetchBatchSize);
1204
- await Promise.all(batch.map(docId => this.loadDocumentInternal(docId).catch(err => {
1205
- this.logger.warn(`Failed to prefetch document ${docId}:`, err);
1206
- return null;
1207
- })));
1845
+ if (docIds.length === 0) {
1846
+ return 0;
1847
+ }
1848
+ // Prefetch is opportunistic: iteration can still fall back to on-demand
1849
+ // loads, so individual prefetch failures are logged and ignored here.
1850
+ await Promise.all(docIds.map((docId) => this.loadDocumentInternal(docId).catch((err) => {
1851
+ this.logger.warn(`Failed to prefetch document ${docId}:`, err);
1852
+ return null;
1853
+ })));
1854
+ return docIds.length;
1855
+ }
1856
+ getStartIndexForCursor(indexSnapshot, actualCursor) {
1857
+ let startIndex = 0;
1858
+ if (indexSnapshot.length === 0) {
1859
+ return startIndex;
1860
+ }
1861
+ const cursorSeq = typeof actualCursor.changeSeq === "number" ? actualCursor.changeSeq : 0;
1862
+ let left = 0;
1863
+ let right = indexSnapshot.length - 1;
1864
+ while (left <= right) {
1865
+ const mid = Math.floor((left + right) / 2);
1866
+ const entry = indexSnapshot[mid];
1867
+ const cursorComparable = {
1868
+ docId: actualCursor.docId,
1869
+ changeSeq: cursorSeq,
1870
+ };
1871
+ const cmp = this.compareIndexEntries(cursorComparable, entry);
1872
+ if (cmp < 0) {
1873
+ right = mid - 1;
1874
+ startIndex = mid;
1875
+ }
1876
+ else {
1877
+ left = mid + 1;
1878
+ startIndex = mid + 1;
1208
1879
  }
1209
1880
  }
1210
- // Iterate through the index and yield documents one at a time
1211
- // Documents should now be in cache from prefetching
1212
- for (let i = startIndex; i < this.index.length; i++) {
1213
- const entry = this.index[i];
1214
- try {
1215
- this.logger.debug(`Yielding document ${entry.docId} from index (lastModified: ${entry.lastModified}, isDeleted: ${entry.isDeleted})`);
1216
- // Check cache first (should be there after prefetching)
1217
- let internalDoc = this.docCache.get(entry.docId) || null;
1218
- if (!internalDoc) {
1219
- // Fallback to loading if not in cache (shouldn't happen after prefetching)
1220
- this.logger.debug(`Document ${entry.docId} not in cache, loading from store`);
1221
- internalDoc = await this.loadDocumentInternal(entry.docId);
1222
- }
1223
- if (!internalDoc) {
1224
- this.logger.warn(`Document ${entry.docId} not found, skipping`);
1225
- continue;
1226
- }
1227
- // Wrap the document (works for both deleted and non-deleted documents)
1228
- const doc = this.wrapDocument(internalDoc);
1229
- this.logger.debug(`Successfully loaded document ${entry.docId} (isDeleted: ${doc.isDeleted()})`);
1230
- // Create cursor for current document
1881
+ if (startIndex < indexSnapshot.length) {
1882
+ const entry = indexSnapshot[startIndex];
1883
+ if (entry.changeSeq === cursorSeq && entry.docId === actualCursor.docId) {
1884
+ startIndex++;
1885
+ }
1886
+ }
1887
+ return startIndex;
1888
+ }
1889
+ async *iterateChangeMetadataSince(cursor) {
1890
+ const startedAt = Date.now();
1891
+ const actualCursor = cursor ?? {
1892
+ changeSeq: 0,
1893
+ lastModified: 0,
1894
+ docId: "",
1895
+ };
1896
+ const indexSnapshot = [...this.index];
1897
+ const startIndex = this.getStartIndexForCursor(indexSnapshot, actualCursor);
1898
+ let yieldedDocuments = 0;
1899
+ try {
1900
+ for (let i = startIndex; i < indexSnapshot.length; i++) {
1901
+ const entry = indexSnapshot[i];
1231
1902
  const currentCursor = {
1903
+ changeSeq: entry.changeSeq,
1232
1904
  lastModified: entry.lastModified,
1233
1905
  docId: entry.docId,
1234
1906
  };
1235
- // Yield immediately - this allows the caller to break early after each document
1236
- // Deleted documents are included so external indexes can handle deletions
1237
- yield { doc, cursor: currentCursor };
1907
+ yieldedDocuments++;
1908
+ yield {
1909
+ docId: entry.docId,
1910
+ lastModified: entry.lastModified,
1911
+ isDeleted: entry.isDeleted,
1912
+ cursor: currentCursor,
1913
+ };
1238
1914
  }
1239
- catch (error) {
1240
- this.logger.error(`Error processing document ${entry.docId}:`, error);
1241
- // Stop processing on error
1242
- throw error;
1915
+ }
1916
+ finally {
1917
+ this.performanceCallback?.onSyncOperation?.({
1918
+ operation: "iterateChangeMetadataSince",
1919
+ time: Date.now() - startedAt,
1920
+ details: {
1921
+ yieldedDocuments,
1922
+ startIndex,
1923
+ },
1924
+ });
1925
+ }
1926
+ }
1927
+ async *iterateChangesSince(cursor) {
1928
+ const startedAt = Date.now();
1929
+ // Default to initial cursor if null is provided.
1930
+ // Prefer deterministic sequence-based cursoring; keep legacy fallback compatibility.
1931
+ const actualCursor = cursor ?? { changeSeq: 0, lastModified: 0, docId: "" };
1932
+ this.logger.debug(`Starting iteration from cursor ${JSON.stringify(actualCursor)}`);
1933
+ // Use a stable snapshot of the index for this generator run so concurrent
1934
+ // updates do not reorder/skip entries while iterating.
1935
+ const indexSnapshot = [...this.index];
1936
+ // Find starting position using binary search
1937
+ // We want to find the first entry that is greater than the cursor.
1938
+ const startIndex = this.getStartIndexForCursor(indexSnapshot, actualCursor);
1939
+ let prefetchedDocuments = await this.prefetchIterationWindow(indexSnapshot, startIndex);
1940
+ let yieldedDocuments = 0;
1941
+ let loadedDocuments = 0;
1942
+ try {
1943
+ // Iterate through the stable snapshot and yield documents one at a time.
1944
+ for (let i = startIndex; i < indexSnapshot.length; i++) {
1945
+ const entry = indexSnapshot[i];
1946
+ try {
1947
+ this.logger.debug(`Yielding document ${entry.docId} from index (lastModified: ${entry.lastModified}, isDeleted: ${entry.isDeleted})`);
1948
+ let internalDoc = this.getCachedDocument(entry.docId);
1949
+ if (!internalDoc) {
1950
+ this.logger.debug(`Document ${entry.docId} not in cache, loading from store`);
1951
+ internalDoc = await this.loadDocumentInternal(entry.docId);
1952
+ if (internalDoc) {
1953
+ loadedDocuments++;
1954
+ }
1955
+ }
1956
+ if (!internalDoc) {
1957
+ this.logger.warn(`Document ${entry.docId} not found, skipping`);
1958
+ continue;
1959
+ }
1960
+ // Wrap the document (works for both deleted and non-deleted documents)
1961
+ const doc = this.wrapDocument(internalDoc);
1962
+ this.logger.debug(`Successfully loaded document ${entry.docId} (isDeleted: ${doc.isDeleted()})`);
1963
+ // Create cursor for current document
1964
+ const currentCursor = {
1965
+ changeSeq: entry.changeSeq,
1966
+ lastModified: entry.lastModified,
1967
+ docId: entry.docId,
1968
+ };
1969
+ // Yield immediately - this allows the caller to break early after each document
1970
+ // Deleted documents are included so external indexes can handle deletions
1971
+ yieldedDocuments++;
1972
+ yield { doc, cursor: currentCursor };
1973
+ prefetchedDocuments += await this.prefetchIterationWindow(indexSnapshot, i + 1);
1974
+ }
1975
+ catch (error) {
1976
+ this.logger.error(`Error processing document ${entry.docId}:`, error);
1977
+ // Stop processing on error
1978
+ throw error;
1979
+ }
1243
1980
  }
1244
1981
  }
1245
- this.logger.debug(`Iteration completed`);
1982
+ finally {
1983
+ this.logger.debug(`Iteration completed`);
1984
+ this.performanceCallback?.onSyncOperation?.({
1985
+ operation: "iterateChangesSince",
1986
+ time: Date.now() - startedAt,
1987
+ details: {
1988
+ yieldedDocuments,
1989
+ prefetchedDocuments,
1990
+ loadedDocuments,
1991
+ startIndex,
1992
+ },
1993
+ });
1994
+ }
1246
1995
  }
1247
1996
  /**
1248
1997
  * Incrementally update a cached document with new entries.
@@ -1256,7 +2005,7 @@ class BaseMindooDB {
1256
2005
  }
1257
2006
  this.logger.debug(`Applying ${newEntryMetadata.length} new entries to cached document ${docId}`);
1258
2007
  // Get current document heads to check if it changes
1259
- const headsBefore = Automerge.getHeads(cachedDoc.doc);
2008
+ const headsBefore = automerge_adapter_1.Automerge.getHeads(cachedDoc.doc);
1260
2009
  // Filter entries to only include change entries (exclude snapshots)
1261
2010
  const entriesToApply = newEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
1262
2011
  if (entriesToApply.length === 0) {
@@ -1340,12 +2089,12 @@ class BaseMindooDB {
1340
2089
  // Clone the cached document to avoid "outdated document" error
1341
2090
  // This happens when the document has been wrapped and returned to the user
1342
2091
  // Automerge marks documents as outdated when they're accessed, preventing direct mutation
1343
- const clonedDoc = Automerge.clone(cachedDoc.doc);
2092
+ const clonedDoc = automerge_adapter_1.Automerge.clone(cachedDoc.doc);
1344
2093
  // Apply all changes at once (Automerge handles dependency ordering)
1345
- const result = Automerge.applyChanges(clonedDoc, changeBytes);
2094
+ const result = automerge_adapter_1.Automerge.applyChanges(clonedDoc, changeBytes);
1346
2095
  const updatedDoc = result[0];
1347
2096
  // Check if document actually changed
1348
- const headsAfter = Automerge.getHeads(updatedDoc);
2097
+ const headsAfter = automerge_adapter_1.Automerge.getHeads(updatedDoc);
1349
2098
  const headsChanged = JSON.stringify(headsBefore) !== JSON.stringify(headsAfter);
1350
2099
  if (!headsChanged) {
1351
2100
  this.logger.debug(`Document ${docId} heads unchanged after applying new entries`);
@@ -1369,57 +2118,113 @@ class BaseMindooDB {
1369
2118
  isDeleted,
1370
2119
  };
1371
2120
  // Update cache
1372
- this.docCache.set(docId, updatedInternalDoc);
2121
+ this.storeCachedDocument(updatedInternalDoc);
2122
+ this.markDocDirty(docId);
1373
2123
  return updatedInternalDoc;
1374
2124
  }
1375
2125
  /**
1376
2126
  * Internal method to load a document from the content-addressed store
1377
2127
  */
1378
2128
  async loadDocumentInternal(docId) {
2129
+ const startedAt = Date.now();
2130
+ const cacheCheckStartedAt = Date.now();
1379
2131
  // Check cache first
1380
2132
  if (this.docCache.has(docId)) {
1381
2133
  this.logger.debug(`Document ${docId} found in cache, returning cached version`);
1382
- return this.docCache.get(docId);
2134
+ const cached = this.getCachedDocument(docId);
2135
+ this.performanceCallback?.onDocumentLoad?.({
2136
+ docId,
2137
+ cacheHit: true,
2138
+ metadataEntriesScanned: 0,
2139
+ replayEntriesLoaded: 0,
2140
+ snapshotUsed: false,
2141
+ cacheCheckTime: Date.now() - cacheCheckStartedAt,
2142
+ storeQueryTime: 0,
2143
+ entryLoadTime: 0,
2144
+ signatureVerificationTime: 0,
2145
+ decryptionTime: 0,
2146
+ automergeTime: 0,
2147
+ totalTime: Date.now() - startedAt,
2148
+ });
2149
+ return cached;
1383
2150
  }
1384
2151
  this.logger.debug(`===== Starting to load document ${docId} from store =====`);
2152
+ const cacheCheckTime = Date.now() - cacheCheckStartedAt;
2153
+ let storeQueryTime = 0;
2154
+ let entryLoadTime = 0;
2155
+ let signatureVerificationTime = 0;
2156
+ let decryptionTime = 0;
2157
+ let automergeTime = 0;
1385
2158
  // Get all entry metadata for this document
1386
2159
  // TODO: Implement loading from last snapshot if available
1387
2160
  this.logger.debug(`Getting all entry hashes for document ${docId}`);
1388
- const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
2161
+ const storeQueryStartedAt = Date.now();
2162
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
2163
+ storeQueryTime += Date.now() - storeQueryStartedAt;
1389
2164
  this.logger.debug(`Found ${allEntryMetadata.length} total entry hashes for document ${docId}`);
1390
2165
  if (allEntryMetadata.length === 0) {
1391
2166
  this.logger.debug(`No entry hashes found for document ${docId}, returning null`);
2167
+ this.performanceCallback?.onDocumentLoad?.({
2168
+ docId,
2169
+ cacheHit: false,
2170
+ metadataEntriesScanned: 0,
2171
+ replayEntriesLoaded: 0,
2172
+ snapshotUsed: false,
2173
+ cacheCheckTime,
2174
+ storeQueryTime,
2175
+ entryLoadTime,
2176
+ signatureVerificationTime,
2177
+ decryptionTime,
2178
+ automergeTime,
2179
+ totalTime: Date.now() - startedAt,
2180
+ });
1392
2181
  return null;
1393
2182
  }
1394
2183
  // Log all entry types
1395
2184
  const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
1396
2185
  this.logger.debug(`Entry types for ${docId}: ${entryTypes}`);
1397
- // Find the most recent snapshot (if any)
1398
- const snapshots = allEntryMetadata.filter(em => em.entryType === "doc_snapshot");
1399
- this.logger.debug(`Found ${snapshots.length} snapshot(s) for document ${docId}`);
1400
- let startFromSnapshot = false;
1401
- let snapshotMeta = null;
1402
- if (snapshots.length > 0) {
1403
- // Use the most recent snapshot
1404
- snapshots.sort((a, b) => b.createdAt - a.createdAt);
1405
- snapshotMeta = snapshots[0];
1406
- startFromSnapshot = true;
1407
- this.logger.debug(`Will start from snapshot ${snapshotMeta.id} created at ${snapshotMeta.createdAt}`);
2186
+ const metadataById = new Map(allEntryMetadata.map((meta) => [meta.id, meta]));
2187
+ const planStartedAt = Date.now();
2188
+ const materializationPlan = (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, allEntryMetadata, {
2189
+ includeDiagnostics: true,
2190
+ });
2191
+ const planTime = Date.now() - planStartedAt;
2192
+ this.performanceCallback?.onSyncOperation?.({
2193
+ operation: "planDocumentMaterialization",
2194
+ time: planTime,
2195
+ details: {
2196
+ docId,
2197
+ metadataEntriesScanned: allEntryMetadata.length,
2198
+ replayEntriesLoaded: materializationPlan.entryIdsToApply.length,
2199
+ snapshotEntryId: materializationPlan.snapshotEntryId,
2200
+ diagnostics: materializationPlan.diagnostics,
2201
+ },
2202
+ });
2203
+ let startFromSnapshot = materializationPlan.snapshotEntryId !== null;
2204
+ const snapshotMeta = materializationPlan.snapshotEntryId
2205
+ ? (metadataById.get(materializationPlan.snapshotEntryId) || null)
2206
+ : null;
2207
+ if (startFromSnapshot && !snapshotMeta) {
2208
+ this.logger.warn(`Planner referenced snapshot ${materializationPlan.snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
2209
+ startFromSnapshot = false;
2210
+ }
2211
+ if (startFromSnapshot && snapshotMeta) {
2212
+ this.logger.debug(`Planner selected snapshot ${snapshotMeta.id} for ${docId}`);
1408
2213
  }
1409
2214
  else {
1410
- this.logger.debug(`No snapshot found, will start from scratch`);
1411
- }
1412
- // Get all entries (excluding snapshot entries - we'll handle delete separately)
1413
- // Include "doc_create", "doc_change", and "doc_delete" types as they all contain Automerge changes to apply
1414
- const entriesToLoad = startFromSnapshot
1415
- ? allEntryMetadata.filter(em => (em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete") && em.createdAt > snapshotMeta.createdAt)
1416
- : allEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
1417
- this.logger.debug(`Will load ${entriesToLoad.length} entries for document ${docId} (after snapshot filter)`);
2215
+ this.logger.debug(`Planner did not select a snapshot for ${docId}`);
2216
+ }
2217
+ const entriesToLoad = materializationPlan.entryIdsToApply
2218
+ .map((id) => metadataById.get(id))
2219
+ .filter((m) => m !== undefined);
2220
+ this.logger.debug(`Planner returned ${entriesToLoad.length} replay entries for ${docId}; diagnostics=${JSON.stringify(materializationPlan.diagnostics || {})}`);
1418
2221
  // Load the snapshot first if we have one
1419
2222
  let doc = undefined;
1420
2223
  if (startFromSnapshot && snapshotMeta) {
1421
2224
  this.logger.debug(`Loading snapshot for document ${docId}`);
2225
+ const snapshotLoadStartedAt = Date.now();
1422
2226
  const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
2227
+ entryLoadTime += Date.now() - snapshotLoadStartedAt;
1423
2228
  this.logger.debug(`Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
1424
2229
  if (snapshotEntries.length > 0) {
1425
2230
  const snapshotData = snapshotEntries[0];
@@ -1431,7 +2236,9 @@ class BaseMindooDB {
1431
2236
  else {
1432
2237
  // Verify signature against the encrypted snapshot (no decryption needed)
1433
2238
  // We sign the encrypted payload, so anyone can verify signatures without decryption keys
2239
+ const signatureStartedAt = Date.now();
1434
2240
  isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
2241
+ signatureVerificationTime += Date.now() - signatureStartedAt;
1435
2242
  }
1436
2243
  if (!isValid) {
1437
2244
  this.logger.warn(`Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
@@ -1441,14 +2248,18 @@ class BaseMindooDB {
1441
2248
  else {
1442
2249
  this.logger.debug(`Snapshot signature valid, decrypting snapshot`);
1443
2250
  // Decrypt snapshot (only after signature verification passes)
2251
+ const decryptStartedAt = Date.now();
1444
2252
  const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
2253
+ decryptionTime += Date.now() - decryptStartedAt;
1445
2254
  this.logger.debug(`Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
1446
2255
  // Load snapshot using Automerge.load()
1447
2256
  // This deserializes a full document snapshot from binary data
1448
2257
  // According to Automerge docs: load() is equivalent to init() followed by loadIncremental()
1449
2258
  this.logger.debug(`Loading snapshot into Automerge document`);
1450
- doc = Automerge.load(decryptedSnapshot);
1451
- this.logger.debug(`Successfully loaded snapshot, document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
2259
+ const automergeStartedAt = Date.now();
2260
+ doc = automerge_adapter_1.Automerge.load(decryptedSnapshot);
2261
+ automergeTime += Date.now() - automergeStartedAt;
2262
+ this.logger.debug(`Successfully loaded snapshot, document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
1452
2263
  // Register the snapshot's automerge hash -> entry ID mapping
1453
2264
  const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
1454
2265
  if (parsed) {
@@ -1460,19 +2271,18 @@ class BaseMindooDB {
1460
2271
  // If we don't have a snapshot, start from scratch
1461
2272
  if (!doc) {
1462
2273
  this.logger.debug(`Initializing new Automerge document for ${docId}`);
1463
- doc = Automerge.init();
1464
- this.logger.debug(`Initialized empty document, heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
2274
+ doc = automerge_adapter_1.Automerge.init();
2275
+ this.logger.debug(`Initialized empty document, heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
1465
2276
  }
1466
- // Sort entries by timestamp
1467
- entriesToLoad.sort((a, b) => a.createdAt - b.createdAt);
1468
- this.logger.debug(`Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
1469
2277
  // Load and apply all entries
1470
2278
  this.logger.debug(`Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
2279
+ const entryLoadStartedAt = Date.now();
1471
2280
  const entries = await this.store.getEntries(entriesToLoad.map(em => em.id));
2281
+ entryLoadTime += Date.now() - entryLoadStartedAt;
1472
2282
  this.logger.debug(`Retrieved ${entries.length} entries from store for document ${docId}`);
1473
2283
  this.logger.debug(`Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? 'starting from snapshot' : 'starting from scratch'})`);
1474
2284
  // Log current document state before applying entries
1475
- this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
2285
+ this.logger.debug(`Document state before applying entries: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
1476
2286
  // Filter entries for admin-only mode first
1477
2287
  const validEntries = entries.filter(entryData => {
1478
2288
  if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
@@ -1508,6 +2318,7 @@ class BaseMindooDB {
1508
2318
  }
1509
2319
  }
1510
2320
  // Verify all signatures in parallel using cached keys
2321
+ const signatureStartedAt = Date.now();
1511
2322
  const signatureVerificationResults = await Promise.all(validEntries.map(async (entryData) => {
1512
2323
  const cryptoKey = keyMap.get(entryData.createdByPublicKey);
1513
2324
  if (!cryptoKey) {
@@ -1517,6 +2328,7 @@ class BaseMindooDB {
1517
2328
  const isValid = await this.verifySignatureWithKey(cryptoKey, entryData.encryptedData, entryData.signature);
1518
2329
  return { entryData, isValid };
1519
2330
  }));
2331
+ signatureVerificationTime += Date.now() - signatureStartedAt;
1520
2332
  // Filter out entries with invalid signatures
1521
2333
  const verifiedEntries = signatureVerificationResults
1522
2334
  .filter(({ isValid }) => {
@@ -1533,10 +2345,12 @@ class BaseMindooDB {
1533
2345
  // Parallel decryption - decrypt all entries concurrently
1534
2346
  // Automerge handles dependency buffering internally, so we can decrypt all in parallel
1535
2347
  this.logger.debug(`Decrypting ${verifiedEntries.length} entries in parallel`);
2348
+ const decryptionStartedAt = Date.now();
1536
2349
  const decryptionResults = await Promise.all(verifiedEntries.map(async (entryData) => {
1537
2350
  const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
1538
2351
  return { entryData, decryptedPayload };
1539
2352
  }));
2353
+ decryptionTime += Date.now() - decryptionStartedAt;
1540
2354
  // Collect change bytes and register automerge hash mappings
1541
2355
  const changeBytes = [];
1542
2356
  for (const { entryData, decryptedPayload } of decryptionResults) {
@@ -1551,10 +2365,12 @@ class BaseMindooDB {
1551
2365
  if (changeBytes.length > 0) {
1552
2366
  this.logger.debug(`Applying ${changeBytes.length} changes to document ${docId} using batch applyChanges`);
1553
2367
  try {
1554
- const result = Automerge.applyChanges(doc, changeBytes);
2368
+ const automergeStartedAt = Date.now();
2369
+ const result = automerge_adapter_1.Automerge.applyChanges(doc, changeBytes);
1555
2370
  doc = result[0];
2371
+ automergeTime += Date.now() - automergeStartedAt;
1556
2372
  this.logger.debug(`Successfully applied ${changeBytes.length} changes to document ${docId}`);
1557
- this.logger.debug(`Document state after applying changes: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
2373
+ this.logger.debug(`Document state after applying changes: heads=${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
1558
2374
  }
1559
2375
  catch (error) {
1560
2376
  this.logger.error(`Error applying changes to document ${docId}:`, error);
@@ -1566,13 +2382,16 @@ class BaseMindooDB {
1566
2382
  }
1567
2383
  // Extract metadata from document (doc is guaranteed to be defined at this point)
1568
2384
  this.logger.debug(`All entries applied successfully for document ${docId}`);
1569
- this.logger.debug(`Final document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
2385
+ this.logger.debug(`Final document heads: ${JSON.stringify(automerge_adapter_1.Automerge.getHeads(doc))}`);
1570
2386
  const payload = doc;
1571
2387
  // Check if document was deleted by looking for a "doc_delete" type entry
1572
2388
  const hasDeleteEntry = allEntryMetadata.some(em => em.entryType === "doc_delete");
1573
2389
  const isDeleted = hasDeleteEntry;
1574
2390
  this.logger.debug(`Document ${docId} isDeleted: ${isDeleted}`);
1575
- const decryptionKeyId = payload._decryptionKeyId || "default";
2391
+ // The authoritative decryptionKeyId comes from the doc_create entry's metadata,
2392
+ // not from the Automerge payload (which does not store encryption metadata).
2393
+ const createEntry = allEntryMetadata.find(em => em.entryType === "doc_create");
2394
+ const decryptionKeyId = createEntry ? createEntry.decryptionKeyId : "default";
1576
2395
  // Get lastModified from payload, or use the timestamp of the last entry
1577
2396
  const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
1578
2397
  const lastModified = payload._lastModified ||
@@ -1590,10 +2409,69 @@ class BaseMindooDB {
1590
2409
  isDeleted,
1591
2410
  };
1592
2411
  // Update cache
1593
- this.docCache.set(docId, internalDoc);
2412
+ this.storeCachedDocument(internalDoc);
2413
+ this.markDocDirty(docId);
1594
2414
  this.logger.debug(`===== Successfully loaded document ${docId} and cached it =====`);
2415
+ this.performanceCallback?.onDocumentLoad?.({
2416
+ docId,
2417
+ cacheHit: false,
2418
+ metadataEntriesScanned: allEntryMetadata.length,
2419
+ replayEntriesLoaded: entriesToLoad.length,
2420
+ snapshotUsed: startFromSnapshot && snapshotMeta !== null,
2421
+ cacheCheckTime,
2422
+ storeQueryTime,
2423
+ entryLoadTime,
2424
+ signatureVerificationTime,
2425
+ decryptionTime,
2426
+ automergeTime,
2427
+ totalTime: Date.now() - startedAt,
2428
+ });
1595
2429
  return internalDoc;
1596
2430
  }
2431
+ /**
2432
+ * Convert an Automerge document to a plain JS object, converting Text objects to strings.
2433
+ * If using native Automerge (react-native-automerge-generated), this uses the native
2434
+ * materialize() method which properly converts Text objects to strings.
2435
+ * Falls back to direct access if native backend is not available.
2436
+ */
2437
+ convertAutomergeToJS(doc) {
2438
+ // Check if this document has a native Automerge handle attached
2439
+ // The native implementation attaches metadata with Symbol.for('_am_meta')
2440
+ const STATE = Symbol.for('_am_meta');
2441
+ const meta = doc[STATE];
2442
+ if (meta && meta.handle && typeof meta.handle.materialize === 'function') {
2443
+ // Use native materialize() which properly converts Text objects to strings
2444
+ try {
2445
+ const materialized = meta.handle.materialize('/');
2446
+ return materialized;
2447
+ }
2448
+ catch (error) {
2449
+ console.error('[MindooDB] Failed to materialize document:', error);
2450
+ // Fall through to direct access
2451
+ }
2452
+ }
2453
+ // Fallback: direct access (for WASM or if native fails)
2454
+ const result = {};
2455
+ const keys = Object.keys(doc);
2456
+ for (const key of keys) {
2457
+ const value = doc[key];
2458
+ if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Uint8Array)) {
2459
+ result[key] = this.convertAutomergeToJS(value);
2460
+ }
2461
+ else if (Array.isArray(value)) {
2462
+ result[key] = value.map(item => {
2463
+ if (item !== null && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Uint8Array)) {
2464
+ return this.convertAutomergeToJS(item);
2465
+ }
2466
+ return item;
2467
+ });
2468
+ }
2469
+ else {
2470
+ result[key] = value;
2471
+ }
2472
+ }
2473
+ return result;
2474
+ }
1597
2475
  /**
1598
2476
  * Wrap an internal document in the MindooDoc interface.
1599
2477
  * This is the read-only wrapper - write methods throw errors.
@@ -1626,7 +2504,11 @@ class BaseMindooDB {
1626
2504
  getCreatedAt: () => internalDoc.createdAt,
1627
2505
  getLastModified: () => internalDoc.lastModified,
1628
2506
  isDeleted: () => internalDoc.isDeleted,
1629
- getData: () => createReadOnlyProxy(internalDoc.doc),
2507
+ getData: () => {
2508
+ // Convert Automerge document to plain JS object, converting Text objects to strings
2509
+ const jsDoc = this.convertAutomergeToJS(internalDoc.doc);
2510
+ return createReadOnlyProxy(jsDoc);
2511
+ },
1630
2512
  // ========== Attachment Write Methods ==========
1631
2513
  // These throw errors in the read-only wrapper
1632
2514
  addAttachment: async () => {
@@ -1662,7 +2544,7 @@ class BaseMindooDB {
1662
2544
  * Get an attachment reference by ID from a document's _attachments array.
1663
2545
  */
1664
2546
  getAttachmentRefInternal(docId, attachmentId) {
1665
- const internalDoc = this.docCache.get(docId);
2547
+ const internalDoc = this.getCachedDocument(docId);
1666
2548
  if (!internalDoc) {
1667
2549
  throw new Error(`Document ${docId} not found in cache`);
1668
2550
  }
@@ -2003,7 +2885,64 @@ class BaseMindooDB {
2003
2885
  };
2004
2886
  }
2005
2887
  /**
2006
- * Pull changes from a remote content-addressed store.
2888
+ * Resolve a sync target to a ContentAddressedStore.
2889
+ * Accepts either a raw store or a MindooDB instance (calls getStore()).
2890
+ */
2891
+ resolveStore(remote) {
2892
+ if ('getStore' in remote && typeof remote.getStore === 'function') {
2893
+ return remote.getStore();
2894
+ }
2895
+ return remote;
2896
+ }
2897
+ /**
2898
+ * Apply a temporary network-auth identity override for a single sync call.
2899
+ *
2900
+ * Purpose:
2901
+ * - Allows per-call authentication as a different user (for example admin bootstrap)
2902
+ * without changing the tenant's default connected user.
2903
+ * - Returns a cleanup callback so caller can always restore default auth state in `finally`.
2904
+ */
2905
+ async applyNetworkAuthOverrideForSync(remoteStore, options) {
2906
+ // Per-call override: use an alternate identity (for example, admin bootstrap)
2907
+ // only for this sync operation.
2908
+ const override = options?.networkAuthOverride;
2909
+ if (!override) {
2910
+ // No override requested: return a no-op cleanup callback for unified call sites.
2911
+ return () => { };
2912
+ }
2913
+ const overrideCapableStore = remoteStore;
2914
+ if (typeof overrideCapableStore.setSyncAuthOverride !== "function") {
2915
+ // Local/in-memory stores do not support network auth override; keep default auth.
2916
+ this.logger.warn("networkAuthOverride was provided, but remote store does not support auth override");
2917
+ return () => { };
2918
+ }
2919
+ const subtle = this.tenant.getCryptoAdapter().getSubtle();
2920
+ // Decrypt and import override signing key (Ed25519) for challenge signing.
2921
+ const signingKeyBuffer = await this.tenant.decryptPrivateKey(override.user.userSigningKeyPair.privateKey, override.password, "signing");
2922
+ const signingKey = await subtle.importKey("pkcs8", signingKeyBuffer, { name: "Ed25519" }, false, ["sign"]);
2923
+ // Decrypt and import override encryption key (RSA-OAEP) so encrypted network
2924
+ // entries can be decrypted with the same override identity.
2925
+ const encryptionKeyBuffer = await this.tenant.decryptPrivateKey(override.user.userEncryptionKeyPair.privateKey, override.password, "encryption");
2926
+ const encryptionKey = await subtle.importKey("pkcs8", encryptionKeyBuffer, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
2927
+ // Activate temporary override on the remote store.
2928
+ overrideCapableStore.setSyncAuthOverride({
2929
+ username: override.user.username,
2930
+ signingKey,
2931
+ privateEncryptionKey: encryptionKey,
2932
+ });
2933
+ // Return cleanup callback so caller can always restore normal auth in finally.
2934
+ return () => {
2935
+ if (typeof overrideCapableStore.clearSyncAuthOverride === "function") {
2936
+ overrideCapableStore.clearSyncAuthOverride();
2937
+ }
2938
+ else {
2939
+ // Backward-compatible fallback if explicit clear API is not implemented.
2940
+ overrideCapableStore.setSyncAuthOverride?.(null);
2941
+ }
2942
+ };
2943
+ }
2944
+ /**
2945
+ * Pull changes from a remote content-addressed store or another MindooDB instance.
2007
2946
  *
2008
2947
  * This method:
2009
2948
  * 1. Finds entries in the remote store that we don't have locally
@@ -2011,69 +2950,93 @@ class BaseMindooDB {
2011
2950
  * 3. Stores them in our local store
2012
2951
  * 4. Syncs the local store to process the new entries
2013
2952
  *
2014
- * @param remoteStore The remote store to pull entries from
2015
- * @return A promise that resolves when the pull is complete
2953
+ * @param remote The remote store or MindooDB instance to pull entries from
2954
+ * @param options Optional sync options for progress tracking, paging, and cancellation
2955
+ * @return A promise that resolves with the sync result
2016
2956
  */
2017
- async pullChangesFrom(remoteStore) {
2957
+ async pullChangesFrom(remote, options) {
2958
+ const remoteStore = this.resolveStore(remote);
2018
2959
  if (this.store.getId() !== remoteStore.getId()) {
2019
2960
  throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
2020
2961
  }
2021
2962
  this.logger.info(`Pulling entries from remote store ${remoteStore.getId()}`);
2022
- // Get all entry IDs we already have in our local store
2023
- const localEntryIds = await this.store.getAllIds();
2024
- this.logger.debug(`Local store has ${localEntryIds.length} entry IDs`);
2025
- // Find entries in the remote store that we don't have
2026
- const newEntryMetadata = await remoteStore.findNewEntries(localEntryIds);
2027
- this.logger.debug(`Found ${newEntryMetadata.length} new entries in remote store`);
2028
- if (newEntryMetadata.length === 0) {
2029
- this.logger.debug(`No new entries to pull`);
2030
- return;
2963
+ const restoreAuthOverride = await this.applyNetworkAuthOverrideForSync(remoteStore, options);
2964
+ try {
2965
+ const syncResult = await this.syncEntriesFromStore(remoteStore, this.store, options);
2966
+ this.logger.debug(`Transferred ${syncResult.transferred} entries from remote store`);
2967
+ if (syncResult.cancelled) {
2968
+ this.logger.info(`Pull cancelled after transferring ${syncResult.transferred} entries`);
2969
+ return { transferredEntries: syncResult.transferred, cancelled: true };
2970
+ }
2971
+ if (syncResult.transferred === 0) {
2972
+ this.logger.debug(`No new entries to pull`);
2973
+ return { transferredEntries: 0, cancelled: false };
2974
+ }
2975
+ options?.onProgress?.({
2976
+ phase: 'processing',
2977
+ message: `Processing ${syncResult.transferred} new entries...`,
2978
+ transferredEntries: syncResult.transferred,
2979
+ scannedEntries: syncResult.transferred,
2980
+ });
2981
+ // Sync the local store to process the new entries
2982
+ // This will update the index, cache, and processedEntryIds
2983
+ await this.syncStoreChanges();
2984
+ this.logger.info(`Pull complete, synced ${syncResult.transferred} entries`);
2985
+ options?.onProgress?.({
2986
+ phase: 'complete',
2987
+ message: `Pull complete: ${syncResult.transferred} entries synced`,
2988
+ transferredEntries: syncResult.transferred,
2989
+ scannedEntries: syncResult.transferred,
2990
+ });
2991
+ return { transferredEntries: syncResult.transferred, cancelled: false };
2992
+ }
2993
+ finally {
2994
+ restoreAuthOverride();
2031
2995
  }
2032
- // Get the full entries from the remote store
2033
- const newEntries = await remoteStore.getEntries(newEntryMetadata.map(em => em.id));
2034
- this.logger.debug(`Retrieved ${newEntries.length} entries from remote store`);
2035
- // Store all entries in our local store
2036
- // The putEntries method handles deduplication (no-op if entry already exists)
2037
- await this.store.putEntries(newEntries);
2038
- this.logger.debug(`Stored ${newEntries.length} entries in local store`);
2039
- // Sync the local store to process the new entries
2040
- // This will update the index, cache, and processedEntryIds
2041
- await this.syncStoreChanges();
2042
- this.logger.info(`Pull complete, synced ${newEntries.length} entries`);
2043
2996
  }
2044
2997
  /**
2045
- * Push changes to a remote content-addressed store.
2998
+ * Push changes to a remote content-addressed store or another MindooDB instance.
2046
2999
  *
2047
3000
  * This method:
2048
3001
  * 1. Finds entries in our local store that the remote doesn't have
2049
3002
  * 2. Retrieves those entries from our local store
2050
3003
  * 3. Stores them in the remote store
2051
3004
  *
2052
- * @param remoteStore The remote store to push entries to
2053
- * @return A promise that resolves when the push is complete
3005
+ * @param remote The remote store or MindooDB instance to push entries to
3006
+ * @param options Optional sync options for progress tracking, paging, and cancellation
3007
+ * @return A promise that resolves with the sync result
2054
3008
  */
2055
- async pushChangesTo(remoteStore) {
3009
+ async pushChangesTo(remote, options) {
3010
+ const remoteStore = this.resolveStore(remote);
2056
3011
  if (this.store.getId() !== remoteStore.getId()) {
2057
3012
  throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
2058
3013
  }
2059
3014
  this.logger.info(`Pushing entries to remote store ${remoteStore.getId()}`);
2060
- // Get all entry IDs the remote store already has
2061
- const remoteEntryIds = await remoteStore.getAllIds();
2062
- this.logger.debug(`Remote store has ${remoteEntryIds.length} entry IDs`);
2063
- // Find entries in our local store that the remote doesn't have
2064
- const newEntryMetadata = await this.store.findNewEntries(remoteEntryIds);
2065
- this.logger.debug(`Found ${newEntryMetadata.length} new entries to push`);
2066
- if (newEntryMetadata.length === 0) {
2067
- this.logger.debug(`No new entries to push`);
2068
- return;
3015
+ const restoreAuthOverride = await this.applyNetworkAuthOverrideForSync(remoteStore, options);
3016
+ try {
3017
+ const syncResult = await this.syncEntriesFromStore(this.store, remoteStore, options);
3018
+ this.logger.debug(`Transferred ${syncResult.transferred} entries to remote store`);
3019
+ if (syncResult.cancelled) {
3020
+ this.logger.info(`Push cancelled after transferring ${syncResult.transferred} entries`);
3021
+ return { transferredEntries: syncResult.transferred, cancelled: true };
3022
+ }
3023
+ if (syncResult.transferred === 0) {
3024
+ this.logger.debug(`No new entries to push`);
3025
+ }
3026
+ else {
3027
+ this.logger.info(`Pushed ${syncResult.transferred} entries to remote store`);
3028
+ }
3029
+ options?.onProgress?.({
3030
+ phase: 'complete',
3031
+ message: `Push complete: ${syncResult.transferred} entries transferred`,
3032
+ transferredEntries: syncResult.transferred,
3033
+ scannedEntries: syncResult.transferred,
3034
+ });
3035
+ return { transferredEntries: syncResult.transferred, cancelled: false };
3036
+ }
3037
+ finally {
3038
+ restoreAuthOverride();
2069
3039
  }
2070
- // Get the full entries from our local store
2071
- const newEntries = await this.store.getEntries(newEntryMetadata.map(em => em.id));
2072
- this.logger.debug(`Retrieved ${newEntries.length} entries from local store`);
2073
- // Store all entries in the remote store
2074
- // The putEntries method handles deduplication (no-op if entry already exists)
2075
- await remoteStore.putEntries(newEntries);
2076
- this.logger.info(`Pushed ${newEntries.length} entries to remote store`);
2077
3040
  }
2078
3041
  }
2079
3042
  exports.BaseMindooDB = BaseMindooDB;