@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/DESIGN.md +421 -0
  5. package/components/OptionsWatcher.ts +368 -102
  6. package/components/Scope.ts +20 -1
  7. package/components/componentLoader.ts +43 -4
  8. package/components/deploymentOperations.ts +4 -1
  9. package/components/deploymentRecorder.ts +9 -2
  10. package/components/mcp/DESIGN.md +109 -0
  11. package/components/mcp/audit.ts +21 -17
  12. package/components/operations.js +284 -52
  13. package/components/operationsValidation.js +49 -2
  14. package/components/packageComponent.ts +25 -1
  15. package/components/requestRestart.ts +11 -0
  16. package/config/DESIGN.md +306 -0
  17. package/config/RootConfigWatcher.ts +191 -37
  18. package/config/configReadRetry.ts +62 -0
  19. package/config/configUtils.ts +78 -26
  20. package/config/parseConfigFile.ts +34 -0
  21. package/config/readConfigFileSync.ts +44 -0
  22. package/config/watcherArming.ts +59 -0
  23. package/config-root.schema.json +4 -0
  24. package/dataLayer/DESIGN.md +179 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  26. package/dataLayer/restoreMarker.ts +92 -25
  27. package/dist/bin/cliOperations.js +13 -0
  28. package/dist/bin/cliOperations.js.map +1 -1
  29. package/dist/bin/restart.js +42 -6
  30. package/dist/bin/restart.js.map +1 -1
  31. package/dist/components/Application.d.ts +104 -9
  32. package/dist/components/Application.js +954 -102
  33. package/dist/components/Application.js.map +1 -1
  34. package/dist/components/OptionsWatcher.d.ts +4 -1
  35. package/dist/components/OptionsWatcher.js +378 -104
  36. package/dist/components/OptionsWatcher.js.map +1 -1
  37. package/dist/components/Scope.js +15 -1
  38. package/dist/components/Scope.js.map +1 -1
  39. package/dist/components/componentLoader.js +35 -3
  40. package/dist/components/componentLoader.js.map +1 -1
  41. package/dist/components/deploymentOperations.js +4 -1
  42. package/dist/components/deploymentOperations.js.map +1 -1
  43. package/dist/components/deploymentRecorder.d.ts +4 -2
  44. package/dist/components/deploymentRecorder.js +1 -0
  45. package/dist/components/deploymentRecorder.js.map +1 -1
  46. package/dist/components/mcp/audit.d.ts +2 -1
  47. package/dist/components/mcp/audit.js +21 -17
  48. package/dist/components/mcp/audit.js.map +1 -1
  49. package/dist/components/operations.d.ts +28 -0
  50. package/dist/components/operations.js +263 -54
  51. package/dist/components/operations.js.map +1 -1
  52. package/dist/components/operationsValidation.js +48 -2
  53. package/dist/components/operationsValidation.js.map +1 -1
  54. package/dist/components/packageComponent.js +24 -0
  55. package/dist/components/packageComponent.js.map +1 -1
  56. package/dist/components/requestRestart.d.ts +1 -0
  57. package/dist/components/requestRestart.js +7 -0
  58. package/dist/components/requestRestart.js.map +1 -1
  59. package/dist/config/RootConfigWatcher.d.ts +2 -0
  60. package/dist/config/RootConfigWatcher.js +189 -35
  61. package/dist/config/RootConfigWatcher.js.map +1 -1
  62. package/dist/config/configReadRetry.d.ts +8 -0
  63. package/dist/config/configReadRetry.js +62 -0
  64. package/dist/config/configReadRetry.js.map +1 -0
  65. package/dist/config/configUtils.d.ts +10 -9
  66. package/dist/config/configUtils.js +63 -27
  67. package/dist/config/configUtils.js.map +1 -1
  68. package/dist/config/parseConfigFile.d.ts +4 -0
  69. package/dist/config/parseConfigFile.js +35 -0
  70. package/dist/config/parseConfigFile.js.map +1 -0
  71. package/dist/config/readConfigFileSync.d.ts +1 -0
  72. package/dist/config/readConfigFileSync.js +47 -0
  73. package/dist/config/readConfigFileSync.js.map +1 -0
  74. package/dist/config/watcherArming.d.ts +15 -0
  75. package/dist/config/watcherArming.js +59 -0
  76. package/dist/config/watcherArming.js.map +1 -0
  77. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  78. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  79. package/dist/dataLayer/restoreMarker.d.ts +21 -8
  80. package/dist/dataLayer/restoreMarker.js +94 -27
  81. package/dist/dataLayer/restoreMarker.js.map +1 -1
  82. package/dist/index.d.ts +2 -0
  83. package/dist/index.js +9 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/json/systemSchema.json +3 -0
  86. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  87. package/dist/resources/DatabaseTransaction.js +224 -18
  88. package/dist/resources/DatabaseTransaction.js.map +1 -1
  89. package/dist/resources/LMDBTransaction.d.ts +2 -1
  90. package/dist/resources/LMDBTransaction.js +22 -3
  91. package/dist/resources/LMDBTransaction.js.map +1 -1
  92. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  93. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  94. package/dist/resources/RecordEncoder.d.ts +1 -1
  95. package/dist/resources/RecordEncoder.js +46 -10
  96. package/dist/resources/RecordEncoder.js.map +1 -1
  97. package/dist/resources/Resource.js +97 -13
  98. package/dist/resources/Resource.js.map +1 -1
  99. package/dist/resources/ResourceInterface.d.ts +8 -0
  100. package/dist/resources/RocksIndexStore.js +2 -1
  101. package/dist/resources/RocksIndexStore.js.map +1 -1
  102. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  103. package/dist/resources/RocksTransactionLogStore.js +104 -33
  104. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  105. package/dist/resources/Table.d.ts +58 -7
  106. package/dist/resources/Table.js +1143 -348
  107. package/dist/resources/Table.js.map +1 -1
  108. package/dist/resources/analytics/write.d.ts +3 -0
  109. package/dist/resources/analytics/write.js +49 -16
  110. package/dist/resources/analytics/write.js.map +1 -1
  111. package/dist/resources/auditStore.d.ts +170 -0
  112. package/dist/resources/auditStore.js +457 -11
  113. package/dist/resources/auditStore.js.map +1 -1
  114. package/dist/resources/crdt.d.ts +10 -0
  115. package/dist/resources/crdt.js +22 -0
  116. package/dist/resources/crdt.js.map +1 -1
  117. package/dist/resources/dataLoader.js +3 -4
  118. package/dist/resources/dataLoader.js.map +1 -1
  119. package/dist/resources/databases.d.ts +16 -13
  120. package/dist/resources/databases.js +625 -177
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  123. package/dist/resources/derivedIndexRegistry.js +68 -0
  124. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  125. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  126. package/dist/resources/derivedIndexRuntime.js +2027 -0
  127. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  128. package/dist/resources/graphql.js +3 -2
  129. package/dist/resources/graphql.js.map +1 -1
  130. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  131. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +867 -39
  132. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  133. package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
  134. package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
  135. package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
  136. package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
  137. package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
  138. package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
  139. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  140. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  141. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  142. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  143. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  144. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  145. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
  146. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
  147. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
  148. package/dist/resources/nodeIdMapping.d.ts +5 -0
  149. package/dist/resources/nodeIdMapping.js +49 -0
  150. package/dist/resources/nodeIdMapping.js.map +1 -1
  151. package/dist/resources/recordLock.d.ts +47 -4
  152. package/dist/resources/recordLock.js +138 -7
  153. package/dist/resources/recordLock.js.map +1 -1
  154. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  155. package/dist/resources/recordLockCoordinator.js +2597 -0
  156. package/dist/resources/recordLockCoordinator.js.map +1 -0
  157. package/dist/resources/replayLogs.js +5 -0
  158. package/dist/resources/replayLogs.js.map +1 -1
  159. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  160. package/dist/resources/replicatedApplyFailure.js +63 -0
  161. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  162. package/dist/resources/scheduler/scheduler.js +3 -3
  163. package/dist/resources/scheduler/scheduler.js.map +1 -1
  164. package/dist/resources/search.d.ts +10 -4
  165. package/dist/resources/search.js +160 -40
  166. package/dist/resources/search.js.map +1 -1
  167. package/dist/resources/tracked.d.ts +5 -1
  168. package/dist/resources/tracked.js +74 -23
  169. package/dist/resources/tracked.js.map +1 -1
  170. package/dist/resources/transactionBroadcast.js +4 -6
  171. package/dist/resources/transactionBroadcast.js.map +1 -1
  172. package/dist/security/auth.js +59 -23
  173. package/dist/security/auth.js.map +1 -1
  174. package/dist/security/deferredAuthentication.d.ts +11 -0
  175. package/dist/security/deferredAuthentication.js +25 -3
  176. package/dist/security/deferredAuthentication.js.map +1 -1
  177. package/dist/security/jsLoader.js +9 -4
  178. package/dist/security/jsLoader.js.map +1 -1
  179. package/dist/server/REST.js +39 -5
  180. package/dist/server/REST.js.map +1 -1
  181. package/dist/server/http.d.ts +5 -1
  182. package/dist/server/http.js +34 -2
  183. package/dist/server/http.js.map +1 -1
  184. package/dist/server/mqtt.js +5 -1
  185. package/dist/server/mqtt.js.map +1 -1
  186. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  187. package/dist/server/serverHelpers/Headers.js +6 -0
  188. package/dist/server/serverHelpers/Headers.js.map +1 -1
  189. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  190. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  191. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  192. package/dist/server/serverHelpers/Request.d.ts +5 -10
  193. package/dist/server/serverHelpers/Request.js +38 -136
  194. package/dist/server/serverHelpers/Request.js.map +1 -1
  195. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  196. package/dist/server/serverHelpers/contentTypes.js +189 -15
  197. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  198. package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
  199. package/dist/server/serverHelpers/serverUtilities.js +110 -20
  200. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  201. package/dist/server/serverHelpers/uwsServer.js +4 -1
  202. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  203. package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
  204. package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
  205. package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
  206. package/dist/server/storageReclamation.js +1 -1
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  209. package/dist/server/threads/isolatedApplications.js +171 -0
  210. package/dist/server/threads/isolatedApplications.js.map +1 -0
  211. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  212. package/dist/server/threads/logRotationTransport.js +33 -0
  213. package/dist/server/threads/logRotationTransport.js.map +1 -0
  214. package/dist/server/threads/manageThreads.d.ts +64 -6
  215. package/dist/server/threads/manageThreads.js +261 -12
  216. package/dist/server/threads/manageThreads.js.map +1 -1
  217. package/dist/server/threads/socketRouter.d.ts +1 -0
  218. package/dist/server/threads/socketRouter.js +196 -13
  219. package/dist/server/threads/socketRouter.js.map +1 -1
  220. package/dist/server/threads/threadServer.js +30 -7
  221. package/dist/server/threads/threadServer.js.map +1 -1
  222. package/dist/utility/errors/hdbError.d.ts +24 -0
  223. package/dist/utility/errors/hdbError.js +58 -1
  224. package/dist/utility/errors/hdbError.js.map +1 -1
  225. package/dist/utility/hdbTerms.d.ts +2 -0
  226. package/dist/utility/hdbTerms.js +2 -0
  227. package/dist/utility/hdbTerms.js.map +1 -1
  228. package/dist/utility/logging/harper_logger.js +218 -38
  229. package/dist/utility/logging/harper_logger.js.map +1 -1
  230. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  231. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  232. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  233. package/dist/utility/logging/logRotation.d.ts +46 -0
  234. package/dist/utility/logging/logRotation.js +365 -0
  235. package/dist/utility/logging/logRotation.js.map +1 -0
  236. package/dist/utility/logging/logRotator.d.ts +1 -1
  237. package/dist/utility/logging/logRotator.js +172 -92
  238. package/dist/utility/logging/logRotator.js.map +1 -1
  239. package/dist/utility/npmUtilities.js +6 -4
  240. package/dist/utility/npmUtilities.js.map +1 -1
  241. package/dist/utility/watcherFallback.d.ts +0 -45
  242. package/dist/utility/watcherFallback.js +1 -125
  243. package/dist/utility/watcherFallback.js.map +1 -1
  244. package/dist/validation/configValidator.js +6 -3
  245. package/dist/validation/configValidator.js.map +1 -1
  246. package/index.ts +9 -0
  247. package/json/systemSchema.json +3 -0
  248. package/npm-shrinkwrap.json +177 -87
  249. package/package.json +13 -5
  250. package/resources/DESIGN.md +689 -19
  251. package/resources/DatabaseTransaction.ts +230 -17
  252. package/resources/LMDBTransaction.ts +21 -3
  253. package/resources/PrimaryRocksDatabase.ts +20 -7
  254. package/resources/RecordEncoder.ts +52 -10
  255. package/resources/Resource.ts +97 -13
  256. package/resources/ResourceInterface.ts +8 -0
  257. package/resources/RocksIndexStore.ts +2 -1
  258. package/resources/RocksTransactionLogStore.ts +111 -31
  259. package/resources/Table.ts +1273 -396
  260. package/resources/analytics/DESIGN.md +38 -0
  261. package/resources/analytics/write.ts +50 -17
  262. package/resources/auditStore.ts +460 -11
  263. package/resources/crdt.ts +22 -0
  264. package/resources/dataLoader.ts +3 -4
  265. package/resources/databases.ts +611 -147
  266. package/resources/derivedIndexRegistry.ts +56 -0
  267. package/resources/derivedIndexRuntime.ts +2292 -0
  268. package/resources/graphql.ts +3 -2
  269. package/resources/indexes/DESIGN.md +833 -0
  270. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +924 -46
  271. package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
  272. package/resources/indexes/fullTextNativeBinding.ts +146 -0
  273. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  274. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  275. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
  276. package/resources/nodeIdMapping.ts +50 -0
  277. package/resources/record-locks.md +1407 -0
  278. package/resources/recordLock.ts +173 -7
  279. package/resources/recordLockCoordinator.ts +3082 -0
  280. package/resources/replayLogs.ts +5 -0
  281. package/resources/replicatedApplyFailure.ts +77 -0
  282. package/resources/scheduler/DESIGN.md +40 -0
  283. package/resources/scheduler/scheduler.ts +4 -4
  284. package/resources/search.ts +169 -49
  285. package/resources/tracked.ts +73 -22
  286. package/resources/transactionBroadcast.ts +4 -4
  287. package/security/DESIGN.md +175 -0
  288. package/security/auth.ts +53 -24
  289. package/security/deferredAuthentication.ts +24 -2
  290. package/security/jsLoader.ts +9 -4
  291. package/server/DESIGN.md +275 -0
  292. package/server/REST.ts +42 -6
  293. package/server/http.ts +34 -2
  294. package/server/mqtt.ts +6 -4
  295. package/server/serverHelpers/Headers.ts +5 -1
  296. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  297. package/server/serverHelpers/Request.ts +33 -131
  298. package/server/serverHelpers/contentTypes.ts +188 -15
  299. package/server/serverHelpers/serverUtilities.ts +157 -27
  300. package/server/serverHelpers/uwsServer.ts +4 -1
  301. package/server/serverHelpers/webSocketCloseReason.ts +25 -0
  302. package/server/storageReclamation.ts +2 -2
  303. package/server/threads/isolatedApplications.ts +157 -0
  304. package/server/threads/logRotationTransport.ts +40 -0
  305. package/server/threads/manageThreads.js +254 -12
  306. package/server/threads/socketRouter.ts +217 -11
  307. package/server/threads/threadServer.js +30 -7
  308. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-DADFFGe_.js} +1 -1
  309. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-D_mI-rZ7.js} +3 -3
  310. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-c2NiSDHa.js} +1 -1
  311. package/studio/web/assets/{applications-DHxGi7JH.js → applications-DktUqh7G.js} +1 -1
  312. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-Cs_75Xhw.js} +1 -1
  313. package/studio/web/assets/{editor-DNcRHK54.js → editor-19b-Y1IN.js} +1 -1
  314. package/studio/web/assets/{html-Bdssedlg.js → html-DiYEQMpB.js} +1 -1
  315. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CmR0y7P_.js} +1 -1
  316. package/studio/web/assets/{index-D6sxmFLR.js → index-Dm0rfkJ7.js} +5 -5
  317. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-7vqt2CC3.js} +1 -1
  318. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-BWtCFuOt.js} +1 -1
  319. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-Buzzbv9y.js} +1 -1
  320. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-SqsFWfTM.js} +1 -1
  321. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
  322. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-CAB-LZWT.js} +1 -1
  323. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-DRzmSRxM.js} +1 -1
  324. package/studio/web/assets/{profile-DdOwtntb.js → profile-BNKAl79n.js} +1 -1
  325. package/studio/web/assets/{regions-n69fwagr.js → regions-CUow_Zw2.js} +1 -1
  326. package/studio/web/assets/{register-PfWTCXWB.js → register-Dkt3WUMp.js} +2 -2
  327. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BZRfMD0N.js} +1 -1
  328. package/studio/web/assets/{setup-CUx_aUDl.js → setup-D_yiEPO2.js} +2 -2
  329. package/studio/web/assets/{status-D7BVKqX9.js → status-DhHh1Ge-.js} +1 -1
  330. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-D7L1PDtV.js} +1 -1
  331. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-CCwLk1YS.js} +1 -1
  332. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-BP1j1mjn.js} +1 -1
  333. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-D7bnYxLw.js} +1 -1
  334. package/studio/web/assets/{workers-pR3jRY9D.js → workers-tOuCNT17.js} +1 -1
  335. package/studio/web/assets/{xml-2iRnMhQO.js → xml-BSG_3mQT.js} +1 -1
  336. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-DxiLprBB.js} +1 -1
  337. package/studio/web/index.html +1 -1
  338. package/utility/DESIGN.md +55 -0
  339. package/utility/errors/hdbError.ts +54 -0
  340. package/utility/hdbTerms.ts +2 -0
  341. package/utility/logging/harper_logger.ts +210 -30
  342. package/utility/logging/logGenerationCoordinator.ts +196 -0
  343. package/utility/logging/logRotation.ts +367 -0
  344. package/utility/logging/logRotator.ts +196 -91
  345. package/utility/npmUtilities.ts +6 -4
  346. package/utility/watcherFallback.ts +0 -122
  347. package/validation/configValidator.ts +6 -3
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.BRANCH_REMOVING_SUFFIX = exports.BRANCH_ROOT_DIR = exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
39
+ exports.CHECKPOINT_ALGORITHM = exports.BRANCH_REMOVING_SUFFIX = exports.BRANCH_ROOT_DIR = exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
40
40
  exports.isReadOnlyMode = isReadOnlyMode;
