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
@@ -0,0 +1,1358 @@
1
+ "use strict";
2
+ /**
3
+ * Node.js filesystem-backed implementation of {@link ContentAddressedStore}.
4
+ *
5
+ * This module provides a persistent, crash-resilient content-addressed store
6
+ * designed for the MindooDB sync protocol. Each database gets its own
7
+ * directory subtree where entry metadata (JSON) and encrypted payloads (binary)
8
+ * are stored as individual files. An optional in-memory index accelerates
9
+ * lookups, cursor-based scans, and bloom-filter generation for efficient
10
+ * network sync.
11
+ *
12
+ * ### On-disk layout
13
+ *
14
+ * ```
15
+ * <basePath>/<dbId>/
16
+ * entries/ one JSON file per entry id (source of truth)
17
+ * content/ one .bin file per unique contentHash
18
+ * metadata-index.json compact snapshot of the in-memory index
19
+ * metadata-segments/ append-only mutation log (upsert / delete)
20
+ * ```
21
+ *
22
+ * ### Durability & crash safety
23
+ *
24
+ * Every file write follows a write-tmp → fsync → atomic-rename → dir-fsync
25
+ * protocol so readers never observe partially written data. Metadata segments
26
+ * provide a low-write-amplification WAL that is periodically compacted into a
27
+ * full snapshot.
28
+ *
29
+ * ### Indexing
30
+ *
31
+ * When `indexingEnabled` is true (the default), the store maintains three
32
+ * in-memory structures:
33
+ * - `entries` — Map<id, metadata> for O(1) point lookups
34
+ * - `docIndex` — Map<docId, Set<id>> for fast per-document queries
35
+ * - `orderedMetadata` — sorted array for O(log N) cursor scans
36
+ *
37
+ * On startup the index is reconstructed from (snapshot + segments), then
38
+ * validated against the authoritative entry files.
39
+ *
40
+ * @module BasicOnDiskContentAddressedStore
41
+ */
42
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
43
+ if (k2 === undefined) k2 = k;
44
+ var desc = Object.getOwnPropertyDescriptor(m, k);
45
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
46
+ desc = { enumerable: true, get: function() { return m[k]; } };
47
+ }
48
+ Object.defineProperty(o, k2, desc);
49
+ }) : (function(o, m, k, k2) {
50
+ if (k2 === undefined) k2 = k;
51
+ o[k2] = m[k];
52
+ }));
53
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
54
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
55
+ }) : function(o, v) {
56
+ o["default"] = v;
57
+ });
58
+ var __importStar = (this && this.__importStar) || (function () {
59
+ var ownKeys = function(o) {
60
+ ownKeys = Object.getOwnPropertyNames || function (o) {
61
+ var ar = [];
62
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
63
+ return ar;
64
+ };
65
+ return ownKeys(o);
66
+ };
67
+ return function (mod) {
68
+ if (mod && mod.__esModule) return mod;
69
+ var result = {};
70
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
71
+ __setModuleDefault(result, mod);
72
+ return result;
73
+ };
74
+ })();
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.BasicOnDiskContentAddressedStoreFactory = exports.BasicOnDiskContentAddressedStore = void 0;
77
+ const promises_1 = require("fs/promises");
78
+ const fs_1 = require("fs");
79
+ const path = __importStar(require("path"));
80
+ const bloom_1 = require("../../core/appendonlystores/bloom");
81
+ const MaterializationPlanner_1 = require("../../core/appendonlystores/MaterializationPlanner");
82
+ const logging_1 = require("../../core/logging");
83
+ /**
84
+ * When the number of uncompacted segment files reaches this threshold,
85
+ * the store will persist a fresh metadata-index snapshot and delete the
86
+ * covered segment files. Set to 0 to disable file-count–based compaction.
87
+ */
88
+ const DEFAULT_METADATA_SEGMENT_COMPACTION_MIN_FILES = 32;
89
+ // ---------------------------------------------------------------------------
90
+ // Serialization helpers
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Convert a runtime {@link StoreEntryMetadata} to its JSON-safe form.
94
+ * The only transformation is encoding the binary `signature` as base64.
95
+ */
96
+ function serializeMetadata(metadata) {
97
+ return {
98
+ ...metadata,
99
+ signature: Buffer.from(metadata.signature).toString("base64"),
100
+ };
101
+ }
102
+ /**
103
+ * Restore a {@link StoreEntryMetadata} from its JSON-safe serialized form.
104
+ * Decodes the base64 `signature` back into a `Uint8Array`.
105
+ */
106
+ function deserializeMetadata(serialized) {
107
+ return {
108
+ ...serialized,
109
+ signature: new Uint8Array(Buffer.from(serialized.signature, "base64")),
110
+ };
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Filename helpers
114
+ // ---------------------------------------------------------------------------
115
+ /**
116
+ * Derive the on-disk filename for an entry's metadata file.
117
+ * The entry id is URI-encoded so it is safe for any filesystem.
118
+ */
119
+ function toEntryFileName(id) {
120
+ return `${encodeURIComponent(id)}.json`;
121
+ }
122
+ /**
123
+ * Recover an entry id from its on-disk metadata filename.
124
+ * Reverses the encoding applied by {@link toEntryFileName}.
125
+ */
126
+ function fromEntryFileName(fileName) {
127
+ const withoutExt = fileName.endsWith(".json") ? fileName.slice(0, -5) : fileName;
128
+ return decodeURIComponent(withoutExt);
129
+ }
130
+ /**
131
+ * Generate a temporary file path for atomic writes.
132
+ *
133
+ * The suffix includes the process PID, current timestamp, and a random token
134
+ * to avoid collisions when multiple processes or rapid sequential writes
135
+ * target the same final path.
136
+ */
137
+ function tempPathFor(finalPath) {
138
+ return `${finalPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
139
+ }
140
+ /**
141
+ * Node.js persistent ContentAddressedStore implementation.
142
+ *
143
+ * On-disk layout per database id:
144
+ *
145
+ * <basePath>/<dbId>/
146
+ * entries/ // one metadata JSON file per entry id
147
+ * content/ // one binary payload file per contentHash
148
+ * metadata-index.json // compact snapshot of ordered metadata (optional)
149
+ * metadata-segments/ // append-only metadata mutation log (optional)
150
+ *
151
+ * Design goals:
152
+ * - Strong visibility: readers only observe committed files (atomic rename).
153
+ * - Crash resilience: fsync + rename + directory fsync for durability.
154
+ * - High sync/read throughput: in-memory ordered index + cursor scan.
155
+ * - Low write amplification: append metadata segments, compact periodically.
156
+ */
157
+ class BasicOnDiskContentAddressedStore {
158
+ // ---------------------------------------------------------------------------
159
+ // Constructor
160
+ // ---------------------------------------------------------------------------
161
+ /**
162
+ * Create a new on-disk content-addressed store for the given database id.
163
+ *
164
+ * Initialization (directory creation, index loading) happens asynchronously.
165
+ * All public methods internally await the init promise so callers do not
166
+ * need to wait explicitly — but {@link awaitIndexReady} can be used to
167
+ * observe progress.
168
+ *
169
+ * @param dbId Unique database identifier (becomes the directory name).
170
+ * @param logger Optional logger; a default console logger is created if omitted.
171
+ * @param options Store configuration (base path, indexing, compaction thresholds, etc.).
172
+ */
173
+ constructor(dbId, logger, options) {
174
+ /**
175
+ * Set of segment file names that have been replayed into the in-memory
176
+ * index (or were appended by this process). Only these files are eligible
177
+ * for compaction — files that appeared concurrently from another process
178
+ * are excluded until they have been replayed.
179
+ */
180
+ this.appliedMetadataSegmentFiles = new Set();
181
+ // ---------------------------------------------------------------------------
182
+ // In-memory indexes (populated only when indexingEnabled = true)
183
+ // ---------------------------------------------------------------------------
184
+ /**
185
+ * Primary index: entry id -> full metadata.
186
+ * Provides O(1) point lookups for `hasEntries`, `getEntries`,
187
+ * `readMetadataById`, and dependency traversal.
188
+ */
189
+ this.entries = new Map();
190
+ /**
191
+ * Secondary index: docId -> set of entry ids belonging to that document.
192
+ * Enables fast per-document queries in `findNewEntriesForDoc`.
193
+ */
194
+ this.docIndex = new Map();
195
+ /**
196
+ * Reference count per content hash. Tracks how many entry ids share the
197
+ * same payload blob, used during `purgeDocHistory` to decide whether a
198
+ * content file can be safely deleted.
199
+ */
200
+ this.contentRefCount = new Map();
201
+ /**
202
+ * All metadata sorted by `(createdAt ASC, id ASC)`.
203
+ * Supports O(log N) lower-bound binary search for cursor-based scans
204
+ * in {@link scanEntriesSince}.
205
+ */
206
+ this.orderedMetadata = [];
207
+ this.dbId = dbId;
208
+ this.logger =
209
+ logger ||
210
+ new logging_1.MindooLogger((0, logging_1.getDefaultLogLevel)(), `BasicOnDiskStore:${dbId}`, true);
211
+ this.storeRoot = path.join(options?.basePath?.toString() || ".mindoodb-store", dbId);
212
+ this.entriesDir = path.join(this.storeRoot, "entries");
213
+ this.contentDir = path.join(this.storeRoot, "content");
214
+ this.metadataIndexPath = path.join(this.storeRoot, "metadata-index.json");
215
+ this.metadataSegmentsDir = path.join(this.storeRoot, "metadata-segments");
216
+ this.indexingEnabled = options?.indexingEnabled !== false;
217
+ const compactionOption = options?.metadataSegmentCompactionMinFiles;
218
+ this.metadataSegmentCompactionMinFiles =
219
+ compactionOption === undefined || compactionOption === null
220
+ ? DEFAULT_METADATA_SEGMENT_COMPACTION_MIN_FILES
221
+ : Math.max(0, Math.floor(Number(compactionOption)));
222
+ const compactionMaxBytesOption = options?.metadataSegmentCompactionMaxBytes;
223
+ this.metadataSegmentCompactionMaxBytes =
224
+ compactionMaxBytesOption === undefined || compactionMaxBytesOption === null
225
+ ? 0
226
+ : Math.max(0, Math.floor(Number(compactionMaxBytesOption)));
227
+ this.compactionStatus = {
228
+ enabled: this.indexingEnabled &&
229
+ (this.metadataSegmentCompactionMinFiles > 0 || this.metadataSegmentCompactionMaxBytes > 0),
230
+ compactionMinFiles: this.metadataSegmentCompactionMinFiles,
231
+ compactionMaxBytes: this.metadataSegmentCompactionMaxBytes,
232
+ totalCompactions: 0,
233
+ totalCompactedFiles: 0,
234
+ totalCompactedBytes: 0,
235
+ totalCompactionDurationMs: 0,
236
+ lastCompactionAt: null,
237
+ lastCompactedFiles: 0,
238
+ lastCompactedBytes: 0,
239
+ lastCompactionDurationMs: 0,
240
+ };
241
+ this.indexStatus = {
242
+ phase: this.indexingEnabled ? "building" : "ready",
243
+ indexingEnabled: this.indexingEnabled,
244
+ progress01: this.indexingEnabled ? 0 : 1,
245
+ };
246
+ this.initPromise = this.initialize(options);
247
+ }
248
+ /**
249
+ * One-time async initialization performed in the constructor's microtask.
250
+ *
251
+ * Steps:
252
+ * 1. Optionally wipe all local data (if `clearLocalDataOnStartup`).
253
+ * 2. Ensure on-disk directory structure exists.
254
+ * 3. When indexing is enabled:
255
+ * a. Attempt to load the persisted metadata-index snapshot (fast path).
256
+ * b. Replay any append-only segment files written since the snapshot.
257
+ * c. Validate the resulting in-memory index against the authoritative
258
+ * entry files on disk.
259
+ * d. If validation fails (stale snapshot, corruption, etc.), do a full
260
+ * rebuild from the entry files and write a fresh snapshot.
261
+ */
262
+ async initialize(options) {
263
+ if (options?.clearLocalDataOnStartup) {
264
+ await this.clearAllLocalDataInternal();
265
+ }
266
+ await (0, promises_1.mkdir)(this.entriesDir, { recursive: true });
267
+ await (0, promises_1.mkdir)(this.contentDir, { recursive: true });
268
+ await (0, promises_1.mkdir)(this.metadataSegmentsDir, { recursive: true });
269
+ if (!this.indexingEnabled) {
270
+ this.indexStatus = {
271
+ phase: "ready",
272
+ indexingEnabled: false,
273
+ progress01: 1,
274
+ };
275
+ return;
276
+ }
277
+ // Startup path:
278
+ // 1) load snapshot (fast path)
279
+ // 2) replay append-only segments (apply recent mutations)
280
+ // 3) validate against authoritative entry files
281
+ // 4) rebuild from entry files when snapshot/segments are stale/corrupt
282
+ await this.tryLoadPersistedIndex();
283
+ await this.replayMetadataSegments();
284
+ const indexCoverageOk = await this.validatePersistedIndexCoverage();
285
+ if (!indexCoverageOk) {
286
+ await this.rebuildInMemoryIndexes();
287
+ await this.persistMetadataIndex();
288
+ await this.clearMetadataSegments(Array.from(this.appliedMetadataSegmentFiles));
289
+ }
290
+ this.indexStatus = {
291
+ phase: "ready",
292
+ indexingEnabled: true,
293
+ progress01: 1,
294
+ };
295
+ }
296
+ /** Block until async initialization has completed. Called by every public method. */
297
+ async ensureInitialized() {
298
+ await this.initPromise;
299
+ }
300
+ /**
301
+ * Unconditionally remove the entire store directory tree and reset all
302
+ * in-memory state. Called by the constructor when `clearLocalDataOnStartup`
303
+ * is set, and by {@link clearAllLocalData} at runtime.
304
+ */
305
+ async clearAllLocalDataInternal() {
306
+ await (0, promises_1.rm)(this.storeRoot, { recursive: true, force: true });
307
+ this.entries.clear();
308
+ this.docIndex.clear();
309
+ this.contentRefCount.clear();
310
+ this.orderedMetadata = [];
311
+ this.appliedMetadataSegmentFiles.clear();
312
+ }
313
+ /**
314
+ * Full index rebuild from the authoritative entry files on disk.
315
+ *
316
+ * Clears all in-memory structures, reads every `entries/*.json` file,
317
+ * populates the primary, secondary, and ref-count indexes, and finally
318
+ * sorts `orderedMetadata` by `(createdAt, id)`.
319
+ *
320
+ * This is the slowest startup path (O(N) filesystem reads) and is only
321
+ * used when the persisted snapshot + segments cannot be validated.
322
+ */
323
+ async rebuildInMemoryIndexes() {
324
+ this.entries.clear();
325
+ this.docIndex.clear();
326
+ this.contentRefCount.clear();
327
+ this.orderedMetadata = [];
328
+ const files = await this.listEntryFiles();
329
+ for (const fileName of files) {
330
+ const metadata = await this.readMetadataByFileName(fileName);
331
+ if (!metadata) {
332
+ continue;
333
+ }
334
+ this.entries.set(metadata.id, metadata);
335
+ if (!this.docIndex.has(metadata.docId)) {
336
+ this.docIndex.set(metadata.docId, new Set());
337
+ }
338
+ this.docIndex.get(metadata.docId).add(metadata.id);
339
+ this.contentRefCount.set(metadata.contentHash, (this.contentRefCount.get(metadata.contentHash) || 0) + 1);
340
+ this.orderedMetadata.push(metadata);
341
+ }
342
+ this.orderedMetadata.sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt - b.createdAt));
343
+ }
344
+ /**
345
+ * Attempt to hydrate the in-memory index from the persisted
346
+ * `metadata-index.json` snapshot.
347
+ *
348
+ * This is the fast startup path: a single file read replaces the need to
349
+ * enumerate and parse every individual entry file.
350
+ *
351
+ * @returns `true` if the snapshot was loaded successfully; `false` if it
352
+ * does not exist or could not be parsed (caller should fall back
353
+ * to a full rebuild).
354
+ */
355
+ async tryLoadPersistedIndex() {
356
+ if (!(await this.fileExists(this.metadataIndexPath))) {
357
+ return false;
358
+ }
359
+ try {
360
+ const raw = await (0, promises_1.readFile)(this.metadataIndexPath, "utf-8");
361
+ const serialized = JSON.parse(raw);
362
+ const loaded = serialized.map((s) => deserializeMetadata(s));
363
+ this.entries.clear();
364
+ this.docIndex.clear();
365
+ this.contentRefCount.clear();
366
+ this.orderedMetadata = [];
367
+ for (const metadata of loaded) {
368
+ this.entries.set(metadata.id, metadata);
369
+ if (!this.docIndex.has(metadata.docId)) {
370
+ this.docIndex.set(metadata.docId, new Set());
371
+ }
372
+ this.docIndex.get(metadata.docId).add(metadata.id);
373
+ this.contentRefCount.set(metadata.contentHash, (this.contentRefCount.get(metadata.contentHash) || 0) + 1);
374
+ this.orderedMetadata.push(metadata);
375
+ }
376
+ return true;
377
+ }
378
+ catch (err) {
379
+ this.logger.warn(`Failed to load metadata index, rebuilding from entry files: ${String(err)}`);
380
+ return false;
381
+ }
382
+ }
383
+ /**
384
+ * Validate that the in-memory index (loaded from snapshot + segments)
385
+ * is consistent with the authoritative entry files on disk.
386
+ *
387
+ * Checks two invariants:
388
+ * 1. The number of entry files matches `this.entries.size`.
389
+ * 2. Every entry file's id exists in the in-memory map.
390
+ *
391
+ * This is a cheap O(N) directory listing + set-membership check (no file
392
+ * reads) that catches stale snapshots, missing segments, and external
393
+ * mutations that happened while the process was down.
394
+ *
395
+ * @returns `true` if the index fully covers the on-disk entries.
396
+ */
397
+ async validatePersistedIndexCoverage() {
398
+ if (!this.indexingEnabled) {
399
+ return true;
400
+ }
401
+ // The entry files are the source of truth.
402
+ // Snapshot+segments are only an acceleration structure.
403
+ const files = await this.listEntryFiles();
404
+ if (files.length !== this.entries.size) {
405
+ return false;
406
+ }
407
+ for (const fileName of files) {
408
+ const entryId = fromEntryFileName(fileName);
409
+ if (!this.entries.has(entryId)) {
410
+ return false;
411
+ }
412
+ }
413
+ return true;
414
+ }
415
+ /**
416
+ * Write a full metadata-index snapshot to disk.
417
+ *
418
+ * Serializes the entire `orderedMetadata` array to `metadata-index.json`
419
+ * using an atomic write. After this, all segment files that have been
420
+ * replayed into the in-memory state are redundant and can be deleted.
421
+ */
422
+ async persistMetadataIndex() {
423
+ if (!this.indexingEnabled) {
424
+ return;
425
+ }
426
+ const serialized = JSON.stringify(this.orderedMetadata.map((m) => serializeMetadata(m)));
427
+ await this.writeFileAtomic(this.metadataIndexPath, serialized);
428
+ }
429
+ /**
430
+ * Delete the given segment files from disk and remove them from the
431
+ * `appliedMetadataSegmentFiles` tracking set.
432
+ *
433
+ * Deletion failures (e.g. race with another process) are silently ignored.
434
+ */
435
+ async clearMetadataSegments(files) {
436
+ for (const fileName of files) {
437
+ const filePath = path.join(this.metadataSegmentsDir, fileName);
438
+ await (0, promises_1.unlink)(filePath).catch(() => {
439
+ // Ignore races or already-deleted files.
440
+ });
441
+ this.appliedMetadataSegmentFiles.delete(fileName);
442
+ }
443
+ }
444
+ /**
445
+ * List all `.json` segment files in the segments directory, sorted
446
+ * lexicographically. Since segment filenames are prefixed with a
447
+ * millisecond timestamp, lexicographic order approximates chronological
448
+ * append order.
449
+ */
450
+ async listMetadataSegmentFiles() {
451
+ try {
452
+ const files = await (0, promises_1.readdir)(this.metadataSegmentsDir);
453
+ return files.filter((f) => f.endsWith(".json")).sort();
454
+ }
455
+ catch {
456
+ return [];
457
+ }
458
+ }
459
+ /**
460
+ * Replay all segment files into the in-memory index.
461
+ *
462
+ * Each segment file contains a JSON array of upsert/delete records.
463
+ * They are applied in lexicographic filename order (≈ chronological order)
464
+ * on top of whatever state the snapshot already provided.
465
+ *
466
+ * Successfully replayed files are tracked in `appliedMetadataSegmentFiles`
467
+ * so they become eligible for compaction. Corrupt or unreadable segment
468
+ * files are warned about and skipped — the subsequent
469
+ * {@link validatePersistedIndexCoverage} step will catch any resulting
470
+ * inconsistencies.
471
+ */
472
+ async replayMetadataSegments() {
473
+ // Segments are replayed in lexicographic filename order.
474
+ // Segment names encode timestamp first, so lexical order approximates append order.
475
+ const files = await this.listMetadataSegmentFiles();
476
+ for (const fileName of files) {
477
+ const filePath = path.join(this.metadataSegmentsDir, fileName);
478
+ try {
479
+ const raw = await (0, promises_1.readFile)(filePath, "utf-8");
480
+ const records = JSON.parse(raw);
481
+ for (const record of records) {
482
+ if (record.op === "upsert") {
483
+ const metadata = deserializeMetadata(record.metadata);
484
+ this.applyMetadataUpsert(metadata);
485
+ }
486
+ else if (record.op === "delete") {
487
+ this.applyMetadataDelete(record.id);
488
+ }
489
+ }
490
+ this.appliedMetadataSegmentFiles.add(fileName);
491
+ }
492
+ catch (err) {
493
+ this.logger.warn(`Failed to replay metadata segment ${fileName}: ${String(err)}`);
494
+ }
495
+ }
496
+ }
497
+ /**
498
+ * Append a new segment file containing the given mutation records.
499
+ *
500
+ * Segment files act as a lightweight write-ahead log: rather than
501
+ * rewriting the entire metadata-index snapshot on every mutation, we
502
+ * append a small file. This keeps per-write I/O proportional to the
503
+ * batch size rather than total store size.
504
+ *
505
+ * After writing, triggers {@link maybeCompactMetadataSegments} to check
506
+ * whether the accumulated segments should be folded into a fresh snapshot.
507
+ */
508
+ async appendMetadataSegment(records) {
509
+ if (!this.indexingEnabled || records.length === 0) {
510
+ return;
511
+ }
512
+ // Name carries time + pid + random suffix to minimize cross-process collisions.
513
+ const segmentName = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}.json`;
514
+ const segmentPath = path.join(this.metadataSegmentsDir, segmentName);
515
+ await this.writeFileAtomic(segmentPath, JSON.stringify(records));
516
+ this.appliedMetadataSegmentFiles.add(segmentName);
517
+ await this.maybeCompactMetadataSegments();
518
+ }
519
+ /**
520
+ * Conditionally compact metadata segments when a threshold is exceeded.
521
+ *
522
+ * Two independent thresholds can trigger compaction:
523
+ * - **File count**: the number of applied segment files ≥
524
+ * `metadataSegmentCompactionMinFiles`.
525
+ * - **Total bytes**: the cumulative size of applied segment files ≥
526
+ * `metadataSegmentCompactionMaxBytes`.
527
+ *
528
+ * Compaction writes a fresh metadata-index snapshot (which already
529
+ * includes all replayed segment data) and then deletes the covered
530
+ * segment files. Only files tracked in `appliedMetadataSegmentFiles`
531
+ * are compacted — files written concurrently by another process are
532
+ * left untouched.
533
+ *
534
+ * Compaction statistics are accumulated in `this.compactionStatus`.
535
+ */
536
+ async maybeCompactMetadataSegments() {
537
+ if (this.metadataSegmentCompactionMinFiles <= 0 &&
538
+ this.metadataSegmentCompactionMaxBytes <= 0) {
539
+ return;
540
+ }
541
+ const allSegmentFiles = await this.listMetadataSegmentFiles();
542
+ // Only compact files we have actually replayed or appended.
543
+ // This avoids deleting a file that appeared concurrently but is not yet represented
544
+ // in our in-memory state/snapshot.
545
+ const compactableFiles = allSegmentFiles.filter((fileName) => this.appliedMetadataSegmentFiles.has(fileName));
546
+ if (compactableFiles.length === 0) {
547
+ return;
548
+ }
549
+ const compactByFileCount = this.metadataSegmentCompactionMinFiles > 0 &&
550
+ compactableFiles.length >= this.metadataSegmentCompactionMinFiles;
551
+ const compactableBytes = this.metadataSegmentCompactionMaxBytes > 0
552
+ ? await this.computeSegmentFilesTotalBytes(compactableFiles)
553
+ : 0;
554
+ const compactByBytes = this.metadataSegmentCompactionMaxBytes > 0 &&
555
+ compactableBytes >= this.metadataSegmentCompactionMaxBytes;
556
+ if (!compactByFileCount && !compactByBytes) {
557
+ return;
558
+ }
559
+ const compactedFiles = compactableFiles.length;
560
+ const compactedBytes = compactableBytes > 0 ? compactableBytes : await this.computeSegmentFilesTotalBytes(compactableFiles);
561
+ const startedAt = Date.now();
562
+ // Compaction = materialize full snapshot, then delete covered segments.
563
+ await this.persistMetadataIndex();
564
+ await this.clearMetadataSegments(compactableFiles);
565
+ const durationMs = Date.now() - startedAt;
566
+ this.compactionStatus.totalCompactions += 1;
567
+ this.compactionStatus.totalCompactedFiles += compactedFiles;
568
+ this.compactionStatus.totalCompactedBytes += compactedBytes;
569
+ this.compactionStatus.totalCompactionDurationMs += durationMs;
570
+ this.compactionStatus.lastCompactionAt = Date.now();
571
+ this.compactionStatus.lastCompactedFiles = compactedFiles;
572
+ this.compactionStatus.lastCompactedBytes = compactedBytes;
573
+ this.compactionStatus.lastCompactionDurationMs = durationMs;
574
+ }
575
+ /**
576
+ * Sum the on-disk sizes of the given segment files.
577
+ * Used to evaluate the byte-based compaction threshold.
578
+ * Files that have been deleted between listing and stat are silently skipped.
579
+ */
580
+ async computeSegmentFilesTotalBytes(files) {
581
+ let totalBytes = 0;
582
+ for (const fileName of files) {
583
+ const filePath = path.join(this.metadataSegmentsDir, fileName);
584
+ try {
585
+ const stats = await (0, promises_1.stat)(filePath);
586
+ totalBytes += stats.size;
587
+ }
588
+ catch {
589
+ // Ignore races where a segment vanished after listing.
590
+ }
591
+ }
592
+ return totalBytes;
593
+ }
594
+ // ---------------------------------------------------------------------------
595
+ // In-memory index helpers
596
+ // ---------------------------------------------------------------------------
597
+ /**
598
+ * Compare two metadata entries by the canonical sort key `(createdAt, id)`.
599
+ * Used to maintain and query the `orderedMetadata` sorted array.
600
+ */
601
+ compareMetadata(a, b) {
602
+ if (a.createdAt !== b.createdAt) {
603
+ return a.createdAt - b.createdAt;
604
+ }
605
+ return a.id.localeCompare(b.id);
606
+ }
607
+ /**
608
+ * Binary search for the first index in `orderedMetadata` that is strictly
609
+ * greater than the given cursor position `(createdAt, id)`.
610
+ *
611
+ * Returns 0 when cursor is `null` (start from the beginning).
612
+ * Runs in O(log N) time.
613
+ */
614
+ lowerBoundForCursor(cursor) {
615
+ if (cursor === null) {
616
+ return 0;
617
+ }
618
+ let left = 0;
619
+ let right = this.orderedMetadata.length;
620
+ while (left < right) {
621
+ const mid = Math.floor((left + right) / 2);
622
+ const candidate = this.orderedMetadata[mid];
623
+ const greater = candidate.createdAt > cursor.createdAt ||
624
+ (candidate.createdAt === cursor.createdAt && candidate.id > cursor.id);
625
+ if (greater) {
626
+ right = mid;
627
+ }
628
+ else {
629
+ left = mid + 1;
630
+ }
631
+ }
632
+ return left;
633
+ }
634
+ /**
635
+ * Test whether a metadata entry passes the given scan filters.
636
+ * Evaluates docId, entryTypes whitelist, and creationDate range.
637
+ * Returns `true` when no filters are provided or all checks pass.
638
+ */
639
+ metadataMatchesFilters(meta, filters) {
640
+ if (filters?.docId && meta.docId !== filters.docId) {
641
+ return false;
642
+ }
643
+ if (filters?.entryTypes && filters.entryTypes.length > 0 && !filters.entryTypes.includes(meta.entryType)) {
644
+ return false;
645
+ }
646
+ if (filters?.creationDateFrom !== undefined && filters.creationDateFrom !== null && meta.createdAt < filters.creationDateFrom) {
647
+ return false;
648
+ }
649
+ if (filters?.creationDateUntil !== undefined && filters.creationDateUntil !== null && meta.createdAt >= filters.creationDateUntil) {
650
+ return false;
651
+ }
652
+ return true;
653
+ }
654
+ /**
655
+ * Insert a metadata entry into `orderedMetadata` at the correct sorted
656
+ * position using binary search + splice. O(log N) search + O(N) splice.
657
+ */
658
+ insertOrderedMetadata(meta) {
659
+ let left = 0;
660
+ let right = this.orderedMetadata.length;
661
+ while (left < right) {
662
+ const mid = Math.floor((left + right) / 2);
663
+ if (this.compareMetadata(this.orderedMetadata[mid], meta) < 0) {
664
+ left = mid + 1;
665
+ }
666
+ else {
667
+ right = mid;
668
+ }
669
+ }
670
+ this.orderedMetadata.splice(left, 0, meta);
671
+ }
672
+ /**
673
+ * Remove an entry from `orderedMetadata` by id.
674
+ * Uses a linear scan; acceptable because removals (purge) are rare.
675
+ */
676
+ removeOrderedMetadataById(id) {
677
+ const idx = this.orderedMetadata.findIndex((m) => m.id === id);
678
+ if (idx >= 0) {
679
+ this.orderedMetadata.splice(idx, 1);
680
+ }
681
+ }
682
+ /**
683
+ * Remove an entry from all in-memory indexes.
684
+ *
685
+ * Updates `entries`, `docIndex`, `orderedMetadata`, and `contentRefCount`.
686
+ * When the content hash reference count drops to zero, the hash is removed
687
+ * from the ref-count map (the caller is responsible for deleting the
688
+ * on-disk content file if needed).
689
+ */
690
+ applyMetadataDelete(id) {
691
+ const existing = this.entries.get(id);
692
+ if (!existing) {
693
+ return;
694
+ }
695
+ this.entries.delete(id);
696
+ this.docIndex.get(existing.docId)?.delete(id);
697
+ if ((this.docIndex.get(existing.docId)?.size || 0) === 0) {
698
+ this.docIndex.delete(existing.docId);
699
+ }
700
+ this.removeOrderedMetadataById(id);
701
+ const nextRef = (this.contentRefCount.get(existing.contentHash) || 1) - 1;
702
+ if (nextRef <= 0) {
703
+ this.contentRefCount.delete(existing.contentHash);
704
+ }
705
+ else {
706
+ this.contentRefCount.set(existing.contentHash, nextRef);
707
+ }
708
+ }
709
+ /**
710
+ * Insert or replace an entry in all in-memory indexes.
711
+ *
712
+ * If the id already exists, the old entry is removed first (via
713
+ * {@link applyMetadataDelete}) before the new one is inserted.
714
+ * Updates `entries`, `docIndex`, `orderedMetadata`, and `contentRefCount`.
715
+ */
716
+ applyMetadataUpsert(metadata) {
717
+ const existing = this.entries.get(metadata.id);
718
+ if (existing) {
719
+ this.applyMetadataDelete(metadata.id);
720
+ }
721
+ this.entries.set(metadata.id, metadata);
722
+ if (!this.docIndex.has(metadata.docId)) {
723
+ this.docIndex.set(metadata.docId, new Set());
724
+ }
725
+ this.docIndex.get(metadata.docId).add(metadata.id);
726
+ this.contentRefCount.set(metadata.contentHash, (this.contentRefCount.get(metadata.contentHash) || 0) + 1);
727
+ this.insertOrderedMetadata(metadata);
728
+ }
729
+ // ---------------------------------------------------------------------------
730
+ // Filesystem I/O helpers
731
+ // ---------------------------------------------------------------------------
732
+ /** List all `.json` files in the `entries/` directory. */
733
+ async listEntryFiles() {
734
+ try {
735
+ const files = await (0, promises_1.readdir)(this.entriesDir);
736
+ return files.filter((f) => f.endsWith(".json"));
737
+ }
738
+ catch {
739
+ return [];
740
+ }
741
+ }
742
+ /** Resolve the absolute path to the metadata file for a given entry id. */
743
+ metadataPathForId(id) {
744
+ return path.join(this.entriesDir, toEntryFileName(id));
745
+ }
746
+ /** Resolve the absolute path to the binary content file for a given hash. */
747
+ contentPathForHash(contentHash) {
748
+ return path.join(this.contentDir, `${contentHash}.bin`);
749
+ }
750
+ /** Check whether a file exists without reading its contents. */
751
+ async fileExists(filePath) {
752
+ try {
753
+ await (0, promises_1.access)(filePath, fs_1.constants.F_OK);
754
+ return true;
755
+ }
756
+ catch {
757
+ return false;
758
+ }
759
+ }
760
+ /**
761
+ * Write data to a file with full crash-safety guarantees.
762
+ *
763
+ * Protocol:
764
+ * 1. Write to a temporary file (unique per process + timestamp + random).
765
+ * 2. `fsync` the temp file to flush OS buffers to disk.
766
+ * 3. Atomically `rename` temp → final path (POSIX guarantees atomicity).
767
+ * 4. `fsync` the parent directory so the new directory entry is durable.
768
+ *
769
+ * This ensures readers never observe a partially written file, even after
770
+ * an unclean shutdown.
771
+ */
772
+ async writeFileAtomic(filePath, data) {
773
+ // Durability protocol:
774
+ // 1) write temp file
775
+ // 2) fsync temp file contents
776
+ // 3) atomic rename temp -> final
777
+ // 4) fsync parent directory (persist directory entry update)
778
+ const tmpPath = tempPathFor(filePath);
779
+ const tmpHandle = await (0, promises_1.open)(tmpPath, "w");
780
+ try {
781
+ await tmpHandle.writeFile(data);
782
+ await tmpHandle.sync();
783
+ }
784
+ finally {
785
+ await tmpHandle.close();
786
+ }
787
+ await (0, promises_1.rename)(tmpPath, filePath);
788
+ await this.syncParentDirectory(filePath);
789
+ }
790
+ /**
791
+ * Fsync the parent directory of a file to ensure its directory entry
792
+ * update is persisted. Required on POSIX systems after rename/unlink
793
+ * to guarantee that the directory metadata change survives a crash.
794
+ *
795
+ * Failures are logged at debug level and swallowed — some platforms
796
+ * (e.g. certain macOS configurations) do not support opening directories
797
+ * for fsync.
798
+ */
799
+ async syncParentDirectory(filePath) {
800
+ const dirPath = path.dirname(filePath);
801
+ try {
802
+ const dirHandle = await (0, promises_1.open)(dirPath, "r");
803
+ try {
804
+ await dirHandle.sync();
805
+ }
806
+ finally {
807
+ await dirHandle.close();
808
+ }
809
+ }
810
+ catch (err) {
811
+ this.logger.debug(`Directory fsync skipped for ${dirPath}: ${String(err)}`);
812
+ }
813
+ }
814
+ /**
815
+ * Look up an entry's metadata by id.
816
+ *
817
+ * When indexing is enabled, this is an O(1) in-memory map lookup.
818
+ * Otherwise, falls back to reading the entry file from disk.
819
+ *
820
+ * @returns The metadata if found, or `null`.
821
+ */
822
+ async readMetadataById(id) {
823
+ if (this.indexingEnabled) {
824
+ return this.entries.get(id) || null;
825
+ }
826
+ const filePath = this.metadataPathForId(id);
827
+ if (!(await this.fileExists(filePath))) {
828
+ return null;
829
+ }
830
+ const raw = await (0, promises_1.readFile)(filePath, "utf-8");
831
+ return deserializeMetadata(JSON.parse(raw));
832
+ }
833
+ /**
834
+ * Read and deserialize a metadata entry directly from its on-disk file.
835
+ * Used during index rebuilds and non-indexed fallback paths.
836
+ * Parse failures are logged and result in `null`.
837
+ */
838
+ async readMetadataByFileName(fileName) {
839
+ const filePath = path.join(this.entriesDir, fileName);
840
+ try {
841
+ const raw = await (0, promises_1.readFile)(filePath, "utf-8");
842
+ const metadata = deserializeMetadata(JSON.parse(raw));
843
+ return metadata;
844
+ }
845
+ catch (err) {
846
+ this.logger.warn(`Failed to read metadata file ${fileName}: ${String(err)}`);
847
+ return null;
848
+ }
849
+ }
850
+ /**
851
+ * Return all metadata entries in the store.
852
+ *
853
+ * When indexing is enabled, returns a shallow copy of `orderedMetadata`
854
+ * (already sorted). Otherwise, reads and parses every entry file from
855
+ * disk (unsorted).
856
+ */
857
+ async listAllMetadata() {
858
+ if (this.indexingEnabled) {
859
+ return [...this.orderedMetadata];
860
+ }
861
+ const files = await this.listEntryFiles();
862
+ const result = [];
863
+ for (const fileName of files) {
864
+ const metadata = await this.readMetadataByFileName(fileName);
865
+ if (metadata) {
866
+ result.push(metadata);
867
+ }
868
+ }
869
+ return result;
870
+ }
871
+ // ---------------------------------------------------------------------------
872
+ // ContentAddressedStore interface — public API
873
+ // ---------------------------------------------------------------------------
874
+ /** Return the database id this store was created for. */
875
+ getId() {
876
+ return this.dbId;
877
+ }
878
+ getCacheIdentity() {
879
+ return `disk:${this.storeRoot}`;
880
+ }
881
+ /**
882
+ * Persist one or more entries to disk.
883
+ *
884
+ * For each entry the method:
885
+ * 1. Skips if the metadata file already exists (content-addressed idempotency).
886
+ * 2. Writes the encrypted payload to `content/<hash>.bin` (deduplicated by hash).
887
+ * 3. Writes the metadata to `entries/<id>.json`.
888
+ * 4. Updates the in-memory index and appends a segment record.
889
+ *
890
+ * The commit order (content → metadata → segment) ensures that a crash at
891
+ * any point leaves the store in a consistent state: orphaned content blobs
892
+ * are harmless, and the index can always be rebuilt from the metadata files.
893
+ */
894
+ async putEntries(entries) {
895
+ await this.ensureInitialized();
896
+ let hasMutation = false;
897
+ const segmentRecords = [];
898
+ for (const entry of entries) {
899
+ const metadataPath = this.metadataPathForId(entry.id);
900
+ if (await this.fileExists(metadataPath)) {
901
+ continue;
902
+ }
903
+ // Commit order is intentional:
904
+ // - write payload by content hash first (deduplicated)
905
+ // - write metadata by id second (entry becomes discoverable)
906
+ // - append segment record last (index acceleration catches up)
907
+ const contentPath = this.contentPathForHash(entry.contentHash);
908
+ if (!(await this.fileExists(contentPath))) {
909
+ await this.writeFileAtomic(contentPath, entry.encryptedData);
910
+ }
911
+ const { encryptedData, ...metadata } = entry;
912
+ const serialized = JSON.stringify(serializeMetadata(metadata));
913
+ await this.writeFileAtomic(metadataPath, serialized);
914
+ hasMutation = true;
915
+ if (this.indexingEnabled) {
916
+ this.applyMetadataUpsert(metadata);
917
+ segmentRecords.push({
918
+ op: "upsert",
919
+ metadata: serializeMetadata(metadata),
920
+ });
921
+ }
922
+ }
923
+ if (hasMutation && this.indexingEnabled) {
924
+ await this.appendMetadataSegment(segmentRecords);
925
+ }
926
+ }
927
+ /**
928
+ * Retrieve full entries (metadata + encrypted payload) by id.
929
+ *
930
+ * IDs that are not found or whose content file is missing are silently
931
+ * omitted from the result. When indexing is enabled, the metadata lookup
932
+ * is O(1); the content read is always a disk I/O.
933
+ */
934
+ async getEntries(ids) {
935
+ await this.ensureInitialized();
936
+ const result = [];
937
+ for (const id of ids) {
938
+ const metadata = await this.readMetadataById(id);
939
+ if (!metadata) {
940
+ continue;
941
+ }
942
+ const contentPath = this.contentPathForHash(metadata.contentHash);
943
+ if (!(await this.fileExists(contentPath))) {
944
+ this.logger.warn(`Content ${metadata.contentHash} not found for entry ${id}`);
945
+ continue;
946
+ }
947
+ const encryptedData = new Uint8Array(await (0, promises_1.readFile)(contentPath));
948
+ result.push({ ...metadata, encryptedData });
949
+ }
950
+ return result;
951
+ }
952
+ /**
953
+ * Check which of the given ids exist in the store.
954
+ *
955
+ * When indexing is enabled, this is a fast in-memory set-membership test.
956
+ * Otherwise, it checks for the existence of the corresponding metadata
957
+ * file on disk.
958
+ *
959
+ * @returns The subset of `ids` that are present in the store.
960
+ */
961
+ async hasEntries(ids) {
962
+ await this.ensureInitialized();
963
+ const existing = [];
964
+ for (const id of ids) {
965
+ if (this.indexingEnabled) {
966
+ if (this.entries.has(id)) {
967
+ existing.push(id);
968
+ }
969
+ }
970
+ else {
971
+ if (await this.fileExists(this.metadataPathForId(id))) {
972
+ existing.push(id);
973
+ }
974
+ }
975
+ }
976
+ return existing;
977
+ }
978
+ /**
979
+ * Find all entries whose ids are not in `knownIds`.
980
+ *
981
+ * Used during sync to discover entries the caller has not yet seen.
982
+ * Returns metadata only (no payload).
983
+ */
984
+ async findNewEntries(knownIds) {
985
+ await this.ensureInitialized();
986
+ const known = new Set(knownIds);
987
+ const all = await this.listAllMetadata();
988
+ return all.filter((meta) => !known.has(meta.id));
989
+ }
990
+ /**
991
+ * Find entries for a specific document that are not in `knownIds`.
992
+ *
993
+ * When indexing is enabled, leverages the `docIndex` for an O(K) scan
994
+ * (K = entries for this doc) instead of the full store.
995
+ */
996
+ async findNewEntriesForDoc(knownIds, docId) {
997
+ await this.ensureInitialized();
998
+ const known = new Set(knownIds);
999
+ if (this.indexingEnabled) {
1000
+ const ids = this.docIndex.get(docId) || new Set();
1001
+ const result = [];
1002
+ for (const id of ids) {
1003
+ if (known.has(id)) {
1004
+ continue;
1005
+ }
1006
+ const metadata = this.entries.get(id);
1007
+ if (metadata) {
1008
+ result.push(metadata);
1009
+ }
1010
+ }
1011
+ return result;
1012
+ }
1013
+ const all = await this.listAllMetadata();
1014
+ return all.filter((meta) => meta.docId === docId && !known.has(meta.id));
1015
+ }
1016
+ /**
1017
+ * Find entries matching a given type and optional creation-date range.
1018
+ *
1019
+ * @param type Entry type to filter by (e.g. "doc_change").
1020
+ * @param creationDateFrom Inclusive lower bound (epoch ms), or `null` for no lower bound.
1021
+ * @param creationDateUntil Exclusive upper bound (epoch ms), or `null` for no upper bound.
1022
+ * @returns Metadata of all matching entries.
1023
+ */
1024
+ async findEntries(type, creationDateFrom, creationDateUntil) {
1025
+ await this.ensureInitialized();
1026
+ const all = await this.listAllMetadata();
1027
+ return all.filter((meta) => {
1028
+ if (meta.entryType !== type) {
1029
+ return false;
1030
+ }
1031
+ if (creationDateFrom !== null && meta.createdAt < creationDateFrom) {
1032
+ return false;
1033
+ }
1034
+ if (creationDateUntil !== null && meta.createdAt >= creationDateUntil) {
1035
+ return false;
1036
+ }
1037
+ return true;
1038
+ });
1039
+ }
1040
+ /**
1041
+ * Return all entry ids in the store.
1042
+ *
1043
+ * When indexing is enabled, returns ids in `(createdAt, id)` order.
1044
+ * Without indexing, order depends on the filesystem directory listing.
1045
+ */
1046
+ async getAllIds() {
1047
+ await this.ensureInitialized();
1048
+ if (this.indexingEnabled) {
1049
+ return this.orderedMetadata.map((m) => m.id);
1050
+ }
1051
+ const files = await this.listEntryFiles();
1052
+ return files.map(fromEntryFileName);
1053
+ }
1054
+ /**
1055
+ * Cursor-based paginated scan over entry metadata.
1056
+ *
1057
+ * Designed for high-volume synchronization: the caller repeatedly advances
1058
+ * a cursor to stream through all entries in deterministic
1059
+ * `(createdAt ASC, id ASC)` order, optionally applying filters.
1060
+ *
1061
+ * **Indexed path** (default):
1062
+ * - O(log N) binary search for the cursor position.
1063
+ * - O(pageSize) slice for unfiltered scans; O(scan window) for filtered.
1064
+ *
1065
+ * **Non-indexed fallback**:
1066
+ * - Reads and sorts *all* metadata on every call. Suitable only for
1067
+ * small stores or when indexing is explicitly disabled.
1068
+ *
1069
+ * @param cursor Position to resume from (exclusive), or `null` to start
1070
+ * from the beginning.
1071
+ * @param limit Maximum number of entries to return in this page.
1072
+ * @param filters Optional filters (docId, entryTypes, date range).
1073
+ * @returns A page of metadata entries, the next cursor, and a `hasMore` flag.
1074
+ */
1075
+ async scanEntriesSince(cursor, limit = Number.MAX_SAFE_INTEGER, filters) {
1076
+ await this.ensureInitialized();
1077
+ if (this.indexingEnabled) {
1078
+ // Indexed path: O(logN) lower-bound + O(pageSize) slice/filter.
1079
+ const startIndex = this.lowerBoundForCursor(cursor);
1080
+ if (!filters || Object.keys(filters).length === 0) {
1081
+ const page = this.orderedMetadata.slice(startIndex, startIndex + limit);
1082
+ const last = page.length > 0 ? page[page.length - 1] : null;
1083
+ return {
1084
+ entries: page,
1085
+ nextCursor: last ? { createdAt: last.createdAt, id: last.id } : cursor,
1086
+ hasMore: startIndex + page.length < this.orderedMetadata.length,
1087
+ };
1088
+ }
1089
+ const page = [];
1090
+ let idx = startIndex;
1091
+ while (idx < this.orderedMetadata.length && page.length < limit) {
1092
+ const meta = this.orderedMetadata[idx];
1093
+ if (this.metadataMatchesFilters(meta, filters)) {
1094
+ page.push(meta);
1095
+ }
1096
+ idx++;
1097
+ }
1098
+ let hasMore = false;
1099
+ while (idx < this.orderedMetadata.length) {
1100
+ if (this.metadataMatchesFilters(this.orderedMetadata[idx], filters)) {
1101
+ hasMore = true;
1102
+ break;
1103
+ }
1104
+ idx++;
1105
+ }
1106
+ const last = page.length > 0 ? page[page.length - 1] : null;
1107
+ return {
1108
+ entries: page,
1109
+ nextCursor: last ? { createdAt: last.createdAt, id: last.id } : cursor,
1110
+ hasMore,
1111
+ };
1112
+ }
1113
+ // Non-indexed fallback: scan and sort all metadata on demand.
1114
+ const all = (await this.listAllMetadata())
1115
+ .filter((meta) => {
1116
+ if (filters?.docId && meta.docId !== filters.docId) {
1117
+ return false;
1118
+ }
1119
+ if (filters?.entryTypes && filters.entryTypes.length > 0 && !filters.entryTypes.includes(meta.entryType)) {
1120
+ return false;
1121
+ }
1122
+ if (filters?.creationDateFrom !== undefined && filters.creationDateFrom !== null && meta.createdAt < filters.creationDateFrom) {
1123
+ return false;
1124
+ }
1125
+ if (filters?.creationDateUntil !== undefined && filters.creationDateUntil !== null && meta.createdAt >= filters.creationDateUntil) {
1126
+ return false;
1127
+ }
1128
+ return true;
1129
+ })
1130
+ .sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt - b.createdAt));
1131
+ const startIndex = cursor === null
1132
+ ? 0
1133
+ : all.findIndex((meta) => meta.createdAt > cursor.createdAt || (meta.createdAt === cursor.createdAt && meta.id > cursor.id));
1134
+ if (startIndex === -1) {
1135
+ return { entries: [], nextCursor: cursor, hasMore: false };
1136
+ }
1137
+ const page = all.slice(startIndex, startIndex + limit);
1138
+ const last = page.length > 0 ? page[page.length - 1] : null;
1139
+ return {
1140
+ entries: page,
1141
+ nextCursor: last ? { createdAt: last.createdAt, id: last.id } : cursor,
1142
+ hasMore: startIndex + page.length < all.length,
1143
+ };
1144
+ }
1145
+ /**
1146
+ * Build a bloom filter summary of all entry ids in this store.
1147
+ *
1148
+ * The summary is a compact probabilistic set representation that a remote
1149
+ * peer can use during sync to cheaply test which of its own ids are
1150
+ * *definitely absent* from this store — avoiding per-id network
1151
+ * round-trips. See {@link createIdBloomSummary} for details on the bloom
1152
+ * filter implementation.
1153
+ *
1154
+ * @returns A serializable {@link StoreIdBloomSummary} with a base64-encoded
1155
+ * bitset, suitable for transmission over the network.
1156
+ */
1157
+ async getIdBloomSummary() {
1158
+ await this.ensureInitialized();
1159
+ const ids = await this.getAllIds();
1160
+ return (0, bloom_1.createIdBloomSummary)(ids);
1161
+ }
1162
+ async planDocumentMaterialization(docId, options) {
1163
+ await this.ensureInitialized();
1164
+ let entries = [];
1165
+ if (this.indexingEnabled) {
1166
+ const ids = this.docIndex.get(docId) || new Set();
1167
+ entries = Array.from(ids)
1168
+ .map((id) => this.entries.get(id))
1169
+ .filter((m) => m !== undefined);
1170
+ }
1171
+ else {
1172
+ const all = await this.listAllMetadata();
1173
+ entries = all.filter((m) => m.docId === docId);
1174
+ }
1175
+ return (0, MaterializationPlanner_1.computeDocumentMaterializationPlan)(docId, entries, options);
1176
+ }
1177
+ async planDocumentMaterializationBatch(docIds, options) {
1178
+ await this.ensureInitialized();
1179
+ const all = await this.listAllMetadata();
1180
+ return (0, MaterializationPlanner_1.computeBatchMaterializationPlan)(all, docIds, options);
1181
+ }
1182
+ /**
1183
+ * Traverse the dependency graph starting from a given entry id.
1184
+ *
1185
+ * Performs a breadth-first traversal through `dependencyIds` links,
1186
+ * collecting all reachable entries. The result is returned in
1187
+ * **dependency-first order** (deepest dependencies first) so callers
1188
+ * can process entries from oldest to newest.
1189
+ *
1190
+ * Use cases:
1191
+ * - Loading all chunks of a large attachment (traverse from the last
1192
+ * chunk back to the first).
1193
+ * - Reconstructing a document state by collecting all changes.
1194
+ * - Stopping at snapshots to avoid loading the full history.
1195
+ *
1196
+ * @param startId The entry id to begin traversal from.
1197
+ * @param options Optional traversal controls:
1198
+ * - `stopAtEntryType` — stop expanding when hitting this type (the
1199
+ * matching entry is still included).
1200
+ * - `maxDepth` — maximum number of hops from `startId`.
1201
+ * - `includeStart` — whether `startId` itself appears in the result
1202
+ * (default `true`).
1203
+ * @returns Entry ids in dependency-first order.
1204
+ */
1205
+ async resolveDependencies(startId, options) {
1206
+ await this.ensureInitialized();
1207
+ const stopAtEntryType = options?.stopAtEntryType;
1208
+ const maxDepth = options?.maxDepth;
1209
+ const includeStart = options?.includeStart !== false;
1210
+ // Breadth-first traversal with a visited set.
1211
+ // We reverse at the end to return dependency-first order.
1212
+ const result = [];
1213
+ const visited = new Set();
1214
+ const queue = [{ id: startId, depth: 0 }];
1215
+ while (queue.length > 0) {
1216
+ const { id, depth } = queue.shift();
1217
+ if (visited.has(id)) {
1218
+ continue;
1219
+ }
1220
+ visited.add(id);
1221
+ if (maxDepth !== undefined && depth > maxDepth) {
1222
+ continue;
1223
+ }
1224
+ const entry = await this.readMetadataById(id);
1225
+ if (!entry) {
1226
+ continue;
1227
+ }
1228
+ if (stopAtEntryType && entry.entryType === stopAtEntryType && id !== startId) {
1229
+ result.push(id);
1230
+ continue;
1231
+ }
1232
+ if (id !== startId || includeStart) {
1233
+ result.push(id);
1234
+ }
1235
+ for (const depId of entry.dependencyIds) {
1236
+ if (!visited.has(depId)) {
1237
+ queue.push({ id: depId, depth: depth + 1 });
1238
+ }
1239
+ }
1240
+ }
1241
+ result.reverse();
1242
+ return result;
1243
+ }
1244
+ /**
1245
+ * Permanently delete all entries belonging to a document (GDPR purge).
1246
+ *
1247
+ * Steps:
1248
+ * 1. Identify all metadata entries with the given `docId`.
1249
+ * 2. Delete each metadata file and update the in-memory index.
1250
+ * 3. Delete orphaned content blobs — content files whose hash is no
1251
+ * longer referenced by any remaining entry.
1252
+ * 4. Append "delete" segment records so the index stays consistent
1253
+ * across restarts.
1254
+ *
1255
+ * @param docId The document whose entire history should be erased.
1256
+ */
1257
+ async purgeDocHistory(docId) {
1258
+ await this.ensureInitialized();
1259
+ const all = await this.listAllMetadata();
1260
+ const toDelete = all.filter((meta) => meta.docId === docId);
1261
+ if (toDelete.length === 0) {
1262
+ return;
1263
+ }
1264
+ // GDPR purge removes all metadata for one document, then drops orphaned payloads.
1265
+ const deletedHashes = new Set();
1266
+ const deletedIds = [];
1267
+ for (const meta of toDelete) {
1268
+ deletedHashes.add(meta.contentHash);
1269
+ deletedIds.push(meta.id);
1270
+ const metadataPath = this.metadataPathForId(meta.id);
1271
+ await (0, promises_1.unlink)(metadataPath).catch(() => {
1272
+ // Ignore races or already-deleted files.
1273
+ });
1274
+ await this.syncParentDirectory(metadataPath);
1275
+ if (this.indexingEnabled) {
1276
+ this.applyMetadataDelete(meta.id);
1277
+ }
1278
+ }
1279
+ const remaining = await this.listAllMetadata();
1280
+ const remainingHashes = new Set(remaining.map((m) => m.contentHash));
1281
+ for (const hash of deletedHashes) {
1282
+ if (!remainingHashes.has(hash)) {
1283
+ const contentPath = this.contentPathForHash(hash);
1284
+ await (0, promises_1.unlink)(contentPath).catch(() => {
1285
+ // Ignore races or already-deleted files.
1286
+ });
1287
+ await this.syncParentDirectory(contentPath);
1288
+ }
1289
+ }
1290
+ if (this.indexingEnabled) {
1291
+ await this.appendMetadataSegment(deletedIds.map((id) => ({ op: "delete", id })));
1292
+ }
1293
+ }
1294
+ /**
1295
+ * Delete all data for this database and recreate empty directories.
1296
+ *
1297
+ * Resets the index status to "building" temporarily, removes the entire
1298
+ * store root, and re-creates the directory structure. The index is
1299
+ * immediately marked "ready" afterwards since the store is now empty.
1300
+ */
1301
+ async clearAllLocalData() {
1302
+ await this.ensureInitialized();
1303
+ this.indexStatus = {
1304
+ phase: this.indexingEnabled ? "building" : "ready",
1305
+ indexingEnabled: this.indexingEnabled,
1306
+ progress01: this.indexingEnabled ? 0 : 1,
1307
+ };
1308
+ await this.clearAllLocalDataInternal();
1309
+ await (0, promises_1.mkdir)(this.entriesDir, { recursive: true });
1310
+ await (0, promises_1.mkdir)(this.contentDir, { recursive: true });
1311
+ await (0, promises_1.mkdir)(this.metadataSegmentsDir, { recursive: true });
1312
+ if (this.indexingEnabled) {
1313
+ this.indexStatus = { phase: "ready", indexingEnabled: true, progress01: 1 };
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Wait until the in-memory index has finished building and return its
1318
+ * status. Since initialization is fully awaited via `initPromise`, this
1319
+ * effectively waits for the constructor's async init to complete.
1320
+ */
1321
+ async awaitIndexReady(_options) {
1322
+ await this.ensureInitialized();
1323
+ return this.indexStatus;
1324
+ }
1325
+ /** Return the current index build status without waiting. */
1326
+ getIndexBuildStatus() {
1327
+ return this.indexStatus;
1328
+ }
1329
+ /** Return a snapshot of cumulative metadata-segment compaction statistics. */
1330
+ async getCompactionStatus() {
1331
+ return { ...this.compactionStatus };
1332
+ }
1333
+ }
1334
+ exports.BasicOnDiskContentAddressedStore = BasicOnDiskContentAddressedStore;
1335
+ /**
1336
+ * Factory that creates {@link BasicOnDiskContentAddressedStore} instances.
1337
+ *
1338
+ * Implements the {@link ContentAddressedStoreFactory} interface so it can be
1339
+ * plugged into MindooDB's store creation pipeline. Each call to
1340
+ * `createStore` returns a fresh store backed by its own on-disk directory
1341
+ * under the configured base path.
1342
+ */
1343
+ class BasicOnDiskContentAddressedStoreFactory {
1344
+ /**
1345
+ * Create a new on-disk content-addressed store for the given database.
1346
+ *
1347
+ * @param dbId Unique database identifier (becomes the directory name).
1348
+ * @param options Store configuration forwarded to the constructor.
1349
+ * @returns A {@link CreateStoreResult} containing the new store as `docStore`.
1350
+ */
1351
+ createStore(dbId, options) {
1352
+ return {
1353
+ docStore: new BasicOnDiskContentAddressedStore(dbId, undefined, options),
1354
+ };
1355
+ }
1356
+ }
1357
+ exports.BasicOnDiskContentAddressedStoreFactory = BasicOnDiskContentAddressedStoreFactory;
1358
+ //# sourceMappingURL=BasicOnDiskContentAddressedStore.js.map