41
41
  exports.getRocksCompression = getRocksCompression;
42
42
  exports.resetRocksCompression = resetRocksCompression;
@@ -65,6 +65,8 @@ exports.table = table;
65
65
  exports.scopedTableFactory = scopedTableFactory;
66
66
  exports.reloadBranchAt = reloadBranchAt;
67
67
  exports.canonicalizeIndexOptions = canonicalizeIndexOptions;
68
+ exports.setIndexingCheckpointPeriod = setIndexingCheckpointPeriod;
69
+ exports.resumeStartKey = resumeStartKey;
68
70
  exports.dropTableMeta = dropTableMeta;
69
71
  exports.onUpdatedTable = onUpdatedTable;
70
72
  exports.onRemovedTable = onRemovedTable;
@@ -72,10 +74,11 @@ exports.onRemovedDB = onRemovedDB;
72
74
  exports.getDefaultCompression = getDefaultCompression;
73
75
  exports.flushDatabases = flushDatabases;
74
76
  const node_events_1 = require("node:events");
77
+ const node_crypto_1 = require("node:crypto");
75
78
  const environmentManager_ts_1 = require("../utility/environment/environmentManager.js");
76
79
  const terms_ts_1 = require("../utility/lmdb/terms.js");
77
80
  const lmdb_1 = require("lmdb");
78
- const path_1 = require("path");
81
+ const node_path_1 = require("node:path");
79
82
  const node_fs_1 = require("node:fs");
80
83
  const promises_1 = require("node:fs/promises");
81
84
  const initializePaths_js_1 = require("../dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js");
@@ -98,13 +101,14 @@ const blob_ts_1 = require("./blob.js");
98
101
  const storageReclamation_ts_1 = require("../server/storageReclamation.js");
99
102
  const common_validators_ts_1 = require("../validation/common_validators.js");
100
103
  const customIndexes_ts_1 = require("./indexes/customIndexes.js");
104
+ const hnswPlaneBinding_ts_1 = require("./indexes/hnswPlaneBinding.js");
101
105
  const OpenDBIObject_ts_1 = require("../utility/lmdb/OpenDBIObject.js");
102
106
  const rocksdb_js_1 = require("@harperfast/rocksdb-js");
103
107
  const PrimaryRocksDatabase_ts_1 = require("./PrimaryRocksDatabase.js");
104
108
  const replayLogs_ts_1 = require("./replayLogs.js");
109
+ const hnswDerivedIndex_ts_1 = require("./indexes/hnswDerivedIndex.js");
105
110
  const node_os_1 = require("node:os");
106
111
  const RocksIndexStore_ts_1 = require("./RocksIndexStore.js");
107
- const when_ts_1 = require("../utility/when.js");
108
112
  const rocksMemoryConfig_ts_1 = require("../utility/rocksMemoryConfig.js");
109
113
  const processManagement_js_1 = require("../utility/processManagement/processManagement.js");
110
114
  const restoreMarker_ts_1 = require("../dataLayer/restoreMarker.js");
@@ -417,6 +421,23 @@ function applyDurableDeclaration(attribute, descriptor) {
417
421
  delete attribute[field];
418
422
  }
419
423
  }
424
+ /**
425
+ * True when a descriptor claims an index build no live operation in this process can own. The PID and
426
+ * worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
427
+ * generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
428
+ * before the field existed, so it belongs to an earlier process; a thread started without one of its
429
+ * own cannot judge, and falls back rather than declaring a live build dead.
430
+ */
431
+ function isAbandonedIndexBuild(descriptor, currentRestartGeneration) {
432
+ if (!descriptor)
433
+ return false;
434
+ if (descriptor.indexingPID && descriptor.indexingPID !== process.pid)
435
+ return true;
436
+ if (descriptor.restartNumber < currentRestartGeneration)
437
+ return true;
438
+ const incarnation = manageThreads.processIncarnation;
439
+ return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
440
+ }
420
441
  // How many times the schema load will try to finish a tombstoned drop before
421
442
  // giving up for the rest of this process's lifetime. A drop that fails once
422
443
  // almost always fails identically forever - the usual cause is a RocksDB
@@ -492,7 +513,7 @@ function getDatabases() {
492
513
  definedDatabases = new Map();
493
514
  relationshipsToHydrate = [];
494
515
  const hdbBasePath = (0, environmentManager_ts_1.getHdbBasePath)();
495
- let databasePath = hdbBasePath && (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
516
+ let databasePath = hdbBasePath && (0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
496
517
  const schemaConfigs = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
497
518
  // not sure why this doesn't work with the environmemt manager
498
519
  if (process.env.SCHEMAS_DATA_PATH)
@@ -500,7 +521,7 @@ function getDatabases() {
500
521
  databasePath =
501
522
  process.env.STORAGE_PATH ||
502
523
  (0, configUtils_ts_1.getConfigPath)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PATH) ||
503
- (databasePath && ((0, node_fs_1.existsSync)(databasePath) ? databasePath : (0, path_1.join)((0, environmentManager_ts_1.getHdbBasePath)(), hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)));
524
+ (databasePath && ((0, node_fs_1.existsSync)(databasePath) ? databasePath : (0, node_path_1.join)((0, environmentManager_ts_1.getHdbBasePath)(), hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)));
504
525
  if (databasePath && (0, node_fs_1.existsSync)(databasePath)) {
505
526
  // First load all the databases from our main database folder
506
527
  // TODO: Load any databases defined with explicit storage paths from the config
@@ -518,14 +539,14 @@ function getDatabases() {
518
539
  // branch directories are process-local derivatives, never databases in their own right
519
540
  if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
520
541
  continue;
521
- const dbName = (0, path_1.basename)(databaseEntry.name, '.mdb');
522
- const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
542
+ const dbName = (0, node_path_1.basename)(databaseEntry.name, '.mdb');
543
+ const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
523
544
  if (blockedByRestore.has(dbName))
524
545
  continue;
525
546
  if (isOpenBranchPath(dbPath))
526
547
  continue;
527
548
  if (databaseEntry.isFile() &&
528
- (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
549
+ (0, node_path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
529
550
  !schemaConfigs[dbName]?.path) {
530
551
  logger.trace(`loading lmdb database: ${dbPath}`);
531
552
  readMetaDb(dbPath, null, dbName);
@@ -552,12 +573,12 @@ function getDatabases() {
552
573
  if ((0, node_fs_1.existsSync)(baseSchemaPath)) {
553
574
  for (const schemaEntry of (0, node_fs_1.readdirSync)(baseSchemaPath, { withFileTypes: true })) {
554
575
  if (!schemaEntry.isFile()) {
555
- const schemaPath = (0, path_1.join)(baseSchemaPath, schemaEntry.name);
556
- const schemaAuditPath = (0, path_1.join)((0, initializePaths_js_1.getTransactionAuditStoreBasePath)(), schemaEntry.name);
576
+ const schemaPath = (0, node_path_1.join)(baseSchemaPath, schemaEntry.name);
577
+ const schemaAuditPath = (0, node_path_1.join)((0, initializePaths_js_1.getTransactionAuditStoreBasePath)(), schemaEntry.name);
557
578
  for (const tableEntry of (0, node_fs_1.readdirSync)(schemaPath, { withFileTypes: true })) {
558
- if (tableEntry.isFile() && (0, path_1.extname)(tableEntry.name).toLowerCase() === '.mdb') {
559
- const auditPath = (0, path_1.join)(schemaAuditPath, tableEntry.name);
560
- readMetaDb((0, path_1.join)(schemaPath, tableEntry.name), (0, path_1.basename)(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
579
+ if (tableEntry.isFile() && (0, node_path_1.extname)(tableEntry.name).toLowerCase() === '.mdb') {
580
+ const auditPath = (0, node_path_1.join)(schemaAuditPath, tableEntry.name);
581
+ readMetaDb((0, node_path_1.join)(schemaPath, tableEntry.name), (0, node_path_1.basename)(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
561
582
  }
562
583
  }
563
584
  }
@@ -577,16 +598,16 @@ function getDatabases() {
577
598
  continue; // reserved restore-metadata dir
578
599
  if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
579
600
  continue; // reserved branch root
580
- if (blockedByRestore.has((0, path_1.basename)(databaseEntry.name, '.mdb')))
601
+ if (blockedByRestore.has((0, node_path_1.basename)(databaseEntry.name, '.mdb')))
581
602
  continue;
582
- if (isOpenBranchPath((0, path_1.join)(databasePath, databaseEntry.name)))
603
+ if (isOpenBranchPath((0, node_path_1.join)(databasePath, databaseEntry.name)))
583
604
  continue;
584
- if (databaseEntry.isFile() && (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
585
- readMetaDb((0, path_1.join)(databasePath, databaseEntry.name), (0, path_1.basename)(databaseEntry.name, '.mdb'), dbName);
605
+ if (databaseEntry.isFile() && (0, node_path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
606
+ readMetaDb((0, node_path_1.join)(databasePath, databaseEntry.name), (0, node_path_1.basename)(databaseEntry.name, '.mdb'), dbName);
586
607
  }
587
608
  else {
588
609
  try {
589
- const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
610
+ const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
590
611
  const files = (0, node_fs_1.readdirSync)(dbPath, { withFileTypes: true });
591
612
  if (files.find((file) => file.name === 'CURRENT')?.isFile() &&
592
613
  files.some((file) => file.name.startsWith('MANIFEST-'))) {
@@ -606,7 +627,7 @@ function getDatabases() {
606
627
  if (tableConfigs) {
607
628
  for (const tableName in tableConfigs) {
608
629
  const tableConfig = tableConfigs[tableName];
609
- const tablePath = (0, path_1.join)(tableConfig.path, (0, path_1.basename)(tableName + '.mdb'));
630
+ const tablePath = (0, node_path_1.join)(tableConfig.path, (0, node_path_1.basename)(tableName + '.mdb'));
610
631
  if ((0, node_fs_1.existsSync)(tablePath)) {
611
632
  readMetaDb(tablePath, tableName, dbName, null, true);
612
633
  }
@@ -843,6 +864,7 @@ function readMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, au
843
864
  rootStore = (0, lmdb_1.open)(envInit);
844
865
  lmdbDatabaseEnvs.set(path, rootStore);
845
866
  }
867
+ rootStore.dbisDb?.resetReadTxn();
846
868
  return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
847
869
  }
848
870
  catch (error) {
@@ -885,7 +907,7 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
885
907
  }
886
908
  function initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy, destination, storeName, openedStores } = {}) {
887
909
  // a store with no tables never reaches the per-table loop below, and blob roots resolve from this
888
- rootStore.databaseName = storeName ?? databaseName;
910
+ rootStore.databaseName ??= storeName ?? databaseName;
889
911
  const envInit = new OpenEnvironmentObject_ts_1.default(path, isReadOnlyMode());
890
912
  const internalDbiInit = createOpenDBIObject(false);
891
913
  let attributesDbi = rootStore.dbisDb;
@@ -921,6 +943,9 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
921
943
  });
922
944
  }
923
945
  auditStore.isLegacy = true;
946
+ // A legacy standalone audit root skips openAuditStore, so give it a floor here or it
947
+ // reports its retention horizon as permanently unknown.
948
+ (0, auditStore_ts_1.establishAuditFloor)(auditStore);
924
949
  }
925
950
  }
926
951
  else {
@@ -1046,6 +1071,8 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1046
1071
  let table = tables[tableName];
1047
1072
  // unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
1048
1073
  const recreateForEngineChange = !!table && table.primaryStore?.rootStore instanceof rocksdb_js_1.RocksDatabase !== rootStore instanceof rocksdb_js_1.RocksDatabase;
1074
+ const recreateForTableIdChange = !!table && primaryAttribute.tableId != null && table.tableId !== primaryAttribute.tableId;
1075
+ const recreateTable = recreateForEngineChange || recreateForTableIdChange;
1049
1076
  let indices = {}, existingAttributes = [];
1050
1077
  let tableId;
1051
1078
  let primaryStore;
@@ -1057,7 +1084,9 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1057
1084
  const cacheControl = primaryAttribute.cacheControl;
1058
1085
  const splitSegments = primaryAttribute.splitSegments;
1059
1086
  const replicate = primaryAttribute.replicate;
1060
- if (table && !recreateForEngineChange) {
1087
+ if (table && !recreateTable) {
1088
+ if (primaryAttribute.audit === true && table.audit !== true)
1089
+ table.enableAuditing();
1061
1090
  indices = table.indices;
1062
1091
  existingAttributes = table.attributes;
1063
1092
  table.schemaVersion++;
@@ -1108,6 +1137,8 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1108
1137
  indices[attribute.name] = dbi;
1109
1138
  indices[attribute.name].indexNulls = attribute.indexNulls;
1110
1139
  }
1140
+ // the only way a thread that never declares the schema reaches Table.indices
1141
+ indices[attribute.name].isIndexing = !!attribute.indexingPID;
1111
1142
  const existingAttribute = existingAttributes.find((existingAttribute) => existingAttribute.name === attribute.name);
1112
1143
  if (existingAttribute)
1113
1144
  existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1, attribute);
@@ -1167,13 +1198,15 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1167
1198
  existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1);
1168
1199
  attributesUpdated = true;
1169
1200
  }
1170
- if (table && !recreateForEngineChange) {
1201
+ if (table && !recreateTable) {
1171
1202
  if (attributesUpdated) {
1172
1203
  table.schemaVersion++;
1173
1204
  table.updatedAttributes();
1174
1205
  }
1175
1206
  }
1176
1207
  else {
1208
+ if (recreateForTableIdChange)
1209
+ table.cleanup();
1177
1210
  table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
1178
1211
  // A branch builds into a caller-owned destination; its tables must refuse DDL.
1179
1212
  isBranch: Boolean(destination),
@@ -1201,6 +1234,8 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1201
1234
  if (!destination)
1202
1235
  exports.databaseEventsEmitter.emit('updateTable', table);
1203
1236
  }
1237
+ void table.derivedIndexRuntime?.close();
1238
+ table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(table);
1204
1239
  if (Array.isArray(primaryAttribute.relationships)) {
1205
1240
  relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
1206
1241
  }
@@ -1239,7 +1274,7 @@ function resolveBranchPath(baseName, appName) {
1239
1274
  throw new Error(`Invalid ${label} name for a branch path: ${JSON.stringify(segment)}`);
1240
1275
  }
1241
1276
  }
1242
- return (0, path_1.join)(resolveDatabaseStorageRoot(baseName), exports.BRANCH_ROOT_DIR, appName, baseName);
1277
+ return (0, node_path_1.join)(resolveDatabaseStorageRoot(baseName), exports.BRANCH_ROOT_DIR, appName, baseName);
1243
1278
  }
1244
1279
  /** `undefined` marks a path reserved by an open still in flight, which owns it just as firmly. */
1245
1280
  const openBranches = new Map();
@@ -1487,6 +1522,8 @@ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1487
1522
  openBranches.set(path, undefined);
1488
1523
  retakeBranchIdentity(storeName);
1489
1524
  try {
1525
+ // before the open: table load schedules TTL, eviction and audit cleanup, which ask who owns this store
1526
+ manageThreads.markBranchStorePath(path);
1490
1527
  rootStore = readRocksMetaDb(path, null, databaseName, { destination: tables, storeName, openedStores });
1491
1528
  // Pin the handle to the roots the caller proved this branch was published with, before it is
1492
1529
  // handed out. A row's `storageIndex` is a position in that list, so resolving through current
@@ -1498,6 +1535,7 @@ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1498
1535
  }
1499
1536
  catch (error) {
1500
1537
  openBranches.delete(path);
1538
+ manageThreads.markBranchStorePath(path, false);
1501
1539
  releaseBranchIdentity(storeName);
1502
1540
  const stranded = rocksdbDatabaseEnvs.get(path);
1503
1541
  rocksdbDatabaseEnvs.delete(path);
@@ -1522,6 +1560,7 @@ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1522
1560
  openBranches.delete(path);
1523
1561
  releaseBranchIdentity(storeName);
1524
1562
  rocksdbDatabaseEnvs.delete(path);
1563
+ manageThreads.markBranchStorePath(path, false);
1525
1564
  closeBranchHandles(path, rootStore, openedStores, tables);
1526
1565
  },
1527
1566
  };
@@ -1647,10 +1686,10 @@ function resolveDatabaseStorageRoot(databaseName, tableName) {
1647
1686
  databaseConfig[databaseName]?.path ||
1648
1687
  process.env.STORAGE_PATH ||
1649
1688
  (0, configUtils_ts_1.getConfigPath)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PATH) ||
1650
- (hdbBasePath && (0, node_fs_1.existsSync)((0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME))
1651
- ? (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME)
1689
+ (hdbBasePath && (0, node_fs_1.existsSync)((0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME))
1690
+ ? (0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME)
1652
1691
  : hdbBasePath
1653
- ? (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)
1692
+ ? (0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)
1654
1693
  : undefined);
1655
1694
  if (!databasePath) {
1656
1695
  throw new Error(`Unable to determine database storage path. Ensure STORAGE_PATH, HDB_ROOT, or a valid config path is set.`);
@@ -1661,7 +1700,7 @@ function resolveDatabaseStorageRoot(databaseName, tableName) {
1661
1700
  * Resolve the directory path of a RocksDB database (whether or not it exists or is loaded).
1662
1701
  */
1663
1702
  function resolveDatabasePath(databaseName) {
1664
- return (0, path_1.join)(resolveDatabaseStorageRoot(databaseName), databaseName);
1703
+ return (0, node_path_1.join)(resolveDatabaseStorageRoot(databaseName), databaseName);
1665
1704
  }
1666
1705
  /**
1667
1706
  * Get root store for a database
@@ -1686,7 +1725,7 @@ function database({ database: databaseName, table: tableName }) {
1686
1725
  let rootStore;
1687
1726
  const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
1688
1727
  if (useRocksdb) {
1689
- const path = (0, path_1.join)(databasePath, tablePath ? tableName : databaseName);
1728
+ const path = (0, node_path_1.join)(databasePath, tablePath ? tableName : databaseName);
1690
1729
  // the scan is not the only way to reach a branch's directory: a branch leaves its store in
1691
1730
  // `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
1692
1731
  // `definedDatabases` and the next `closeDatabase` would close it under the live handle
@@ -1709,7 +1748,7 @@ function database({ database: databaseName, table: tableName }) {
1709
1748
  }
1710
1749
  }
1711
1750
  else {
1712
- const path = (0, path_1.join)(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
1751
+ const path = (0, node_path_1.join)(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
1713
1752
  rootStore = lmdbDatabaseEnvs.get(path);
1714
1753
  if (!rootStore || rootStore.status === 'closed') {
1715
1754
  // TODO: validate database name
@@ -2062,6 +2101,9 @@ function openIndex(dbiKey, rootStore, attribute) {
2062
2101
  const CustomIndex = customIndexes_ts_1.CUSTOM_INDEXES[attribute.indexed.type];
2063
2102
  if (CustomIndex) {
2064
2103
  indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
2104
+ // derived state whose maintaining option is now off must not linger to be adopted
2105
+ // stale on a later re-enable
2106
+ indexStore.customIndex.cleanupDisabledPlane?.();
2065
2107
  }
2066
2108
  else {
2067
2109
  logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
@@ -2096,14 +2138,17 @@ const GLOBAL_TARGET = {
2096
2138
  /**
2097
2139
  * The factory a branched application declares tables through: each declaration goes to the branch
2098
2140
  * of the database it names, or to `table()` itself for a database the application did not branch.
2099
- * An unbranched application gets `table` by identity -- no wrapper, no per-call routing.
2141
+ * An unbranched, shared application gets `table` by identity. An isolated application still gets a
2142
+ * wrapper so its own declarations can claim their single-threaded maintenance work.
2100
2143
  */
2101
- function scopedTableFactory(branches) {
2102
- if (!branches?.size)
2144
+ function scopedTableFactory(branches, isolatedApplicationOwner = false) {
2145
+ if (!branches?.size && !isolatedApplicationOwner)
2103
2146
  return table;
2104
2147
  return function scopedTable(tableDefinition) {
2148
+ if (isolatedApplicationOwner)
2149
+ tableDefinition = { ...tableDefinition, isolatedApplicationOwner: true };
2105
2150
  // `||`, not `??`: `table()` resolves every falsy name to the default database
2106
- const branch = branches.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
2151
+ const branch = branches?.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
2107
2152
  return branch ? declareTable(branchTarget(branch), tableDefinition) : table(tableDefinition);
2108
2153
  };
2109
2154
  }
@@ -2160,7 +2205,9 @@ function reloadBranchAt(path) {
2160
2205
  return branch.tables;
2161
2206
  }
2162
2207
  function declareTable(target, tableDefinition) {
2163
- let { table: tableName, database: databaseName, expiration, eviction, scanInterval, attributes, audit, sealed, splitSegments, replicate, randomAccessFields, trackDeletes, schemaDefined, schemaRelationshipsDefined, origin, description, properties, hidden, cacheControl, } = tableDefinition;
2208
+ let { table: tableName, database: databaseName, expiration, eviction, scanInterval, attributes, audit, sealed, splitSegments, replicate, randomAccessFields, trackDeletes, schemaDefined, schemaRelationshipsDefined, origin, description, properties, hidden, cacheControl, isolatedApplicationOwner, } = tableDefinition;
2209
+ const auditExplicitlyEnabled = audit === true;
2210
+ const auditExplicitlyDisabled = audit === false;
2164
2211
  if (!databaseName)
2165
2212
  databaseName = DEFAULT_DATABASE_NAME;
2166
2213
  // Reject reserved names here too, not only at the operations API: a database
@@ -2197,27 +2244,137 @@ function declareTable(target, tableDefinition) {
2197
2244
  schemaDefined = true;
2198
2245
  const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
2199
2246
  const internalDbiInit = createOpenDBIObject(false);
2200
- for (const attribute of attributes) {
2201
- if (attribute.attribute && !attribute.name) {
2202
- // there is some legacy code that calls the attribute's name the attribute's attribute
2203
- attribute.name = attribute.attribute;
2204
- attribute.indexed = true;
2247
+ let releaseExclusiveLock;
2248
+ const hasHnswAtEntry = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
2249
+ const persistedPrimaryDescriptor = (catalog) => {
2250
+ const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name ?? Table?.primaryKey;
2251
+ if (declaredPrimaryKey) {
2252
+ const key = `${tableName}/${declaredPrimaryKey}`;
2253
+ const descriptor = catalog?.getSync(key);
2254
+ if (descriptor?.isPrimaryKey)
2255
+ return { key, descriptor };
2256
+ }
2257
+ const key = `${tableName}/`;
2258
+ return { key, descriptor: catalog?.getSync(key) };
2259
+ };
2260
+ const hasLegacyHnswStateAtEntry = Table &&
2261
+ origin !== 'cluster' &&
2262
+ attributes.some((attribute) => {
2263
+ if (attribute.indexed?.type !== 'HNSW')
2264
+ return false;
2265
+ const persisted = Table.dbisDB?.getSync(`${tableName}/${attribute.name || attribute.attribute || ''}`)?.indexed;
2266
+ if (persisted?.type !== 'HNSW')
2267
+ return false;
2268
+ if (Object.hasOwn(persisted, 'nativePlane') && typeof persisted.nativePlane !== 'boolean')
2269
+ return true;
2270
+ for (const name of customIndexes_ts_1.CUSTOM_INDEXES.HNSW.numericOptions)
2271
+ if (Object.hasOwn(persisted, name) && typeof persisted[name] !== 'number')
2272
+ return true;
2273
+ return false;
2274
+ });
2275
+ try {
2276
+ if (Table &&
2277
+ hasHnswAtEntry &&
2278
+ origin !== 'cluster' &&
2279
+ (rootStore instanceof rocksdb_js_1.RocksDatabase || hasLegacyHnswStateAtEntry))
2280
+ exclusiveLock();
2281
+ const persistedAuditAtEntry = hasHnswAtEntry || (Table && Table.audit !== true)
2282
+ ? persistedPrimaryDescriptor(Table?.dbisDB).descriptor?.audit
2283
+ : undefined;
2284
+ if (!auditExplicitlyDisabled && persistedAuditAtEntry === true && Table?.audit !== true)
2285
+ Table.enableAuditing();
2286
+ for (const attribute of attributes) {
2287
+ if (attribute.attribute && !attribute.name) {
2288
+ // there is some legacy code that calls the attribute's name the attribute's attribute
2289
+ attribute.name = attribute.attribute;
2290
+ attribute.indexed = true;
2291
+ }
2292
+ else
2293
+ attribute.attribute = attribute.name;
2294
+ if (attribute.expiresAt)
2295
+ attribute.indexed = true;
2296
+ if (attribute.indexed?.type === 'HNSW' && origin !== 'cluster') {
2297
+ const existingAttribute = Table?.attributes.find((existing) => existing.name === attribute.name);
2298
+ const persistedIndexed = Table?.dbisDB?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed ?? existingAttribute?.indexed;
2299
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.normalizeDeclarationOptions(attribute.indexed, persistedIndexed);
2300
+ if (attribute.indexed.nativePlane != null) {
2301
+ const persistedNativePlane = persistedIndexed?.nativePlane;
2302
+ const matchesPersistedLegacySpelling = persistedIndexed?.type === 'HNSW' &&
2303
+ Object.hasOwn(persistedIndexed, 'nativePlane') &&
2304
+ typeof persistedNativePlane !== 'boolean' &&
2305
+ (Object.is(persistedNativePlane, attribute.indexed.nativePlane) ||
2306
+ (typeof persistedNativePlane === 'string' &&
2307
+ typeof attribute.indexed.nativePlane === 'number' &&
2308
+ persistedNativePlane.trim() !== '' &&
2309
+ Number(persistedNativePlane) === attribute.indexed.nativePlane));
2310
+ if (matchesPersistedLegacySpelling) {
2311
+ attribute.indexed.nativePlane = persistedNativePlane;
2312
+ }
2313
+ else {
2314
+ attribute.indexed.nativePlane = customIndexes_ts_1.CUSTOM_INDEXES.HNSW.normalizeNativePlaneDeclaration(attribute.indexed.nativePlane);
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ const auditEnabledAtEntry = auditExplicitlyEnabled ||
2320
+ (!auditExplicitlyDisabled &&
2321
+ (persistedAuditAtEntry === true || (persistedAuditAtEntry == null && Table?.audit === true)));
2322
+ if (origin !== 'cluster' &&
2323
+ attributes.some((attribute) => {
2324
+ if (attribute.indexed?.type !== 'HNSW')
2325
+ return false;
2326
+ if (attribute.indexed.nativePlane != null)
2327
+ return Boolean(attribute.indexed.nativePlane);
2328
+ const existingAttribute = Table?.attributes.find((existing) => existing.name === attribute.name && existing.indexed?.type === 'HNSW');
2329
+ return Boolean(existingAttribute?.indexed.nativePlane);
2330
+ }) &&
2331
+ !auditEnabledAtEntry) {
2332
+ throw new hdbError_ts_1.ClientError(`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`);
2205
2333
  }
2206
- else
2207
- attribute.attribute = attribute.name;
2208
- if (attribute.expiresAt)
2209
- attribute.indexed = true;
2210
2334
  }
2335
+ catch (error) {
2336
+ releaseLock();
2337
+ throw error;
2338
+ }
2339
+ const validateHnswOptions = (catalog, auditQualifiesDefault) => {
2340
+ for (const attribute of attributes) {
2341
+ const indexed = attribute.indexed;
2342
+ if (indexed?.type !== 'HNSW')
2343
+ continue;
2344
+ const persistedIndexed = catalog?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed;
2345
+ if (indexed.nativePlane) {
2346
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, indexed);
2347
+ continue;
2348
+ }
2349
+ if (indexed.nativePlane != null)
2350
+ continue;
2351
+ if (persistedIndexed?.type === 'HNSW' && Object.hasOwn(persistedIndexed, 'nativePlane')) {
2352
+ if (persistedIndexed.nativePlane)
2353
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, {
2354
+ ...indexed,
2355
+ nativePlane: persistedIndexed.nativePlane,
2356
+ });
2357
+ continue;
2358
+ }
2359
+ if (!auditQualifiesDefault)
2360
+ continue;
2361
+ if (persistedIndexed?.type !== 'HNSW')
2362
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed);
2363
+ }
2364
+ };
2365
+ if (!Table && origin !== 'cluster')
2366
+ validateHnswOptions(undefined, auditExplicitlyEnabled);
2211
2367
  let hasChanges;
2212
2368
  let refreshRelationshipAttributes = false;
2369
+ let refreshedLiveAttributes = false;
2213
2370
  let deferredPrimaryRow;
2214
2371
  let unpublishedPrimaryStore;
2215
2372
  let published = false;
2216
- let releaseExclusiveLock;
2217
2373
  const attributesToIndex = [];
2218
2374
  const indicesToRemove = [];
2219
2375
  try {
2220
2376
  if (Table) {
2377
+ refreshedLiveAttributes = true;
2221
2378
  primaryKey = Table.primaryKey;
2222
2379
  if (Table.primaryStore.rootStore.status === 'closed') {
2223
2380
  throw new Error(`Can not use a closed data store from ${tableName} class`);
@@ -2243,11 +2400,15 @@ function declareTable(target, tableDefinition) {
2243
2400
  }
2244
2401
  }
2245
2402
  // Acquire before the first mutation of the live Table below, so a lost race leaves no
2246
- // attributes this worker describes but never persisted. Only the RocksDB acquire is bounded
2247
- // and can throw, and only it is cheap when uncontended: LMDB's exclusiveLock() opens an
2248
- // environment-wide write transaction that cannot time out, so it stays lazy.
2403
+ // attributes this worker describes but never persisted. Only the RocksDB acquire is bounded;
2404
+ // ordinary LMDB declarations stay lazy, while legacy HNSW normalization locks at entry.
2249
2405
  if (rootStore instanceof rocksdb_js_1.RocksDatabase)
2250
2406
  exclusiveLock();
2407
+ if (origin !== 'cluster') {
2408
+ const lockedAttributesDbi = Table.dbisDB;
2409
+ const persistedAuditUnderLock = persistedPrimaryDescriptor(lockedAttributesDbi).descriptor?.audit;
2410
+ validateHnswOptions(lockedAttributesDbi, auditExplicitlyEnabled || (!auditExplicitlyDisabled && persistedAuditUnderLock === true));
2411
+ }
2251
2412
  // it table already exists, get the split segments setting
2252
2413
  if (splitSegments == undefined)
2253
2414
  splitSegments = Table.splitSegments;
@@ -2271,6 +2432,13 @@ function declareTable(target, tableDefinition) {
2271
2432
  }
2272
2433
  attributes = merged;
2273
2434
  }
2435
+ else if (!attributes.some((attribute) => attribute.isPrimaryKey)) {
2436
+ const existingPrimary = Table.attributes.find((attribute) => attribute.isPrimaryKey);
2437
+ if (existingPrimary && attributes.some((attribute) => attribute.name === existingPrimary.name))
2438
+ throw new hdbError_ts_1.ClientError(`Cannot remove the primary key designation from '${databaseName}.${tableName}.${existingPrimary.name}'`);
2439
+ if (existingPrimary)
2440
+ attributes = [existingPrimary, ...attributes];
2441
+ }
2274
2442
  Table.attributes.splice(0, Table.attributes.length, ...attributes);
2275
2443
  // Re-assert from the live declaration so a stale value on disk (replicated event,
2276
2444
  // v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
@@ -2470,7 +2638,7 @@ function declareTable(target, tableDefinition) {
2470
2638
  const attribute = attributes.find((attribute) => attribute.name === attribute_name);
2471
2639
  const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
2472
2640
  // rows already present under a create are aborted state
2473
- const staleRow = !attribute || Boolean(deferredPrimaryRow);
2641
+ const staleRow = (!attribute && !value.isPrimaryKey) || Boolean(deferredPrimaryRow);
2474
2642
  if (staleRow || removeIndex) {
2475
2643
  exclusiveLock();
2476
2644
  hasChanges = true;
@@ -2483,9 +2651,75 @@ function declareTable(target, tableDefinition) {
2483
2651
  }
2484
2652
  }
2485
2653
  }
2654
+ const hasHnswDeclaration = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
2655
+ const persistedAudit = hasHnswDeclaration ? persistedPrimaryDescriptor(attributesDbi).descriptor?.audit : undefined;
2656
+ // A cluster declaration can apply audit on a create, but deliberately cannot rewrite an existing
2657
+ // table's primary row. Do not let an incoming audit value qualify a replicated native descriptor
2658
+ // that this node would then persist beside its durable audit:false row.
2659
+ const explicitAuditCanBeApplied = origin !== 'cluster' || Boolean(deferredPrimaryRow);
2660
+ const auditEnabledForNativeDefault = (auditExplicitlyEnabled && explicitAuditCanBeApplied) || (!auditExplicitlyDisabled && persistedAudit === true);
2661
+ const auditEnabledForNativePlane = !auditExplicitlyDisabled && (auditEnabledForNativeDefault || (persistedAudit == null && Table.audit === true));
2662
+ for (const attribute of attributes) {
2663
+ const indexed = attribute.indexed;
2664
+ if (!indexed || typeof indexed !== 'object' || indexed.type !== 'HNSW')
2665
+ continue;
2666
+ const descriptor = attributesDbi.getSync(tableName + '/' + (attribute.name || ''));
2667
+ const existingHnsw = descriptor?.indexed?.type === 'HNSW';
2668
+ if (indexed.nativePlane == null) {
2669
+ if (existingHnsw) {
2670
+ if (Object.hasOwn(descriptor.indexed, 'nativePlane')) {
2671
+ indexed.nativePlane = descriptor.indexed.nativePlane;
2672
+ }
2673
+ }
2674
+ else if (origin !== 'cluster' &&
2675
+ auditEnabledForNativeDefault &&
2676
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed)) {
2677
+ indexed.nativePlane = true;
2678
+ }
2679
+ }
2680
+ else if (origin === 'cluster' && !existingHnsw && indexed.nativePlane) {
2681
+ let canRunNative = false;
2682
+ try {
2683
+ canRunNative = auditEnabledForNativeDefault && customIndexes_ts_1.CUSTOM_INDEXES.HNSW.canRunNativePlane(rootStore, indexed);
2684
+ }
2685
+ catch { }
2686
+ if (!canRunNative) {
2687
+ logger.warn(`Using the JS HNSW index for replicated attribute ${databaseName}.${tableName}.${attribute.name} because this node does not satisfy the nativePlane requirements`);
2688
+ indexed.nativePlane = false;
2689
+ }
2690
+ }
2691
+ }
2692
+ const nativePlaneEnabled = origin !== 'cluster' &&
2693
+ attributes.some((attribute) => attribute.indexed?.type === 'HNSW' && attribute.indexed.nativePlane);
2694
+ if (nativePlaneEnabled && !auditEnabledForNativePlane) {
2695
+ throw new hdbError_ts_1.ClientError(`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`);
2696
+ }
2697
+ if (nativePlaneEnabled && persistedAudit !== true)
2698
+ audit = true;
2699
+ if (nativePlaneEnabled && persistedAudit !== true && !attributes.some((attribute) => attribute.isPrimaryKey)) {
2700
+ exclusiveLock();
2701
+ const primaryKey = primaryDescriptorKey();
2702
+ const primaryDescriptor = attributesDbi.getSync(primaryKey);
2703
+ if (primaryDescriptor && !tableIsDropping(primaryDescriptor, primaryKey)) {
2704
+ Table.enableAuditing();
2705
+ attributesDbi.put(primaryKey, { ...primaryDescriptor, audit: true });
2706
+ hasChanges = true;
2707
+ }
2708
+ }
2486
2709
  // TODO: If we have attributes and the schemaDefined flag is not set, turn it on
2487
2710
  // iterate through the attributes to ensure that we have all the dbis created and indexed
2488
- for (const attribute of attributes || []) {
2711
+ const attributesInPersistenceOrder = nativePlaneEnabled
2712
+ ? [
2713
+ ...attributes.filter((attribute) => attribute.isPrimaryKey),
2714
+ ...attributes.filter((attribute) => !attribute.isPrimaryKey),
2715
+ ]
2716
+ : auditExplicitlyDisabled
2717
+ ? [
2718
+ ...attributes.filter((attribute) => !attribute.isPrimaryKey),
2719
+ ...attributes.filter((attribute) => attribute.isPrimaryKey),
2720
+ ]
2721
+ : attributes;
2722
+ for (const attribute of attributesInPersistenceOrder) {
2489
2723
  if (attribute.relationship) {
2490
2724
  refreshRelationshipAttributes = true;
2491
2725
  continue;
@@ -2509,7 +2743,7 @@ function declareTable(target, tableDefinition) {
2509
2743
  // primary key can't change indexing, but settings can change
2510
2744
  if (origin !== 'cluster' &&
2511
2745
  (schemaDefinedMismatch ||
2512
- (audit !== undefined && audit !== Table.audit) ||
2746
+ (typeof audit === 'boolean' && audit !== attributeDescriptor.audit) ||
2513
2747
  (sealed !== undefined && sealed !== Table.sealed) ||
2514
2748
  (replicate !== undefined && replicate !== Table.replicate) ||
2515
2749
  (+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
@@ -2550,8 +2784,7 @@ function declareTable(target, tableDefinition) {
2550
2784
  applyDurableDeclaration(attribute, attributeDescriptor);
2551
2785
  const abandonedIndexBuild = attribute.indexed &&
2552
2786
  (attributeDescriptor.indexingFailed ||
2553
- (attributeDescriptor.indexingPID && attributeDescriptor.indexingPID !== process.pid) ||
2554
- attributeDescriptor.restartNumber < (worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber));
2787
+ isAbandonedIndexBuild(attributeDescriptor, worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber));
2555
2788
  if (abandonedIndexBuild) {
2556
2789
  // Recovery is the exception to skipping the handling below, because without it `isIndexing`
2557
2790
  // stays pinned on with nothing left to clear it and every query on the attribute fails with
@@ -2640,15 +2873,13 @@ function declareTable(target, tableDefinition) {
2640
2873
  if (changed ||
2641
2874
  indexFormatNeedsPersist ||
2642
2875
  attributeDescriptor?.indexingFailed ||
2643
- (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
2644
- attributeDescriptor?.restartNumber < currentRestartGeneration) {
2876
+ isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)) {
2645
2877
  hasChanges = true;
2646
2878
  exclusiveLock();
2647
2879
  attributeDescriptor = attributesDbi.getSync(dbiKey);
2648
2880
  if (structurallyChanged ||
2649
2881
  attributeDescriptor?.indexingFailed ||
2650
- (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
2651
- attributeDescriptor?.restartNumber < currentRestartGeneration) {
2882
+ isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)) {
2652
2883
  hasChanges = true;
2653
2884
  if (attribute.indexNulls === undefined)
2654
2885
  attribute.indexNulls = true;
@@ -2667,9 +2898,20 @@ function declareTable(target, tableDefinition) {
2667
2898
  // representation-only option difference — preserve the checkpoint so the backfill
2668
2899
  // resumes rather than restarts. Canonicalized to match structurallyChanged above.
2669
2900
  const indexOptionsChanged = canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
2670
- attribute.lastIndexedKey = indexOptionsChanged
2671
- ? undefined
2672
- : (attributeDescriptor?.lastIndexedKey ?? undefined);
2901
+ // Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
2902
+ // lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
2903
+ const uncertifiedCheckpoint = attributeDescriptor?.lastIndexedKey !== undefined &&
2904
+ (attributeDescriptor.checkpointCertified === undefined ||
2905
+ attributeDescriptor.checkpointAlgorithm !== exports.CHECKPOINT_ALGORITHM ||
2906
+ (0, lmdb_1.compareKeys)(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
2907
+ attribute.lastIndexedKey =
2908
+ indexOptionsChanged || uncertifiedCheckpoint
2909
+ ? undefined
2910
+ : (attributeDescriptor?.lastIndexedKey ?? undefined);
2911
+ if (attribute.lastIndexedKey !== undefined) {
2912
+ attribute.checkpointCertified = attribute.lastIndexedKey;
2913
+ attribute.checkpointAlgorithm = exports.CHECKPOINT_ALGORITHM;
2914
+ }
2673
2915
  // Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
2674
2916
  // object store to the versioned, VT-cacheable format. A full rebuild from scratch
2675
2917
  // (lastIndexedKey === undefined) clears the store and rewrites every node, so the
@@ -2691,6 +2933,9 @@ function declareTable(target, tableDefinition) {
2691
2933
  // the new process reuses the old PID. Cleared on clean completion; left in place
2692
2934
  // on failure/crash so the next, higher-numbered restart re-triggers the backfill.
2693
2935
  attribute.restartNumber = currentRestartGeneration;
2936
+ if (manageThreads.processIncarnation != null)
2937
+ attribute.indexingIncarnation = manageThreads.processIncarnation;
2938
+ attribute.indexingBuildId = (0, node_crypto_1.randomBytes)(8).toString('hex');
2694
2939
  delete attribute.indexingFailed; // clear failure flag for the new run
2695
2940
  dbi.isIndexing = true;
2696
2941
  Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
@@ -2706,6 +2951,12 @@ function declareTable(target, tableDefinition) {
2706
2951
  reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
2707
2952
  if (attributeDescriptor?.restartNumber < currentRestartGeneration)
2708
2953
  reindexReasons.push('restart-number');
2954
+ if (uncertifiedCheckpoint)
2955
+ reindexReasons.push('uncertified-checkpoint');
2956
+ if (attributeDescriptor?.indexingPID === process.pid &&
2957
+ manageThreads.processIncarnation != null &&
2958
+ attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation)
2959
+ reindexReasons.push('abandoned-build(previous process incarnation)');
2709
2960
  logger.info(`reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`);
2710
2961
  // we only set indexing nulls to true if new or reindexing, we can't have partial indexing of null
2711
2962
  attributesToIndex.push(attribute);
@@ -2718,12 +2969,22 @@ function declareTable(target, tableDefinition) {
2718
2969
  // workers / a reload would treat the still-partial index as ready and return incomplete results.
2719
2970
  attribute.indexingPID = attributeDescriptor.indexingPID;
2720
2971
  attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
2972
+ if (attributeDescriptor.checkpointCertified !== undefined) {
2973
+ attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
2974
+ attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
2975
+ }
2721
2976
  // Carry the in-progress restart generation too, so persisting this metadata-only
2722
2977
  // change doesn't drop it and break the crash-recovery trigger for the running backfill.
2723
2978
  attribute.restartNumber = attributeDescriptor.restartNumber;
2979
+ attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
2980
+ attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
2724
2981
  if (attributeDescriptor.indexingFailed)
2725
2982
  attribute.indexingFailed = attributeDescriptor.indexingFailed;
2726
2983
  }
2984
+ // The declared attribute never carries the stamp, so any rewrite of a descriptor that has
2985
+ // one would drop it and make a completed index look like a pre-stamp build.
2986
+ if (attribute.checkpointAlgorithm === undefined && attributeDescriptor?.checkpointAlgorithm !== undefined)
2987
+ attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
2727
2988
  attributesDbi.put(dbiKey, attribute);
2728
2989
  }
2729
2990
  // If a migration is in progress (indexingPID set), any newly opened dbi must also
@@ -2784,32 +3045,39 @@ function declareTable(target, tableDefinition) {
2784
3045
  finally {
2785
3046
  releaseLock();
2786
3047
  }
2787
- if (hasChanges || refreshRelationshipAttributes) {
3048
+ if (hasChanges || refreshRelationshipAttributes)
2788
3049
  Table.schemaVersion++;
3050
+ if (hasChanges || refreshRelationshipAttributes || refreshedLiveAttributes)
2789
3051
  Table.updatedAttributes();
2790
- }
2791
3052
  logger.trace(`${tableName} table loading, running index`);
2792
3053
  const branchPath = target.branch?.path;
2793
3054
  if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
2794
- Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath);
3055
+ // captured before the backfill can rewrite the attributes
3056
+ const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
3057
+ const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
3058
+ Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath).then(markSettled, markSettled);
2795
3059
  }
2796
3060
  else if (hasChanges)
2797
3061
  signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
3062
+ void Table.derivedIndexRuntime?.close();
3063
+ Table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(Table);
2798
3064
  Table.origin = origin;
2799
3065
  // scope-private: replication and other global subscribers must not learn of a branch class
2800
3066
  if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
2801
3067
  exports.databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
2802
3068
  }
2803
- if (expiration || eviction || scanInterval)
3069
+ if (expiration || eviction || scanInterval || attributes.some((attribute) => attribute.expiresAt))
2804
3070
  Table.setTTLExpiration({
2805
3071
  expiration,
2806
3072
  eviction,
2807
3073
  scanInterval,
3074
+ fromSchema: true,
3075
+ isolatedApplicationOwner,
2808
3076
  });
2809
3077
  logger.trace(`${tableName} table loaded`);
2810
3078
  return Table;
2811
- // dropTable() tombstones the bare table row, which is not the row a legacy catalog keeps the
2812
- // table's settings in, so a drop in flight has to be checked on both.
3079
+ // A migrated catalog can retain a named primary descriptor beside a bare table tombstone, so a
3080
+ // drop in flight has to be checked on both representations.
2813
3081
  function tableIsDropping(descriptor, descriptorKey) {
2814
3082
  if (descriptor?.dropping)
2815
3083
  return true;
@@ -2818,13 +3086,7 @@ function declareTable(target, tableDefinition) {
2818
3086
  // The catalog row initStores() reads a table's settings from: the primary key's own row when it
2819
3087
  // has one, and the bare table row otherwise.
2820
3088
  function primaryDescriptorKey() {
2821
- const declaredPrimaryKey = attributes?.find((attribute) => attribute.isPrimaryKey)?.name;
2822
- if (declaredPrimaryKey) {
2823
- const attributeKey = tableName + '/' + declaredPrimaryKey;
2824
- if (attributesDbi.getSync(attributeKey))
2825
- return attributeKey;
2826
- }
2827
- return tableName + '/';
3089
+ return persistedPrimaryDescriptor(attributesDbi).key;
2828
3090
  }
2829
3091
  // The catalog of a published table stays, but a class the registration never accepted is
2830
3092
  // unreachable, so release what makeTable() registered process-wide instead of leaving its timers
@@ -2892,73 +3154,238 @@ function declareTable(target, tableDefinition) {
2892
3154
  }
2893
3155
  }
2894
3156
  /**
2895
- * Canonical form used ONLY for the structural (reindex-triggering) comparison of index options.
2896
- * `@indexed(...)` records options in source-argument order and as strings, while the operations API
2897
- * and config objects can supply them reordered or as numbers; without canonicalizing, such a
2898
- * representation-only difference flips the structural comparison and forces a needless full rebuild
2899
- * (clearing + rebuilding the index, 503-ing the attribute throughout) for a semantically identical
2900
- * index. Sorts object keys and coerces numeric-looking (non-zero) string scalars to numbers.
2901
- * Conservative by design: boolean-vs-object, absent-vs-present, and string-"0"-vs-number-0
2902
- * differences are all preserved, so a genuine change (`true` vs `{ type: 'HNSW' }`, an added/removed
2903
- * option, a changed value) still triggers a rebuild. Persistence keys off the raw form, so the stored
2904
- * descriptor self-heals toward this shape over time. harper#1357
3157
+ * Stable structural form for deciding whether an index must be rebuilt. `coerceZero` extends numeric
3158
+ * coercion to zero; a truthiness-sensitive numeric option must normalize its value before using it.
2905
3159
  */
2906
- function canonicalizeIndexOptions(value) {
3160
+ function canonicalizeIndexOptions(value, coerceZero = false) {
2907
3161
  if (Array.isArray(value))
2908
- return value.map(canonicalizeIndexOptions);
3162
+ return value.map((item) => canonicalizeIndexOptions(item, coerceZero));
2909
3163
  if (value && typeof value === 'object') {
2910
3164
  const canonical = {};
2911
- for (const key of Object.keys(value).sort())
2912
- canonical[key] = canonicalizeIndexOptions(value[key]);
3165
+ const customIndex = value.type && customIndexes_ts_1.CUSTOM_INDEXES[value.type];
3166
+ for (const key of Object.keys(value).sort()) {
3167
+ if (customIndex?.truthyStructuralOptions?.has(key)) {
3168
+ if (value[key])
3169
+ canonical[key] = true;
3170
+ continue;
3171
+ }
3172
+ const optionValue = customIndex?.normalizeOptionValue
3173
+ ? customIndex.normalizeOptionValue(key, value[key])
3174
+ : value[key];
3175
+ canonical[key] = canonicalizeIndexOptions(optionValue, coerceZero || Boolean(customIndex?.numericOptions?.has(key)));
3176
+ }
2913
3177
  return canonical;
2914
3178
  }
2915
- // Coerce numeric-looking strings ("16" -> 16) so string-vs-number representations of the same
2916
- // option compare equal — EXCEPT zero: the string "0" is truthy while the number 0 is falsy, and
2917
- // index code may branch on truthiness (e.g. HNSW `if (this.optimizeRouting)` doubles maxConnections),
2918
- // so "0" and 0 build structurally different indexes and must still trigger a rebuild. Zero is the
2919
- // only finite number whose string and numeric forms diverge in truthiness, so excluding it fully
2920
- // closes that gap. Leave non-numeric strings, booleans, null, etc. intact.
2921
3179
  if (typeof value === 'string' && value.trim() !== '') {
2922
3180
  const numeric = Number(value);
2923
- if (numeric !== 0 && Number.isFinite(numeric))
3181
+ if ((numeric !== 0 || coerceZero) && Number.isFinite(numeric))
2924
3182
  return numeric;
2925
3183
  }
2926
3184
  return value;
2927
3185
  }
3186
+ // Bumped when a change alters which keys a checkpoint may certify. A descriptor stamped by any other
3187
+ // version resumes as uncertified (full rebuild) rather than being trusted, and a completed index keeps
3188
+ // the stamp of the build that wrote it.
3189
+ exports.CHECKPOINT_ALGORITHM = 2;
2928
3190
  const MAX_OUTSTANDING_INDEXING = 1000;
2929
3191
  const MIN_OUTSTANDING_INDEXING = 10;
3192
+ const INDEXING_YIELD_INTERVAL = 100;
3193
+ // A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
3194
+ // and never before this many more records: the flush seals every column family in the database, so a
3195
+ // slow backfill must not impose the period's flush rate on unrelated tables.
3196
+ let indexingCheckpointPeriodMs = 5000;
3197
+ let indexingCheckpointMinRecords = 10000;
3198
+ function setIndexingCheckpointPeriod(ms, minRecords = indexingCheckpointMinRecords) {
3199
+ const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
3200
+ indexingCheckpointPeriodMs = ms;
3201
+ indexingCheckpointMinRecords = minRecords;
3202
+ return previous;
3203
+ }
3204
+ const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
3205
+ // RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
3206
+ // entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
3207
+ // never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
3208
+ // shares — at most one in flight and one queued.
3209
+ const indexingFlushes = new WeakMap();
3210
+ function flushIndexStores(rootStore) {
3211
+ if (!(rootStore instanceof rocksdb_js_1.RocksDatabase))
3212
+ return;
3213
+ let flushes = indexingFlushes.get(rootStore);
3214
+ if (!flushes)
3215
+ indexingFlushes.set(rootStore, (flushes = {}));
3216
+ if (flushes.queued)
3217
+ return flushes.queued;
3218
+ const start = () => {
3219
+ flushes.queued = undefined;
3220
+ const flush = rootStore.flush().finally(() => {
3221
+ if (flushes.inFlight === flush)
3222
+ flushes.inFlight = undefined;
3223
+ });
3224
+ flushes.inFlight = flush;
3225
+ return flush;
3226
+ };
3227
+ if (!flushes.inFlight)
3228
+ return start();
3229
+ return (flushes.queued = flushes.inFlight.then(start, start));
3230
+ }
3231
+ function resumeStartKey(attributes) {
3232
+ let start;
3233
+ for (const attribute of attributes) {
3234
+ if (attribute.lastIndexedKey == undefined)
3235
+ return undefined;
3236
+ if (start === undefined || (0, lmdb_1.compareKeys)(attribute.lastIndexedKey, start) < 0)
3237
+ start = attribute.lastIndexedKey;
3238
+ }
3239
+ return start;
3240
+ }
3241
+ /**
3242
+ * Persists the failure marker for a build that ended without running one of runIndexing's own exit
3243
+ * paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
3244
+ * serialization boundary, because a replacement generation (or another thread declaring different index
3245
+ * options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
3246
+ * a live build. The fence read and write stay synchronous, and nothing here may throw because
3247
+ * `Table.indexingOperation` reaches operations-API callers.
3248
+ */
3249
+ async function markAbandonedIndexBuild(Table, rootStore, buildIds) {
3250
+ for (const [attribute, buildId] of buildIds) {
3251
+ try {
3252
+ let marked;
3253
+ if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId)
3254
+ continue;
3255
+ const markIfOwned = () => {
3256
+ const descriptor = Table.dbisDB.getSync(attribute.key);
3257
+ if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
3258
+ Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
3259
+ marked = true;
3260
+ }
3261
+ };
3262
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
3263
+ (0, Table_ts_1.acquireUpdateAttributesLock)(rootStore, `abandoned index build '${Table.tableName}.${attribute.name}'`);
3264
+ try {
3265
+ markIfOwned();
3266
+ }
3267
+ finally {
3268
+ (0, Table_ts_1.releaseUpdateAttributesLock)(rootStore);
3269
+ }
3270
+ }
3271
+ else {
3272
+ rootStore.transactionSync(markIfOwned);
3273
+ }
3274
+ if (marked)
3275
+ logger.warn(`Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
3276
+ `The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
3277
+ `the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`);
3278
+ }
3279
+ catch (error) {
3280
+ // A store closed by shutdown is the common case, and it cannot be written to at all.
3281
+ try {
3282
+ logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
3283
+ }
3284
+ catch { }
3285
+ }
3286
+ }
3287
+ }
2930
3288
  async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
3289
+ let checkpointing;
3290
+ let hadIndexingErrors = false;
3291
+ const attributeErrorReported = {};
3292
+ const onIndexPutRejected = (property, error) => {
3293
+ hadIndexingErrors = true;
3294
+ if (attributeErrorReported[property])
3295
+ return;
3296
+ attributeErrorReported[property] = true;
3297
+ logger.error(`Error indexing attribute ${property}`, error);
3298
+ };
3299
+ const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
2931
3300
  try {
2932
3301
  logger.info(`Indexing ${Table.tableName} attributes`, attributes);
2933
3302
  await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
2934
3303
  let lastResolution;
3304
+ // The checkpoint and completion barriers have to cover every mutation still in flight: any of them
3305
+ // may reject after those barriers read hadIndexingErrors.
3306
+ const pendingMutations = new Set();
3307
+ let settleWaiter;
3308
+ const track = (result, onRejected) => {
3309
+ if (!result?.then)
3310
+ return result;
3311
+ const tracked = result.then(() => {
3312
+ pendingMutations.delete(tracked);
3313
+ settleWaiter?.();
3314
+ return false;
3315
+ }, (error) => {
3316
+ pendingMutations.delete(tracked);
3317
+ settleWaiter?.();
3318
+ onRejected(error);
3319
+ return true;
3320
+ });
3321
+ pendingMutations.add(tracked);
3322
+ return result;
3323
+ };
3324
+ // The tracked promises absorb their own rejections, so one failure never abandons its siblings.
3325
+ const drainMutations = async () => {
3326
+ if (!pendingMutations.size)
3327
+ return false;
3328
+ return (await Promise.all([...pendingMutations])).some(Boolean);
3329
+ };
3330
+ // Waiting on a chosen entry would stall behind a slow one the others have already overtaken.
3331
+ const nextSettlement = () => new Promise((resolve) => {
3332
+ settleWaiter = () => {
3333
+ settleWaiter = undefined;
3334
+ resolve(undefined);
3335
+ };
3336
+ });
2935
3337
  for (const index of indicesToRemove) {
2936
- lastResolution = index.drop();
3338
+ index.customIndex?.resetDerivedStorage?.();
3339
+ track(index.drop(), (error) => onIndexPutRejected(index.name, error));
2937
3340
  }
2938
3341
  let interrupted;
2939
- let hadIndexingErrors = false;
2940
- const attributeErrorReported = {};
2941
3342
  let indexed = 0;
2942
3343
  const attributesLength = attributes.length;
2943
3344
  await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
2944
3345
  if (attributesLength > 0) {
2945
- let start;
2946
- for (const attribute of attributes) {
2947
- // if we are resuming, we need to start from the last key we indexed by all attributes
2948
- if ((0, lmdb_1.compareKeys)(attribute.lastIndexedKey, start) < 0)
2949
- start = attribute.lastIndexedKey;
2950
- if (attribute.lastIndexedKey == undefined) {
3346
+ const start = resumeStartKey(attributes);
3347
+ if (start === undefined) {
3348
+ for (const attribute of attributes) {
2951
3349
  // if we are starting from the beginning, clear out any previous index entries since we are rewriting
3350
+ attribute.dbi.customIndex?.resetDerivedStorage?.();
2952
3351
  if (attribute.dbi.clearAsync) {
2953
- // LMDB, note that we don't need to wait for this to complete, just gets enqueued in front of the other writes
2954
- attribute.dbi.clearAsync();
3352
+ // LMDB enqueues this ahead of the index writes, so the scan need not wait for it — but the
3353
+ // barriers must, or a rejected clear certifies a checkpoint over stale entries.
3354
+ track(attribute.dbi.clearAsync(), (error) => onIndexPutRejected(attribute.name, error));
2955
3355
  }
2956
3356
  else {
2957
3357
  await attribute.dbi.clear();
2958
3358
  }
2959
3359
  }
2960
3360
  }
2961
- let outstanding = 0;
3361
+ // A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
3362
+ // durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
3363
+ // record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
3364
+ const persistCheckpoint = async (key) => {
3365
+ if (hadIndexingErrors)
3366
+ return;
3367
+ try {
3368
+ // Everything still in flight was issued for a key at or before this one: the scan has not
3369
+ // moved past it yet. So a failure among them is a failure this checkpoint would cover.
3370
+ const failed = await drainMutations();
3371
+ if (failed)
3372
+ return;
3373
+ await flushIndexStores(Table.primaryStore.rootStore);
3374
+ const puts = [];
3375
+ for (const attribute of attributes) {
3376
+ attribute.lastIndexedKey = key;
3377
+ attribute.checkpointCertified = key;
3378
+ attribute.checkpointAlgorithm = exports.CHECKPOINT_ALGORITHM;
3379
+ puts.push(Table.dbisDB.put(attribute.key, attribute));
3380
+ }
3381
+ await Promise.all(puts);
3382
+ }
3383
+ catch (error) {
3384
+ logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
3385
+ }
3386
+ };
3387
+ let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
3388
+ let nextCheckpointRecord = indexingCheckpointMinRecords;
2962
3389
  // this means that a new attribute has been introduced that needs to be indexed
2963
3390
  for (const { key, value: record } of Table.primaryStore.getRange({
2964
3391
  start,
@@ -2966,96 +3393,90 @@ async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
2966
3393
  versions: true,
2967
3394
  snapshot: false, // don't hold a read transaction this whole time
2968
3395
  })) {
2969
- if (!record)
2970
- continue; // deletion entry
3396
+ const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
2971
3397
  // TODO: Do we ever need to interrupt due to a schema change that was not a restart?
2972
3398
  //if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
2973
- outstanding++;
2974
- // Custom indexes (e.g. HNSW) index synchronously and never raise `outstanding`, so the
2975
- // outstanding-based yield below never fires for them. Track that this row did synchronous
3399
+ // Custom indexes (e.g. HNSW) index synchronously and leave pendingMutations empty, so the
3400
+ // backpressure yield below never fires for them. Track that this row did synchronous
2976
3401
  // indexing work so we can still yield the event loop after it.
2977
3402
  let didSynchronousIndexing = false;
2978
3403
  // every index operation needs to be guarded by the version still be the same. If it has already changed before
2979
3404
  // we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
2980
3405
  // during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
2981
3406
  // performed indexing and we don't need to do anything further
2982
- for (let i = 0; i < attributesLength; i++) {
2983
- const attribute = attributes[i];
2984
- const property = attribute.name;
2985
- const index = attribute.dbi;
2986
- try {
2987
- const resolver = attribute.resolve;
2988
- const value = record && (resolver ? resolver(record) : record[property]);
2989
- if (index.customIndex) {
2990
- index.customIndex.index(key, value);
2991
- didSynchronousIndexing = true;
2992
- continue;
2993
- }
2994
- const values = (0, commonUtility_ts_1.getIndexedValues)(value, index.indexNulls);
2995
- if (values) {
2996
- for (let i = 0, l = values.length; i < l; i++) {
2997
- lastResolution = index.put(values[i], key);
3407
+ if (record) {
3408
+ for (let i = 0; i < attributesLength; i++) {
3409
+ const attribute = attributes[i];
3410
+ const property = attribute.name;
3411
+ const index = attribute.dbi;
3412
+ const onPutRejected = putRejectionHandlers[i];
3413
+ try {
3414
+ const resolver = attribute.resolve;
3415
+ const value = record && (resolver ? resolver(record) : record[property]);
3416
+ if (index.customIndex) {
3417
+ index.customIndex.index(key, value);
3418
+ didSynchronousIndexing = true;
3419
+ continue;
3420
+ }
3421
+ const values = (0, commonUtility_ts_1.getIndexedValues)(value, index.indexNulls);
3422
+ if (values) {
3423
+ for (let i = 0, l = values.length; i < l; i++) {
3424
+ track(index.put(values[i], key), onPutRejected);
3425
+ }
2998
3426
  }
2999
3427
  }
3000
- }
3001
- catch (error) {
3002
- hadIndexingErrors = true;
3003
- if (!attributeErrorReported[property]) {
3004
- // just report an indexing error once per attribute so we don't spam the logs.
3005
- // A store closed by worker shutdown surfaces here as "Database not open"; that is
3006
- // a benign interruption (the next generation re-runs the backfill), so don't log
3007
- // it as an error — the outer catch returns quietly once the iterator also throws.
3008
- attributeErrorReported[property] = true;
3009
- if (Table.primaryStore?.rootStore?.status === 'closed')
3010
- logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
3011
- else
3012
- logger.error(`Error indexing attribute ${property}`, error);
3428
+ catch (error) {
3429
+ hadIndexingErrors = true;
3430
+ if (!attributeErrorReported[property]) {
3431
+ // just report an indexing error once per attribute so we don't spam the logs.
3432
+ // A store closed by worker shutdown surfaces here as "Database not open"; that is
3433
+ // a benign interruption (the next generation re-runs the backfill), so don't log
3434
+ // it as an error — the outer catch returns quietly once the iterator also throws.
3435
+ attributeErrorReported[property] = true;
3436
+ if (Table.primaryStore?.rootStore?.status === 'closed')
3437
+ logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
3438
+ else
3439
+ logger.error(`Error indexing attribute ${property}`, error);
3440
+ }
3013
3441
  }
3014
3442
  }
3015
3443
  }
3016
- (0, when_ts_1.when)(lastResolution, () => outstanding--, (error) => {
3017
- outstanding--;
3018
- hadIndexingErrors = true;
3019
- logger.error(error);
3020
- });
3021
3444
  if (worker_threads_1.workerData && worker_threads_1.workerData.restartNumber !== manageThreads.restartNumber) {
3022
3445
  interrupted = true;
3023
3446
  }
3024
- if (++indexed % 100 === 0 || interrupted) {
3025
- // occasionally update our progress so if we crash, we can resume
3026
- for (const attribute of attributes) {
3027
- attribute.lastIndexedKey = key;
3028
- Table.dbisDB.put(attribute.key, attribute);
3029
- }
3030
- if (interrupted)
3031
- return;
3447
+ if (interrupted) {
3448
+ await drainMutations();
3449
+ await checkpointing;
3450
+ await persistCheckpoint(key);
3451
+ return;
3032
3452
  }
3033
- if (outstanding > MAX_OUTSTANDING_INDEXING)
3034
- await lastResolution;
3035
- else if (outstanding > MIN_OUTSTANDING_INDEXING)
3036
- await new Promise((resolve) => setImmediate(resolve)); // yield event turn, don't want to use all computation
3037
- else if (didSynchronousIndexing)
3038
- await new Promise((resolve) => setImmediate(resolve)); // custom indexes (e.g. HNSW) index synchronously and never raise `outstanding`; without this yield a large backfill runs in a single event-loop turn, starving keepalive/replication and queries and never letting the isIndexing flag be observed
3039
- }
3040
- }
3041
- // Await the last pending put. If it rejects, that is also an indexing error.
3042
- // Note: the when() calls above already attach rejection handlers to each record's
3043
- // last-put promise; this try-catch specifically handles the case where lastResolution
3044
- // itself rejects (i.e. the very last put in the loop failed) which would otherwise
3045
- // throw past the hadIndexingErrors check to the outer catch. The broader issue of
3046
- // unhandled rejections from non-last puts in multi-value attributes is pre-existing
3047
- // and out of scope for this fix.
3048
- try {
3049
- await lastResolution;
3050
- }
3051
- catch (error) {
3052
- hadIndexingErrors = true;
3053
- logger.error(error);
3453
+ if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
3454
+ nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
3455
+ nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
3456
+ await checkpointing;
3457
+ checkpointing = persistCheckpoint(key);
3458
+ }
3459
+ // Checked once per record, so a record's own fan-out can overshoot before the bound applies.
3460
+ while (pendingMutations.size > MAX_OUTSTANDING_INDEXING)
3461
+ await nextSettlement();
3462
+ if (atInterval || didSynchronousIndexing || pendingMutations.size > MIN_OUTSTANDING_INDEXING)
3463
+ await yieldEventTurn();
3464
+ }
3465
+ }
3466
+ await checkpointing;
3467
+ // A mutation that rejects after completion is declared has no build left to park.
3468
+ await drainMutations();
3469
+ // the tail since the last checkpoint is not durable until flushed; announcing the index complete
3470
+ // before that would outlive a crash that loses it
3471
+ if (!hadIndexingErrors) {
3472
+ try {
3473
+ await flushIndexStores(Table.primaryStore.rootStore);
3474
+ }
3475
+ catch (error) {
3476
+ hadIndexingErrors = true;
3477
+ logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
3478
+ }
3054
3479
  }
3055
- // Yield one more event turn so any queued when() error callbacks (which fire as
3056
- // microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
3057
- // before we decide whether to mark indexing as complete.
3058
- await new Promise((resolve) => setImmediate(resolve));
3059
3480
  if (hadIndexingErrors) {
3060
3481
  // Some records failed to index. Persist the failure marker in the descriptor so
3061
3482
  // the next call to table() (including after a restart with a fresh PID) re-triggers
@@ -3084,9 +3505,15 @@ async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
3084
3505
  // update the attributes to indicate that we are finished
3085
3506
  for (const attribute of attributes) {
3086
3507
  delete attribute.lastIndexedKey;
3508
+ delete attribute.checkpointCertified;
3509
+ // Survives completion, unlike the checkpoint fields: without it an index built here is
3510
+ // indistinguishable from one a release that could skip a failed record declared complete.
3511
+ attribute.checkpointAlgorithm = exports.CHECKPOINT_ALGORITHM;
3087
3512
  delete attribute.indexingPID;
3088
3513
  delete attribute.indexingFailed;
3089
3514
  delete attribute.restartNumber;
3515
+ delete attribute.indexingIncarnation;
3516
+ delete attribute.indexingBuildId;
3090
3517
  attribute.dbi.isIndexing = false;
3091
3518
  // Also clear isIndexing on the currently-active dbi in Table.indices, which may
3092
3519
  // differ from attribute.dbi if a resetDatabases() call during this migration
@@ -3103,6 +3530,7 @@ async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
3103
3530
  }
3104
3531
  }
3105
3532
  catch (error) {
3533
+ await checkpointing;
3106
3534
  // A worker shutting down closes its stores mid-backfill, so the range iterator or a
3107
3535
  // put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
3108
3536
  // interruption, not a data error: the next worker generation re-runs the backfill via
@@ -3166,6 +3594,26 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableNa
3166
3594
  finally {
3167
3595
  columnStore.close();
3168
3596
  }
3597
+ // derived HNSW plane files live next to the store; the normal drop path removes
3598
+ // them through the custom index, but this recovery path drops raw column stores,
3599
+ // and a same-name recreate must never open a stale plane over a fresh CF
3600
+ try {
3601
+ (0, node_fs_1.unlinkSync)((0, hnswPlaneBinding_ts_1.planeFilePathFor)(rootStore.path, columnName));
3602
+ }
3603
+ catch (error) {
3604
+ // a stale plane left behind (e.g. Windows EBUSY while still mapped) would be
3605
+ // opened over a fresh same-name CF, resolving another graph's node ids
3606
+ // against it — tombstone it so no attach ever adopts it
3607
+ if (error?.code !== 'ENOENT') {
3608
+ logger.warn(`could not delete the HNSW plane file for ${columnName}; tombstoning it as stale`, error);
3609
+ try {
3610
+ (0, node_fs_1.closeSync)((0, node_fs_1.openSync)((0, hnswPlaneBinding_ts_1.planeStalePathFor)((0, hnswPlaneBinding_ts_1.planeFilePathFor)(rootStore.path, columnName)), 'w'));
3611
+ }
3612
+ catch (tombstoneError) {
3613
+ logger.warn(`could not tombstone the stale HNSW plane file for ${columnName}`, tombstoneError);
3614
+ }
3615
+ }
3616
+ }
3169
3617
  }
3170
3618
  }
3171
3619
  }