@harperfast/harper 5.2.10 → 5.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (554) hide show
  1. package/agent/mcpTools.ts +1 -1
  2. package/agent/session.ts +25 -14
  3. package/bin/cliOperations.ts +46 -9
  4. package/bin/copyDb.ts +282 -59
  5. package/bin/deploySetup.ts +16 -5
  6. package/bin/harper.ts +1 -1
  7. package/bin/help.ts +4 -1
  8. package/bin/lite.ts +4 -1
  9. package/bin/restart.ts +59 -3
  10. package/bin/run.ts +6 -11
  11. package/bin/upgrade.js +7 -3
  12. package/bin/workloadIdentity.ts +119 -0
  13. package/components/Application.ts +2414 -242
  14. package/components/ApplicationScope.ts +8 -0
  15. package/components/EntryHandler.ts +59 -39
  16. package/components/OptionsWatcher.ts +150 -74
  17. package/components/RuntimeModuleTracker.ts +38 -7
  18. package/components/Scope.ts +37 -15
  19. package/components/awaitRestart.ts +84 -0
  20. package/components/componentLoader.ts +340 -30
  21. package/components/componentPreparationLock.ts +16 -5
  22. package/components/mcp/adapters/harperHttp.ts +4 -0
  23. package/components/mcp/listChanged.ts +4 -0
  24. package/components/mcp/toolRegistry.ts +2 -0
  25. package/components/mcp/tools/operations.ts +9 -0
  26. package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
  27. package/components/operations.js +270 -78
  28. package/components/operationsValidation.js +49 -1
  29. package/components/status/ComponentStatusRegistry.ts +59 -0
  30. package/config/RootConfigWatcher.ts +80 -34
  31. package/config/configUtils.ts +291 -34
  32. package/config/harperConfigEnvVars.ts +170 -27
  33. package/config-root.schema.json +29 -0
  34. package/dataLayer/blobBackup.ts +160 -50
  35. package/dataLayer/delete.ts +6 -1
  36. package/dataLayer/harperBridge/ResourceBridge.ts +52 -8
  37. package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  38. package/dataLayer/hdbInfoController.ts +34 -1
  39. package/dataLayer/insert.ts +44 -1
  40. package/dataLayer/rocksdbBackup.ts +53 -10
  41. package/dataLayer/schema.ts +11 -1
  42. package/dataLayer/schemaDescribe.ts +8 -1
  43. package/dist/agent/mcpTools.js +1 -1
  44. package/dist/agent/mcpTools.js.map +1 -1
  45. package/dist/agent/session.d.ts +22 -0
  46. package/dist/agent/session.js +26 -15
  47. package/dist/agent/session.js.map +1 -1
  48. package/dist/bin/cliOperations.js +48 -9
  49. package/dist/bin/cliOperations.js.map +1 -1
  50. package/dist/bin/copyDb.d.ts +12 -1
  51. package/dist/bin/copyDb.js +248 -60
  52. package/dist/bin/copyDb.js.map +1 -1
  53. package/dist/bin/deploySetup.d.ts +2 -0
  54. package/dist/bin/deploySetup.js +11 -3
  55. package/dist/bin/deploySetup.js.map +1 -1
  56. package/dist/bin/harper.js +1 -1
  57. package/dist/bin/harper.js.map +1 -1
  58. package/dist/bin/help.js +4 -1
  59. package/dist/bin/help.js.map +1 -1
  60. package/dist/bin/lite.js +4 -1
  61. package/dist/bin/lite.js.map +1 -1
  62. package/dist/bin/restart.js +54 -5
  63. package/dist/bin/restart.js.map +1 -1
  64. package/dist/bin/run.js +4 -10
  65. package/dist/bin/run.js.map +1 -1
  66. package/dist/bin/upgrade.js +4 -3
  67. package/dist/bin/upgrade.js.map +1 -1
  68. package/dist/bin/workloadIdentity.d.ts +18 -0
  69. package/dist/bin/workloadIdentity.js +100 -0
  70. package/dist/bin/workloadIdentity.js.map +1 -0
  71. package/dist/components/Application.d.ts +139 -16
  72. package/dist/components/Application.js +2215 -267
  73. package/dist/components/Application.js.map +1 -1
  74. package/dist/components/ApplicationScope.d.ts +8 -0
  75. package/dist/components/ApplicationScope.js +7 -0
  76. package/dist/components/ApplicationScope.js.map +1 -1
  77. package/dist/components/EntryHandler.js +26 -10
  78. package/dist/components/EntryHandler.js.map +1 -1
  79. package/dist/components/OptionsWatcher.d.ts +1 -0
  80. package/dist/components/OptionsWatcher.js +141 -74
  81. package/dist/components/OptionsWatcher.js.map +1 -1
  82. package/dist/components/RuntimeModuleTracker.js +40 -6
  83. package/dist/components/RuntimeModuleTracker.js.map +1 -1
  84. package/dist/components/Scope.js +38 -13
  85. package/dist/components/Scope.js.map +1 -1
  86. package/dist/components/awaitRestart.d.ts +33 -0
  87. package/dist/components/awaitRestart.js +61 -0
  88. package/dist/components/awaitRestart.js.map +1 -0
  89. package/dist/components/componentLoader.d.ts +38 -1
  90. package/dist/components/componentLoader.js +279 -22
  91. package/dist/components/componentLoader.js.map +1 -1
  92. package/dist/components/componentPreparationLock.d.ts +5 -0
  93. package/dist/components/componentPreparationLock.js +14 -6
  94. package/dist/components/componentPreparationLock.js.map +1 -1
  95. package/dist/components/mcp/adapters/harperHttp.js +4 -0
  96. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  97. package/dist/components/mcp/listChanged.js +5 -0
  98. package/dist/components/mcp/listChanged.js.map +1 -1
  99. package/dist/components/mcp/toolRegistry.d.ts +1 -0
  100. package/dist/components/mcp/toolRegistry.js.map +1 -1
  101. package/dist/components/mcp/tools/operations.d.ts +5 -0
  102. package/dist/components/mcp/tools/operations.js +9 -0
  103. package/dist/components/mcp/tools/operations.js.map +1 -1
  104. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
  105. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  106. package/dist/components/operations.js +231 -77
  107. package/dist/components/operations.js.map +1 -1
  108. package/dist/components/operationsValidation.js +49 -1
  109. package/dist/components/operationsValidation.js.map +1 -1
  110. package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
  111. package/dist/components/status/ComponentStatusRegistry.js +63 -0
  112. package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
  113. package/dist/config/RootConfigWatcher.d.ts +7 -1
  114. package/dist/config/RootConfigWatcher.js +64 -27
  115. package/dist/config/RootConfigWatcher.js.map +1 -1
  116. package/dist/config/configUtils.d.ts +9 -1
  117. package/dist/config/configUtils.js +254 -33
  118. package/dist/config/configUtils.js.map +1 -1
  119. package/dist/config/harperConfigEnvVars.d.ts +16 -0
  120. package/dist/config/harperConfigEnvVars.js +162 -25
  121. package/dist/config/harperConfigEnvVars.js.map +1 -1
  122. package/dist/dataLayer/blobBackup.d.ts +49 -20
  123. package/dist/dataLayer/blobBackup.js +139 -50
  124. package/dist/dataLayer/blobBackup.js.map +1 -1
  125. package/dist/dataLayer/delete.js +1 -1
  126. package/dist/dataLayer/delete.js.map +1 -1
  127. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
  128. package/dist/dataLayer/harperBridge/ResourceBridge.js +51 -10
  129. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  130. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
  131. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  132. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
  133. package/dist/dataLayer/hdbInfoController.d.ts +10 -0
  134. package/dist/dataLayer/hdbInfoController.js +30 -1
  135. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  136. package/dist/dataLayer/insert.d.ts +9 -1
  137. package/dist/dataLayer/insert.js +30 -0
  138. package/dist/dataLayer/insert.js.map +1 -1
  139. package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
  140. package/dist/dataLayer/rocksdbBackup.js +45 -8
  141. package/dist/dataLayer/rocksdbBackup.js.map +1 -1
  142. package/dist/dataLayer/schema.js +8 -0
  143. package/dist/dataLayer/schema.js.map +1 -1
  144. package/dist/dataLayer/schemaDescribe.js +8 -1
  145. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  146. package/dist/index.d.ts +1 -1
  147. package/dist/index.js.map +1 -1
  148. package/dist/json/systemSchema.json +52 -0
  149. package/dist/resources/DatabaseTransaction.d.ts +55 -3
  150. package/dist/resources/DatabaseTransaction.js +460 -134
  151. package/dist/resources/DatabaseTransaction.js.map +1 -1
  152. package/dist/resources/LMDBTransaction.js +22 -4
  153. package/dist/resources/LMDBTransaction.js.map +1 -1
  154. package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
  155. package/dist/resources/PrimaryRocksDatabase.js +30 -2
  156. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  157. package/dist/resources/RecordEncoder.d.ts +20 -0
  158. package/dist/resources/RecordEncoder.js +94 -9
  159. package/dist/resources/RecordEncoder.js.map +1 -1
  160. package/dist/resources/RequestTarget.d.ts +2 -0
  161. package/dist/resources/RequestTarget.js.map +1 -1
  162. package/dist/resources/Resource.js +20 -11
  163. package/dist/resources/Resource.js.map +1 -1
  164. package/dist/resources/ResourceInterface.d.ts +20 -1
  165. package/dist/resources/ResourceInterface.js.map +1 -1
  166. package/dist/resources/RocksIndexStore.d.ts +6 -1
  167. package/dist/resources/RocksIndexStore.js +24 -10
  168. package/dist/resources/RocksIndexStore.js.map +1 -1
  169. package/dist/resources/RocksTransactionLogStore.d.ts +14 -1
  170. package/dist/resources/RocksTransactionLogStore.js +57 -17
  171. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  172. package/dist/resources/Table.d.ts +112 -8
  173. package/dist/resources/Table.js +1071 -197
  174. package/dist/resources/Table.js.map +1 -1
  175. package/dist/resources/auditStore.d.ts +11 -2
  176. package/dist/resources/auditStore.js +183 -18
  177. package/dist/resources/auditStore.js.map +1 -1
  178. package/dist/resources/blob.d.ts +129 -9
  179. package/dist/resources/blob.js +992 -114
  180. package/dist/resources/blob.js.map +1 -1
  181. package/dist/resources/branchDatabase.d.ts +48 -0
  182. package/dist/resources/branchDatabase.js +892 -0
  183. package/dist/resources/branchDatabase.js.map +1 -0
  184. package/dist/resources/crdt.js +50 -12
  185. package/dist/resources/crdt.js.map +1 -1
  186. package/dist/resources/databases.d.ts +150 -9
  187. package/dist/resources/databases.js +1247 -525
  188. package/dist/resources/databases.js.map +1 -1
  189. package/dist/resources/defineTable.d.ts +10 -2
  190. package/dist/resources/defineTable.js +9 -1
  191. package/dist/resources/defineTable.js.map +1 -1
  192. package/dist/resources/graphql.d.ts +1 -1
  193. package/dist/resources/graphql.js +50 -15
  194. package/dist/resources/graphql.js.map +1 -1
  195. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +1 -0
  196. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +6 -2
  197. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  198. package/dist/resources/longLivedTransactions.d.ts +71 -0
  199. package/dist/resources/longLivedTransactions.js +358 -0
  200. package/dist/resources/longLivedTransactions.js.map +1 -0
  201. package/dist/resources/models/backendRegistry.d.ts +26 -0
  202. package/dist/resources/models/backendRegistry.js +60 -2
  203. package/dist/resources/models/backendRegistry.js.map +1 -1
  204. package/dist/resources/models/bootstrap.d.ts +33 -1
  205. package/dist/resources/models/bootstrap.js +416 -31
  206. package/dist/resources/models/bootstrap.js.map +1 -1
  207. package/dist/resources/recordLock.d.ts +80 -0
  208. package/dist/resources/recordLock.js +184 -0
  209. package/dist/resources/recordLock.js.map +1 -0
  210. package/dist/resources/replayLogs.d.ts +14 -1
  211. package/dist/resources/replayLogs.js +152 -24
  212. package/dist/resources/replayLogs.js.map +1 -1
  213. package/dist/resources/replayLogsGuards.d.ts +94 -7
  214. package/dist/resources/replayLogsGuards.js +111 -7
  215. package/dist/resources/replayLogsGuards.js.map +1 -1
  216. package/dist/resources/search.js +158 -32
  217. package/dist/resources/search.js.map +1 -1
  218. package/dist/resources/transactionBroadcast.d.ts +1 -1
  219. package/dist/resources/transactionBroadcast.js +2 -2
  220. package/dist/resources/transactionBroadcast.js.map +1 -1
  221. package/dist/security/auth.js +61 -30
  222. package/dist/security/auth.js.map +1 -1
  223. package/dist/security/authn/oidc/claims.d.ts +22 -0
  224. package/dist/security/authn/oidc/claims.js +71 -0
  225. package/dist/security/authn/oidc/claims.js.map +1 -0
  226. package/dist/security/authn/oidc/identityToken.d.ts +27 -0
  227. package/dist/security/authn/oidc/identityToken.js +111 -0
  228. package/dist/security/authn/oidc/identityToken.js.map +1 -0
  229. package/dist/security/authn/oidc/jwks.d.ts +25 -0
  230. package/dist/security/authn/oidc/jwks.js +261 -0
  231. package/dist/security/authn/oidc/jwks.js.map +1 -0
  232. package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
  233. package/dist/security/authn/oidc/providers/generic.js +34 -0
  234. package/dist/security/authn/oidc/providers/generic.js.map +1 -0
  235. package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
  236. package/dist/security/authn/oidc/providers/githubActions.js +129 -0
  237. package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
  238. package/dist/security/authn/oidc/providers/index.d.ts +37 -0
  239. package/dist/security/authn/oidc/providers/index.js +24 -0
  240. package/dist/security/authn/oidc/providers/index.js.map +1 -0
  241. package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
  242. package/dist/security/authn/oidc/tokenExchange.js +306 -0
  243. package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
  244. package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
  245. package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
  246. package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
  247. package/dist/security/authn/oidc/types.d.ts +38 -0
  248. package/dist/security/authn/oidc/types.js +6 -0
  249. package/dist/security/authn/oidc/types.js.map +1 -0
  250. package/dist/security/certificateVerification/index.js +40 -11
  251. package/dist/security/certificateVerification/index.js.map +1 -1
  252. package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
  253. package/dist/security/certificateVerification/trustedIssuers.js +79 -0
  254. package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
  255. package/dist/security/certificateVerification/types.d.ts +1 -0
  256. package/dist/security/credentialProvenance.d.ts +35 -0
  257. package/dist/security/credentialProvenance.js +51 -0
  258. package/dist/security/credentialProvenance.js.map +1 -0
  259. package/dist/security/credentialRejection.d.ts +4 -0
  260. package/dist/security/credentialRejection.js +24 -0
  261. package/dist/security/credentialRejection.js.map +1 -0
  262. package/dist/security/deferredAuthentication.d.ts +36 -0
  263. package/dist/security/deferredAuthentication.js +70 -0
  264. package/dist/security/deferredAuthentication.js.map +1 -0
  265. package/dist/security/impersonation.d.ts +21 -0
  266. package/dist/security/impersonation.js +108 -9
  267. package/dist/security/impersonation.js.map +1 -1
  268. package/dist/security/jsLoader.d.ts +6 -0
  269. package/dist/security/jsLoader.js +75 -15
  270. package/dist/security/jsLoader.js.map +1 -1
  271. package/dist/security/keys.js +301 -71
  272. package/dist/security/keys.js.map +1 -1
  273. package/dist/security/operationScope.d.ts +21 -0
  274. package/dist/security/operationScope.js +36 -0
  275. package/dist/security/operationScope.js.map +1 -0
  276. package/dist/security/permissionsTranslator.js +21 -0
  277. package/dist/security/permissionsTranslator.js.map +1 -1
  278. package/dist/security/tokenAuthentication.d.ts +19 -1
  279. package/dist/security/tokenAuthentication.js +191 -10
  280. package/dist/security/tokenAuthentication.js.map +1 -1
  281. package/dist/security/user.js +4 -3
  282. package/dist/security/user.js.map +1 -1
  283. package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
  284. package/dist/server/DurableSubscriptionsSession.js +67 -10
  285. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  286. package/dist/server/REST.js +73 -0
  287. package/dist/server/REST.js.map +1 -1
  288. package/dist/server/graphqlQuerying.js +4 -0
  289. package/dist/server/graphqlQuerying.js.map +1 -1
  290. package/dist/server/http.d.ts +11 -0
  291. package/dist/server/http.js +88 -15
  292. package/dist/server/http.js.map +1 -1
  293. package/dist/server/itc/serverHandlers.js +8 -1
  294. package/dist/server/itc/serverHandlers.js.map +1 -1
  295. package/dist/server/jobs/jobProcess.js +6 -2
  296. package/dist/server/jobs/jobProcess.js.map +1 -1
  297. package/dist/server/jobs/jobs.js +4 -1
  298. package/dist/server/jobs/jobs.js.map +1 -1
  299. package/dist/server/liveSubscriptionAuth.d.ts +26 -4
  300. package/dist/server/liveSubscriptionAuth.js +105 -39
  301. package/dist/server/liveSubscriptionAuth.js.map +1 -1
  302. package/dist/server/loadRootComponents.js +49 -10
  303. package/dist/server/loadRootComponents.js.map +1 -1
  304. package/dist/server/mqtt.d.ts +2 -0
  305. package/dist/server/mqtt.js +165 -30
  306. package/dist/server/mqtt.js.map +1 -1
  307. package/dist/server/nodeName.d.ts +2 -0
  308. package/dist/server/nodeName.js +107 -23
  309. package/dist/server/nodeName.js.map +1 -1
  310. package/dist/server/serverHelpers/Headers.d.ts +25 -0
  311. package/dist/server/serverHelpers/Headers.js +139 -1
  312. package/dist/server/serverHelpers/Headers.js.map +1 -1
  313. package/dist/server/serverHelpers/contentTypes.d.ts +9 -0
  314. package/dist/server/serverHelpers/contentTypes.js +32 -25
  315. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  316. package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
  317. package/dist/server/serverHelpers/deployValidationState.js +9 -19
  318. package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
  319. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
  320. package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
  321. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
  322. package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
  323. package/dist/server/serverHelpers/registeredOperations.js +74 -21
  324. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  325. package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
  326. package/dist/server/serverHelpers/requestSanitization.js +20 -0
  327. package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
  328. package/dist/server/serverHelpers/serverHandlers.js +6 -3
  329. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  330. package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
  331. package/dist/server/serverHelpers/serverUtilities.js +90 -20
  332. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  333. package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
  334. package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
  335. package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
  336. package/dist/server/serverHelpers/uwsServer.js +19 -1
  337. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  338. package/dist/server/static.js +24 -28
  339. package/dist/server/static.js.map +1 -1
  340. package/dist/server/storageReclamation.d.ts +5 -0
  341. package/dist/server/storageReclamation.js +16 -0
  342. package/dist/server/storageReclamation.js.map +1 -1
  343. package/dist/server/threads/itc.d.ts +7 -2
  344. package/dist/server/threads/itc.js +5 -1
  345. package/dist/server/threads/itc.js.map +1 -1
  346. package/dist/server/threads/manageThreads.d.ts +26 -4
  347. package/dist/server/threads/manageThreads.js +517 -72
  348. package/dist/server/threads/manageThreads.js.map +1 -1
  349. package/dist/server/threads/socketRouter.js +89 -26
  350. package/dist/server/threads/socketRouter.js.map +1 -1
  351. package/dist/server/threads/threadHeapMemory.d.ts +2 -0
  352. package/dist/server/threads/threadHeapMemory.js +31 -0
  353. package/dist/server/threads/threadHeapMemory.js.map +1 -0
  354. package/dist/server/threads/threadServer.js +46 -15
  355. package/dist/server/threads/threadServer.js.map +1 -1
  356. package/dist/sqlEngine/config.d.ts +1 -3
  357. package/dist/sqlEngine/config.js +19 -16
  358. package/dist/sqlEngine/config.js.map +1 -1
  359. package/dist/sqlTranslator/index.d.ts +1 -1
  360. package/dist/sqlTranslator/index.js +30 -7
  361. package/dist/sqlTranslator/index.js.map +1 -1
  362. package/dist/upgrade/directives/5-3-0.d.ts +7 -0
  363. package/dist/upgrade/directives/5-3-0.js +148 -0
  364. package/dist/upgrade/directives/5-3-0.js.map +1 -0
  365. package/dist/upgrade/directives/directivesController.js +2 -1
  366. package/dist/upgrade/directives/directivesController.js.map +1 -1
  367. package/dist/utility/OperationFunctionCaller.js +2 -1
  368. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  369. package/dist/utility/common_utils.d.ts +16 -0
  370. package/dist/utility/common_utils.js +32 -6
  371. package/dist/utility/common_utils.js.map +1 -1
  372. package/dist/utility/componentNames.d.ts +8 -0
  373. package/dist/utility/componentNames.js +12 -1
  374. package/dist/utility/componentNames.js.map +1 -1
  375. package/dist/utility/environment/environmentManager.js +3 -6
  376. package/dist/utility/environment/environmentManager.js.map +1 -1
  377. package/dist/utility/environment/systemInformation.d.ts +1 -0
  378. package/dist/utility/environment/systemInformation.js +1 -0
  379. package/dist/utility/environment/systemInformation.js.map +1 -1
  380. package/dist/utility/errors/commonErrors.d.ts +2 -0
  381. package/dist/utility/errors/commonErrors.js +2 -0
  382. package/dist/utility/errors/commonErrors.js.map +1 -1
  383. package/dist/utility/errors/hdbError.d.ts +17 -0
  384. package/dist/utility/errors/hdbError.js +30 -1
  385. package/dist/utility/errors/hdbError.js.map +1 -1
  386. package/dist/utility/globalSchema.d.ts +18 -0
  387. package/dist/utility/hdbTerms.d.ts +16 -0
  388. package/dist/utility/hdbTerms.js +18 -2
  389. package/dist/utility/hdbTerms.js.map +1 -1
  390. package/dist/utility/logging/harper_logger.d.ts +2 -0
  391. package/dist/utility/logging/harper_logger.js +91 -14
  392. package/dist/utility/logging/harper_logger.js.map +1 -1
  393. package/dist/utility/logging/logRotator.js +76 -49
  394. package/dist/utility/logging/logRotator.js.map +1 -1
  395. package/dist/utility/nodeIdentity.d.ts +9 -0
  396. package/dist/utility/nodeIdentity.js +58 -0
  397. package/dist/utility/nodeIdentity.js.map +1 -0
  398. package/dist/utility/npmUtilities.js +9 -7
  399. package/dist/utility/npmUtilities.js.map +1 -1
  400. package/dist/utility/operationPermissions.d.ts +3 -1
  401. package/dist/utility/operationPermissions.js +16 -1
  402. package/dist/utility/operationPermissions.js.map +1 -1
  403. package/dist/utility/operation_authorization.d.ts +10 -7
  404. package/dist/utility/operation_authorization.js +212 -41
  405. package/dist/utility/operation_authorization.js.map +1 -1
  406. package/dist/utility/watchPath.d.ts +29 -0
  407. package/dist/utility/watchPath.js +68 -0
  408. package/dist/utility/watchPath.js.map +1 -0
  409. package/dist/utility/watcherFallback.d.ts +86 -0
  410. package/dist/utility/watcherFallback.js +278 -1
  411. package/dist/utility/watcherFallback.js.map +1 -1
  412. package/dist/validation/configValidator.d.ts +12 -0
  413. package/dist/validation/configValidator.js +199 -72
  414. package/dist/validation/configValidator.js.map +1 -1
  415. package/dist/validation/installValidator.js +12 -0
  416. package/dist/validation/installValidator.js.map +1 -1
  417. package/dist/validation/validationWrapper.d.ts +11 -0
  418. package/dist/validation/validationWrapper.js +16 -3
  419. package/dist/validation/validationWrapper.js.map +1 -1
  420. package/index.ts +2 -0
  421. package/json/systemSchema.json +52 -0
  422. package/npm-shrinkwrap.json +235 -233
  423. package/package.json +8 -7
  424. package/resources/DESIGN.md +114 -52
  425. package/resources/DatabaseTransaction.ts +522 -130
  426. package/resources/LMDBTransaction.ts +26 -4
  427. package/resources/PrimaryRocksDatabase.ts +29 -3
  428. package/resources/RecordEncoder.ts +103 -8
  429. package/resources/RequestTarget.ts +2 -0
  430. package/resources/Resource.ts +17 -9
  431. package/resources/ResourceInterface.ts +23 -0
  432. package/resources/RocksIndexStore.ts +30 -10
  433. package/resources/RocksTransactionLogStore.ts +78 -21
  434. package/resources/Table.ts +1107 -164
  435. package/resources/auditStore.ts +187 -21
  436. package/resources/blob.ts +1029 -110
  437. package/resources/branchDatabase.ts +941 -0
  438. package/resources/crdt.ts +76 -12
  439. package/resources/databases.ts +1354 -522
  440. package/resources/defineTable.ts +18 -2
  441. package/resources/graphql.ts +70 -16
  442. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +6 -2
  443. package/resources/longLivedTransactions.ts +360 -0
  444. package/resources/models/backendRegistry.ts +84 -2
  445. package/resources/models/bootstrap.ts +473 -28
  446. package/resources/recordLock.ts +253 -0
  447. package/resources/replayLogs.ts +147 -26
  448. package/resources/replayLogsGuards.ts +171 -8
  449. package/resources/search.ts +154 -32
  450. package/resources/transactionBroadcast.ts +3 -3
  451. package/security/auth.ts +68 -29
  452. package/security/authn/oidc/claims.ts +72 -0
  453. package/security/authn/oidc/identityToken.ts +129 -0
  454. package/security/authn/oidc/jwks.ts +260 -0
  455. package/security/authn/oidc/providers/generic.ts +40 -0
  456. package/security/authn/oidc/providers/githubActions.ts +137 -0
  457. package/security/authn/oidc/providers/index.ts +52 -0
  458. package/security/authn/oidc/tokenExchange.ts +300 -0
  459. package/security/authn/oidc/trustPolicyOperations.ts +343 -0
  460. package/security/authn/oidc/types.ts +41 -0
  461. package/security/certificateVerification/index.ts +54 -13
  462. package/security/certificateVerification/trustedIssuers.ts +76 -0
  463. package/security/certificateVerification/types.ts +1 -0
  464. package/security/credentialProvenance.ts +47 -0
  465. package/security/credentialRejection.ts +22 -0
  466. package/security/deferredAuthentication.ts +71 -0
  467. package/security/impersonation.ts +117 -12
  468. package/security/jsLoader.ts +81 -18
  469. package/security/keys.ts +298 -72
  470. package/security/operationScope.ts +33 -0
  471. package/security/permissionsTranslator.js +23 -0
  472. package/security/tokenAuthentication.ts +233 -12
  473. package/security/user.ts +4 -3
  474. package/server/DESIGN.md +183 -16
  475. package/server/DurableSubscriptionsSession.ts +71 -11
  476. package/server/REST.ts +79 -1
  477. package/server/graphqlQuerying.ts +4 -0
  478. package/server/http.ts +99 -18
  479. package/server/itc/serverHandlers.js +8 -1
  480. package/server/jobs/jobProcess.ts +8 -2
  481. package/server/jobs/jobs.ts +4 -1
  482. package/server/liveSubscriptionAuth.ts +129 -46
  483. package/server/loadRootComponents.js +50 -8
  484. package/server/mqtt.ts +179 -38
  485. package/server/nodeName.ts +103 -21
  486. package/server/serverHelpers/Headers.ts +131 -0
  487. package/server/serverHelpers/contentTypes.ts +29 -21
  488. package/server/serverHelpers/deployValidationState.ts +24 -13
  489. package/server/serverHelpers/operationAuthorizationState.ts +34 -3
  490. package/server/serverHelpers/registeredOperations.ts +79 -22
  491. package/server/serverHelpers/requestSanitization.ts +15 -0
  492. package/server/serverHelpers/serverHandlers.js +6 -3
  493. package/server/serverHelpers/serverUtilities.ts +104 -31
  494. package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
  495. package/server/serverHelpers/uwsServer.ts +17 -2
  496. package/server/static.ts +23 -29
  497. package/server/storageReclamation.ts +13 -0
  498. package/server/threads/itc.js +11 -1
  499. package/server/threads/manageThreads.js +526 -63
  500. package/server/threads/socketRouter.ts +81 -26
  501. package/server/threads/threadHeapMemory.ts +26 -0
  502. package/server/threads/threadServer.js +43 -15
  503. package/sqlTranslator/index.ts +31 -8
  504. package/studio/web/assets/{Chat-Br06zdMA.js → Chat-BnCBegQz.js} +1 -1
  505. package/studio/web/assets/{FloatingChat-BWImX5fA.js → FloatingChat-CoDW1ySS.js} +3 -3
  506. package/studio/web/assets/{apiToken-DN0nmDsq.js → apiToken-Bwk5BLXW.js} +1 -1
  507. package/studio/web/assets/{applications-kSxVoyeU.js → applications-DHxGi7JH.js} +1 -1
  508. package/studio/web/assets/{cssMode-C1JeufH5.js → cssMode-s0cWI-_M.js} +1 -1
  509. package/studio/web/assets/{editor-qoo9CrGO.js → editor-DNcRHK54.js} +1 -1
  510. package/studio/web/assets/{html-Dt4IIy04.js → html-Bdssedlg.js} +1 -1
  511. package/studio/web/assets/{htmlMode-DXgKKr4C.js → htmlMode-CoDlJ3fw.js} +1 -1
  512. package/studio/web/assets/{index-6onkYFOG.js → index-D6sxmFLR.js} +5 -5
  513. package/studio/web/assets/{index.lazy-BrCFnpNJ.js → index.lazy-tmU5BS8s.js} +1 -1
  514. package/studio/web/assets/{javascript-DNCQGUBc.js → javascript-B8meVSTH.js} +1 -1
  515. package/studio/web/assets/{jsonMode-CR6HWruP.js → jsonMode-DpIPd35T.js} +1 -1
  516. package/studio/web/assets/{languageServices-BM4fI4rS.js → languageServices-C_5FMJzQ.js} +1 -1
  517. package/studio/web/assets/{lspLanguageFeatures-DSa1ttcD.js → lspLanguageFeatures-BIzNBzPK.js} +1 -1
  518. package/studio/web/assets/{notifications-BHXLnh6x.js → notifications-CQf18QKb.js} +1 -1
  519. package/studio/web/assets/{notifications-CUmtIA6z.js → notifications-CvZivSbh.js} +1 -1
  520. package/studio/web/assets/{profile-8BeFSF3j.js → profile-DdOwtntb.js} +1 -1
  521. package/studio/web/assets/{regions-C8qR0HhD.js → regions-n69fwagr.js} +1 -1
  522. package/studio/web/assets/{register-B4n5i0SD.js → register-PfWTCXWB.js} +2 -2
  523. package/studio/web/assets/{setComponentFile-g0_B0lgX.js → setComponentFile-Bg6O7X0S.js} +1 -1
  524. package/studio/web/assets/{setup-B0CTj_Q5.js → setup-CUx_aUDl.js} +2 -2
  525. package/studio/web/assets/{status-C6Yib7-K.js → status-D7BVKqX9.js} +1 -1
  526. package/studio/web/assets/{toggleHighContrast-Dgta7bVi.js → toggleHighContrast-DBSyXzMr.js} +1 -1
  527. package/studio/web/assets/{tsMode-CH_jHvU-.js → tsMode-BByKCjBS.js} +1 -1
  528. package/studio/web/assets/{typescript-Co9LCXd5.js → typescript-DDLnLpw9.js} +1 -1
  529. package/studio/web/assets/{useEntityRestURL-wKC8NsC_.js → useEntityRestURL-31CHGaHk.js} +1 -1
  530. package/studio/web/assets/{workers-CWeLxCXA.js → workers-pR3jRY9D.js} +1 -1
  531. package/studio/web/assets/{xml-BadC-0Rk.js → xml-2iRnMhQO.js} +1 -1
  532. package/studio/web/assets/{yaml-BiUfxPbC.js → yaml-Bf92gJpd.js} +1 -1
  533. package/studio/web/index.html +1 -1
  534. package/upgrade/directives/5-3-0.ts +132 -0
  535. package/upgrade/directives/directivesController.ts +2 -1
  536. package/utility/OperationFunctionCaller.ts +2 -1
  537. package/utility/common_utils.ts +30 -5
  538. package/utility/componentNames.ts +12 -0
  539. package/utility/environment/environmentManager.ts +3 -7
  540. package/utility/environment/systemInformation.ts +7 -0
  541. package/utility/errors/commonErrors.ts +4 -0
  542. package/utility/errors/hdbError.ts +29 -0
  543. package/utility/hdbTerms.ts +17 -0
  544. package/utility/logging/harper_logger.ts +87 -14
  545. package/utility/logging/logRotator.ts +72 -45
  546. package/utility/nodeIdentity.ts +45 -0
  547. package/utility/npmUtilities.ts +10 -8
  548. package/utility/operationPermissions.ts +18 -1
  549. package/utility/operation_authorization.ts +231 -42
  550. package/utility/watchPath.ts +63 -0
  551. package/utility/watcherFallback.ts +270 -0
  552. package/validation/configValidator.ts +209 -72
  553. package/validation/installValidator.ts +15 -0
  554. package/validation/validationWrapper.ts +18 -4
@@ -36,14 +36,24 @@ 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.ABANDONED_MARK_LOCK_TIMEOUT = exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
39
+ 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;
43
43
  exports.toRocksCompression = toRocksCompression;
44
44
  exports.getTables = getTables;
45
45
  exports.getDatabases = getDatabases;
46
+ exports.hydrateBranchRelationships = hydrateBranchRelationships;
46
47
  exports.readMetaDb = readMetaDb;
48
+ exports.resolveBranchPath = resolveBranchPath;
49
+ exports.quarantineBranchIdentity = quarantineBranchIdentity;
50
+ exports.assertBranchIdentityAvailable = assertBranchIdentityAvailable;
51
+ exports.reserveBranchIdentity = reserveBranchIdentity;
52
+ exports.retakeBranchIdentity = retakeBranchIdentity;
53
+ exports.releaseBranchIdentity = releaseBranchIdentity;
54
+ exports.isBranchIdentity = isBranchIdentity;
55
+ exports.openBranchDatabase = openBranchDatabase;
56
+ exports.closeBranchDatabases = closeBranchDatabases;
47
57
  exports.resetDatabases = resetDatabases;
48
58
  exports.resolveDatabaseStorageRoot = resolveDatabaseStorageRoot;
49
59
  exports.resolveDatabasePath = resolveDatabasePath;
@@ -52,10 +62,9 @@ exports.dropDatabase = dropDatabase;
52
62
  exports.closeDatabase = closeDatabase;
53
63
  exports.closeLoadedDatabases = closeLoadedDatabases;
54
64
  exports.table = table;
65
+ exports.scopedTableFactory = scopedTableFactory;
66
+ exports.reloadBranchAt = reloadBranchAt;
55
67
  exports.canonicalizeIndexOptions = canonicalizeIndexOptions;
56
- exports.setIndexingCheckpointPeriod = setIndexingCheckpointPeriod;
57
- exports.resumeStartKey = resumeStartKey;
58
- exports.tryAcquireUpdateAttributesLock = tryAcquireUpdateAttributesLock;
59
68
  exports.dropTableMeta = dropTableMeta;
60
69
  exports.onUpdatedTable = onUpdatedTable;
61
70
  exports.onRemovedTable = onRemovedTable;
@@ -63,7 +72,6 @@ exports.onRemovedDB = onRemovedDB;
63
72
  exports.getDefaultCompression = getDefaultCompression;
64
73
  exports.flushDatabases = flushDatabases;
65
74
  const node_events_1 = require("node:events");
66
- const node_crypto_1 = require("node:crypto");
67
75
  const environmentManager_ts_1 = require("../utility/environment/environmentManager.js");
68
76
  const terms_ts_1 = require("../utility/lmdb/terms.js");
69
77
  const lmdb_1 = require("lmdb");
@@ -85,9 +93,10 @@ const harper_logger_ts_1 = __importDefault(require("../utility/logging/harper_lo
85
93
  const { forComponent } = harper_logger_ts_1.default;
86
94
  const manageThreads = __importStar(require("../server/threads/manageThreads.js"));
87
95
  const auditStore_ts_1 = require("./auditStore.js");
88
- const storageReclamation_ts_1 = require("../server/storageReclamation.js");
89
96
  const RecordEncoder_ts_1 = require("./RecordEncoder.js");
90
97
  const blob_ts_1 = require("./blob.js");
98
+ const storageReclamation_ts_1 = require("../server/storageReclamation.js");
99
+ const common_validators_ts_1 = require("../validation/common_validators.js");
91
100
  const customIndexes_ts_1 = require("./indexes/customIndexes.js");
92
101
  const OpenDBIObject_ts_1 = require("../utility/lmdb/OpenDBIObject.js");
93
102
  const rocksdb_js_1 = require("@harperfast/rocksdb-js");
@@ -144,8 +153,60 @@ function markInternalDbiNonVersioned(dbisDb) {
144
153
  const logger = forComponent('storage');
145
154
  const DEFAULT_DATABASE_NAME = 'data';
146
155
  const DEFINED_TABLES = Symbol('defined-tables');
156
+ const CATALOG_RELATIONSHIP = Symbol('catalog-relationship');
147
157
  const DEFAULT_COMPRESSION_THRESHOLD = ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PAGESIZE) || 4096) - 60; // larger than this requires multiple pages
148
158
  (0, environmentManager_ts_1.initSync)();
159
+ let relationshipsToHydrate = [];
160
+ const reportedRelationshipErrors = new Set();
161
+ // an interrupted create is reported once per table and thread, not on every rescan
162
+ const reportedIncompleteCatalogs = new Set();
163
+ function normalizeRelationships(attributes) {
164
+ const relationships = [];
165
+ for (const attribute of attributes) {
166
+ const target = attribute.relationshipReference;
167
+ if (!attribute.relationship || !target)
168
+ continue;
169
+ const relationship = {};
170
+ if (typeof attribute.relationship.from === 'string')
171
+ relationship.from = attribute.relationship.from;
172
+ if (typeof attribute.relationship.to === 'string')
173
+ relationship.to = attribute.relationship.to;
174
+ // the GraphQL parser hands every directive argument over as a string, and the resolver reads
175
+ // filterMissing for truthiness, so persist what the resolver would see rather than the literal
176
+ if (attribute.relationship.filterMissing !== undefined)
177
+ relationship.filterMissing = Boolean(attribute.relationship.filterMissing);
178
+ if (!relationship.from && !relationship.to)
179
+ continue;
180
+ const definition = {
181
+ name: attribute.name,
182
+ type: attribute.type,
183
+ relationship,
184
+ target: { database: target.database, table: target.table },
185
+ };
186
+ if (attribute.type === 'array')
187
+ definition.elements = { type: attribute.elements?.type };
188
+ relationships.push(definition);
189
+ }
190
+ return relationships;
191
+ }
192
+ function relationshipEquals(left, right) {
193
+ return (left?.name === right?.name &&
194
+ left?.type === right?.type &&
195
+ left?.elements?.type === right?.elements?.type &&
196
+ left?.relationship?.from === right?.relationship?.from &&
197
+ left?.relationship?.to === right?.relationship?.to &&
198
+ left?.relationship?.filterMissing === right?.relationship?.filterMissing &&
199
+ left?.target?.database === right?.target?.database &&
200
+ left?.target?.table === right?.target?.table);
201
+ }
202
+ function relationshipListsEqual(left, right) {
203
+ if (!Array.isArray(left) || left.length !== right.length)
204
+ return false;
205
+ for (let index = 0; index < right.length; index++)
206
+ if (!relationshipEquals(left[index], right[index]))
207
+ return false;
208
+ return true;
209
+ }
149
210
  /**
150
211
  * The RocksDB block/blob codec for every column family this process opens (`storage.rocks.compression`),
151
212
  * or `undefined` to leave rocksdb-js on its own default (lz4 wherever the native build has it).
@@ -312,9 +373,12 @@ function openRocksDatabase(path, options) {
312
373
  else {
313
374
  db = new PrimaryRocksDatabase_ts_1.PrimaryRocksDatabase(path, options).open();
314
375
  // the RocksDB put and remove return promises, which masks thrown errors in non-awaiting calls to put/remove,
315
- // making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem
376
+ // making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem.
377
+ // The versioned remove is necessarily async and its callers must await or otherwise track its promise.
316
378
  db.put = db.putSync;
317
- db.remove = db.removeSync;
379
+ db.remove = ((id, removeOptions) => typeof removeOptions === 'number'
380
+ ? db.removeIfVersion(id, removeOptions)
381
+ : db.removeSync(id, removeOptions));
318
382
  db.encoder.name = options.name;
319
383
  }
320
384
  db.env = {};
@@ -326,22 +390,32 @@ const rocksdbDatabaseEnvs = new Map();
326
390
  (0, globals_js_1._assignPackageExport)('databases', exports.databases);
327
391
  (0, globals_js_1._assignPackageExport)('tables', exports.tables);
328
392
  const NEXT_TABLE_ID = Symbol.for('next-table-id');
329
- /**
330
- * True when a descriptor claims an index build no live operation in this process can own. The PID and
331
- * worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
332
- * generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
333
- * before the field existed, so it belongs to an earlier process; a thread started without one of its
334
- * own cannot judge, and falls back rather than declaring a live build dead.
335
- */
336
- function isAbandonedIndexBuild(descriptor, currentRestartGeneration) {
337
- if (!descriptor)
338
- return false;
339
- if (descriptor.indexingPID && descriptor.indexingPID !== process.pid)
340
- return true;
341
- if (descriptor.restartNumber < currentRestartGeneration)
342
- return true;
343
- const incarnation = manageThreads.processIncarnation;
344
- return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
393
+ // Restore every field used by `commonChanged`, plus `indexed` and `indexNulls`,
394
+ // from the durable descriptor. In particular, preserve `indexNulls: false` so
395
+ // an index that excludes nulls is not reopened as though it contains them.
396
+ const PEER_REDEFINABLE_FIELDS = [
397
+ 'type',
398
+ 'indexed',
399
+ 'indexNulls',
400
+ 'nullable',
401
+ 'enumerable',
402
+ 'version',
403
+ 'elements',
404
+ 'properties',
405
+ 'embed',
406
+ ];
407
+ // `indexNulls` is derived from the durable descriptor, never sent by a peer, so naming it in the
408
+ // discard warn would blame the peer for a field it did not write.
409
+ const PEER_DECLARABLE_FIELDS = PEER_REDEFINABLE_FIELDS.filter((field) => field !== 'indexNulls');
410
+ // A cluster-origin caller's list can predate a declaration another thread has already committed, so on
411
+ // that path the descriptor — not the caller — decides what the attribute is, in both directions.
412
+ function applyDurableDeclaration(attribute, descriptor) {
413
+ for (const field of PEER_REDEFINABLE_FIELDS) {
414
+ if (field in descriptor)
415
+ attribute[field] = descriptor[field];
416
+ else
417
+ delete attribute[field];
418
+ }
345
419
  }
346
420
  // How many times the schema load will try to finish a tombstoned drop before
347
421
  // giving up for the rest of this process's lifetime. A drop that fails once
@@ -416,6 +490,7 @@ function getDatabases() {
416
490
  }
417
491
  loadedDatabases = true;
418
492
  definedDatabases = new Map();
493
+ relationshipsToHydrate = [];
419
494
  const hdbBasePath = (0, environmentManager_ts_1.getHdbBasePath)();
420
495
  let databasePath = hdbBasePath && (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
421
496
  const schemaConfigs = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
@@ -440,10 +515,15 @@ function getDatabases() {
440
515
  // create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
441
516
  if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
442
517
  continue;
518
+ // branch directories are process-local derivatives, never databases in their own right
519
+ if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
520
+ continue;
443
521
  const dbName = (0, path_1.basename)(databaseEntry.name, '.mdb');
444
522
  const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
445
523
  if (blockedByRestore.has(dbName))
446
524
  continue;
525
+ if (isOpenBranchPath(dbPath))
526
+ continue;
447
527
  if (databaseEntry.isFile() &&
448
528
  (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
449
529
  !schemaConfigs[dbName]?.path) {
@@ -495,8 +575,12 @@ function getDatabases() {
495
575
  continue; // migration staging dir
496
576
  if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
497
577
  continue; // reserved restore-metadata dir
578
+ if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
579
+ continue; // reserved branch root
498
580
  if (blockedByRestore.has((0, path_1.basename)(databaseEntry.name, '.mdb')))
499
581
  continue;
582
+ if (isOpenBranchPath((0, path_1.join)(databasePath, databaseEntry.name)))
583
+ continue;
500
584
  if (databaseEntry.isFile() && (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
501
585
  readMetaDb((0, path_1.join)(databasePath, databaseEntry.name), (0, path_1.basename)(databaseEntry.name, '.mdb'), dbName);
502
586
  }
@@ -541,11 +625,15 @@ function getDatabases() {
541
625
  for (const tableName in tables) {
542
626
  if (!definedTables.has(tableName)) {
543
627
  logger.trace(`delete table class ${tableName}`);
628
+ tables[tableName]?.cleanup?.();
544
629
  delete tables[tableName];
545
630
  }
546
631
  }
547
632
  }
548
633
  else {
634
+ const removedTables = exports.databases[dbName];
635
+ for (const tableName in removedTables)
636
+ removedTables[tableName]?.cleanup?.();
549
637
  delete exports.databases[dbName];
550
638
  if (dbName === 'data') {
551
639
  for (const tableName in exports.tables) {
@@ -555,6 +643,7 @@ function getDatabases() {
555
643
  }
556
644
  }
557
645
  }
646
+ hydrateCatalogRelationships();
558
647
  if ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.ANALYTICS_REPLICATE) === false) {
559
648
  if (!exports.NON_REPLICATING_SYSTEM_TABLES.includes('hdb_analytics'))
560
649
  exports.NON_REPLICATING_SYSTEM_TABLES.push('hdb_analytics');
@@ -573,6 +662,148 @@ function getDatabases() {
573
662
  }
574
663
  return exports.databases;
575
664
  }
665
+ /**
666
+ * Hydrate one branch's relationships, resolving each target against the application's own branches
667
+ * first and only then against the real databases: a target the application also branched must be its
668
+ * branch's table, and a target it did not branch is legitimately the shared one.
669
+ */
670
+ function hydrateBranchRelationships(branch, branches) {
671
+ const resolveTarget = (target) => {
672
+ const targetBranch = branches.get(target.database);
673
+ // A branched target resolves ONLY within that branch. A durable branch is a checkpoint frozen
674
+ // at creation while the base keeps evolving, so falling through to the base for a table the
675
+ // branch's own copy lacks would point a branched application's relationship reads at live base
676
+ // data -- the fallback belongs to a database the application did not branch, never to one it did.
677
+ return targetBranch ? targetBranch.tables?.[target.table] : exports.databases[target.database]?.[target.table];
678
+ };
679
+ // Kept, not drained, like the global list: a target declared later (on this or another thread) is
680
+ // picked up by the next pass, and `hydrateTableRelationships` is a no-op once everything resolves.
681
+ for (const hydration of branch.pendingRelationships) {
682
+ try {
683
+ hydrateTableRelationships(hydration, resolveTarget, false);
684
+ }
685
+ catch (error) {
686
+ logger.error(`Unable to hydrate persisted relationships for branch table ${hydration.databaseName}.${hydration.tableName}`, error);
687
+ }
688
+ }
689
+ }
690
+ function hydrateCatalogRelationships() {
691
+ for (const hydration of relationshipsToHydrate) {
692
+ try {
693
+ hydrateTableRelationships(hydration);
694
+ }
695
+ catch (error) {
696
+ const key = `${hydration.databaseName}.${hydration.tableName}:hydrate`;
697
+ if (!reportedRelationshipErrors.has(key)) {
698
+ reportedRelationshipErrors.add(key);
699
+ logger.error(`Unable to hydrate persisted relationships for ${hydration.databaseName}.${hydration.tableName}`, error);
700
+ }
701
+ }
702
+ }
703
+ }
704
+ const resolveTargetGlobally = (target) => exports.databases[target.database]?.[target.table];
705
+ function hydrateTableRelationships({ table, databaseName, tableName, definitions }, resolveTarget = resolveTargetGlobally, announce = true) {
706
+ const hydratable = [];
707
+ for (let index = 0; index < definitions.length; index++) {
708
+ const definition = definitions[index];
709
+ // Keyed by name rather than list position, so a reordered list cannot inherit the previous
710
+ // occupant's reported state and swallow a different relationship's failure — and by reason, so
711
+ // hydrating one entry does not clear the report of a same-named invalid duplicate.
712
+ const errorKey = `${databaseName}.${tableName}:${definition?.name || `#${index}`}`;
713
+ if (!validRelationshipDefinition(definition, definitions, index)) {
714
+ reportRelationshipError(`${errorKey}:invalid`, `Ignoring invalid persisted relationship ${databaseName}.${tableName}[${index}]`);
715
+ continue;
716
+ }
717
+ // a live schema attribute of the same name owns the name; the catalog copy is only a stand-in
718
+ // for threads that never loaded the schema
719
+ if (table.attributes.some((attribute) => attribute.name === definition.name && !attribute[CATALOG_RELATIONSHIP]))
720
+ continue;
721
+ const targetTable = resolveTarget(definition.target);
722
+ if (!targetTable || !relationshipFieldsExist(table, targetTable, definition)) {
723
+ reportRelationshipError(`${errorKey}:unavailable`, `Unable to hydrate persisted relationship ${databaseName}.${tableName}.${definition.name}: target or foreign key is unavailable`);
724
+ continue;
725
+ }
726
+ reportedRelationshipErrors.delete(`${errorKey}:unavailable`);
727
+ hydratable.push({ definition, targetTable });
728
+ }
729
+ const installed = table.attributes.filter((attribute) => attribute[CATALOG_RELATIONSHIP]);
730
+ if (installed.length === hydratable.length &&
731
+ hydratable.every(({ definition, targetTable }, index) => relationshipEquals(installed[index], definition) &&
732
+ (installed[index].definition || installed[index].elements?.definition)?.tableClass === targetTable))
733
+ return;
734
+ const attributes = table.attributes.filter((attribute) => !attribute[CATALOG_RELATIONSHIP]);
735
+ for (const { definition, targetTable } of hydratable)
736
+ attributes.push(createCatalogRelationship(definition, targetTable));
737
+ table.attributes.splice(0, table.attributes.length, ...attributes);
738
+ table.schemaVersion++;
739
+ table.updatedAttributes();
740
+ if (announce)
741
+ exports.databaseEventsEmitter.emit('updateTable', table);
742
+ }
743
+ function validRelationshipDefinition(definition, definitions, index) {
744
+ if (!definition || typeof definition !== 'object')
745
+ return false;
746
+ const validName = (value) => typeof value === 'string' && value.length > 0 && !/[`/]/.test(value);
747
+ if (!validName(definition.name) || !validName(definition.type))
748
+ return false;
749
+ if (!validName(definition.target?.database) || !validName(definition.target?.table))
750
+ return false;
751
+ if (!definition.relationship || typeof definition.relationship !== 'object')
752
+ return false;
753
+ const { from, to, filterMissing } = definition.relationship;
754
+ if (from !== undefined && !validName(from))
755
+ return false;
756
+ if (to !== undefined && !validName(to))
757
+ return false;
758
+ if (!from && !to)
759
+ return false;
760
+ if (filterMissing !== undefined && typeof filterMissing !== 'boolean')
761
+ return false;
762
+ if (definition.type === 'array' ? !validName(definition.elements?.type) : definition.elements !== undefined)
763
+ return false;
764
+ for (let earlier = 0; earlier < index; earlier++)
765
+ if (definitions[earlier]?.name === definition.name)
766
+ return false;
767
+ return true;
768
+ }
769
+ function relationshipFieldsExist(sourceTable, targetTable, definition) {
770
+ if (definition.relationship.from &&
771
+ !sourceTable.attributes.some((attribute) => attribute.name === definition.relationship.from))
772
+ return false;
773
+ if (definition.relationship.to &&
774
+ !targetTable.attributes.some((attribute) => attribute.name === definition.relationship.to))
775
+ return false;
776
+ return true;
777
+ }
778
+ function createCatalogRelationship(definition, targetTable) {
779
+ const attribute = {
780
+ name: definition.name,
781
+ attribute: definition.name,
782
+ type: definition.type,
783
+ relationship: { ...definition.relationship },
784
+ target: { ...definition.target },
785
+ };
786
+ const targetDefinition = {
787
+ tableClass: targetTable,
788
+ type: targetTable.tableName,
789
+ attributes: targetTable.attributes,
790
+ };
791
+ if (definition.elements) {
792
+ attribute.elements = { type: definition.elements.type };
793
+ Object.defineProperty(attribute.elements, 'definition', { value: targetDefinition, configurable: true });
794
+ }
795
+ else {
796
+ Object.defineProperty(attribute, 'definition', { value: targetDefinition, configurable: true });
797
+ }
798
+ Object.defineProperty(attribute, CATALOG_RELATIONSHIP, { value: true });
799
+ return attribute;
800
+ }
801
+ function reportRelationshipError(key, message) {
802
+ if (reportedRelationshipErrors.has(key))
803
+ return;
804
+ reportedRelationshipErrors.add(key);
805
+ logger.error(message);
806
+ }
576
807
  /**
577
808
  * Scan a databases directory's entries for restore lock/marker files and return the names of
578
809
  * databases that must not be loaded: a held restore lock means a restore is in progress in some
@@ -612,15 +843,14 @@ function readMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, au
612
843
  rootStore = (0, lmdb_1.open)(envInit);
613
844
  lmdbDatabaseEnvs.set(path, rootStore);
614
845
  }
615
- rootStore.dbisDb?.resetReadTxn();
616
- return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
846
+ return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
617
847
  }
618
848
  catch (error) {
619
849
  error.message += ` opening database ${path}`;
620
850
  throw error;
621
851
  }
622
852
  }
623
- function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME) {
853
+ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, { destination, storeName, openedStores } = {}) {
624
854
  try {
625
855
  logger.trace(`loading rocksdb database: ${path}`);
626
856
  if (process.env.HARPER_PARENT_PROCESS_PID) {
@@ -631,14 +861,18 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
631
861
  }
632
862
  let rootStore = rocksdbDatabaseEnvs.get(path);
633
863
  if (rootStore) {
634
- initStores(path, rootStore, databaseName, defaultTable);
864
+ initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
635
865
  }
636
866
  else {
637
867
  rootStore = openRocksDatabase(path, { disableWAL: false, enableStats: true });
638
868
  rocksdbDatabaseEnvs.set(path, rootStore);
639
- initStores(path, rootStore, databaseName, defaultTable);
640
- // Skip transaction log replay in read-only mode
641
- if (!isReadOnlyMode()) {
869
+ initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
870
+ // A branch (`destination`) recovers its transaction-log tail in `openOrCreate`
871
+ // (branchDatabase.ts), not here: the branch claim elects exactly one replaying thread —
872
+ // applications load on workers, where this call would be a no-op — and awaits the replay
873
+ // before the branch is published to any reader. See the contract note on
874
+ // `openBranchDatabase` (harper#643).
875
+ if (!isReadOnlyMode() && !destination) {
642
876
  (0, replayLogs_ts_1.replayLogs)(rootStore, exports.databases[databaseName]);
643
877
  }
644
878
  }
@@ -649,7 +883,9 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
649
883
  throw error;
650
884
  }
651
885
  }
652
- function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy) {
886
+ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy, destination, storeName, openedStores } = {}) {
887
+ // a store with no tables never reaches the per-table loop below, and blob roots resolve from this
888
+ rootStore.databaseName = storeName ?? databaseName;
653
889
  const envInit = new OpenEnvironmentObject_ts_1.default(path, isReadOnlyMode());
654
890
  const internalDbiInit = createOpenDBIObject(false);
655
891
  let attributesDbi = rootStore.dbisDb;
@@ -664,6 +900,7 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
664
900
  else {
665
901
  attributesDbi = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
666
902
  }
903
+ openedStores?.push(attributesDbi);
667
904
  rootStore.dbisDb = markInternalDbiNonVersioned(attributesDbi);
668
905
  }
669
906
  let auditStore = rootStore.auditStore;
@@ -690,7 +927,9 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
690
927
  auditStore = (0, auditStore_ts_1.openAuditStore)(rootStore);
691
928
  }
692
929
  }
693
- const tables = ensureDB(databaseName);
930
+ const tables = destination ?? ensureDB(databaseName);
931
+ if (destination && !destination[DEFINED_TABLES])
932
+ destination[DEFINED_TABLES] = new Set();
694
933
  const definedTables = tables[DEFINED_TABLES];
695
934
  definedTables.rootStore = rootStore;
696
935
  const tablesToLoad = new Map();
@@ -789,10 +1028,20 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
789
1028
  }
790
1029
  }
791
1030
  if (!primaryAttribute) {
792
- logger.warn(`Unable to find a primary key attribute on table ${tableName}, with attributes: ${JSON.stringify(attributes)}`);
1031
+ const tableKey = `${databaseName}/${tableName}`;
1032
+ if (reportedIncompleteCatalogs.has(tableKey))
1033
+ logger.debug(`Skipping table ${databaseName}.${tableName}: still no primary key row`);
1034
+ else {
1035
+ reportedIncompleteCatalogs.add(tableKey);
1036
+ logger.warn(`Skipping table ${databaseName}.${tableName}: its catalog has attribute rows (${attributes.map((attribute) => attribute.name).join(', ')}) but no primary key row - a create in progress on another thread, or an interrupted one that re-running create_table repairs`);
1037
+ }
1038
+ // not defined until it loads, so the cleanup pass evicts a class left from a dropped same-name table
1039
+ definedTables?.delete(tableName);
793
1040
  continue;
794
1041
  }
795
1042
  }
1043
+ if (reportedIncompleteCatalogs.size)
1044
+ reportedIncompleteCatalogs.delete(`${databaseName}/${tableName}`);
796
1045
  // if the table has already been defined, use that class, don't create a new one
797
1046
  let table = tables[tableName];
798
1047
  // unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
@@ -838,13 +1087,13 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
838
1087
  // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
839
1088
  if (typeof primaryAttribute.randomAccessFields === 'boolean')
840
1089
  dbiInit.randomAccessStructure = primaryAttribute.randomAccessFields;
841
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
842
- primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true }), rootStore);
843
- }
844
- else {
845
- primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(rootStore.openDB(primaryAttribute.key, dbiInit), rootStore);
846
- }
847
- rootStore.databaseName = databaseName;
1090
+ // recorded before the wrapper below, which is the only thing between the native open and
1091
+ // the only list a failed open can release it from
1092
+ const opened = rootStore instanceof rocksdb_js_1.RocksDatabase
1093
+ ? openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true })
1094
+ : rootStore.openDB(primaryAttribute.key, dbiInit);
1095
+ openedStores?.push(opened);
1096
+ primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(opened, rootStore);
848
1097
  primaryStore.tableId = tableId;
849
1098
  }
850
1099
  let attributesUpdated;
@@ -855,11 +1104,10 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
855
1104
  if (!attribute.isPrimaryKey && (attribute.indexed || (attribute.attribute && !attribute.name))) {
856
1105
  if (!indices[attribute.name]) {
857
1106
  const dbi = openIndex(attribute.key, rootStore, attribute);
1107
+ openedStores?.push(dbi);
858
1108
  indices[attribute.name] = dbi;
859
1109
  indices[attribute.name].indexNulls = attribute.indexNulls;
860
1110
  }
861
- // the only way a thread that never declares the schema reaches Table.indices
862
- indices[attribute.name].isIndexing = !!attribute.indexingPID;
863
1111
  const existingAttribute = existingAttributes.find((existingAttribute) => existingAttribute.name === attribute.name);
864
1112
  if (existingAttribute)
865
1113
  existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1, attribute);
@@ -927,6 +1175,8 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
927
1175
  }
928
1176
  else {
929
1177
  table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
1178
+ // A branch builds into a caller-owned destination; its tables must refuse DDL.
1179
+ isBranch: Boolean(destination),
930
1180
  primaryStore,
931
1181
  auditStore,
932
1182
  audit,
@@ -948,11 +1198,385 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
948
1198
  dbisDB: attributesDbi,
949
1199
  }));
950
1200
  table.schemaVersion = 1;
951
- exports.databaseEventsEmitter.emit('updateTable', table);
1201
+ if (!destination)
1202
+ exports.databaseEventsEmitter.emit('updateTable', table);
1203
+ }
1204
+ if (Array.isArray(primaryAttribute.relationships)) {
1205
+ relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
1206
+ }
1207
+ else if (primaryAttribute.relationships !== undefined) {
1208
+ reportRelationshipError(`${databaseName}.${tableName}:list`, `Ignoring invalid persisted relationship list for ${databaseName}.${tableName}`);
1209
+ relationshipsToHydrate.push({ table, databaseName, tableName, definitions: [] });
952
1210
  }
953
1211
  }
954
1212
  return rootStore;
955
1213
  }
1214
+ /**
1215
+ * Branch directories live beside the base database's own storage root, never under the HDB root: a
1216
+ * database can be placed on its own volume, and `createCheckpoint` only hardlinks when source and
1217
+ * target share a filesystem — off-volume it degrades to a full byte copy, which is the property the
1218
+ * whole feature rests on.
1219
+ *
1220
+ * The backticks are what make the name reserved rather than merely conventional: `schemaRegex`
1221
+ * (validation/common_validators.ts) excludes 0x60, so no database can ever be created under this
1222
+ * name and shadow the branch root -- the same protection RESTORE_META_DIR uses.
1223
+ */
1224
+ exports.BRANCH_ROOT_DIR = '`branches`';
1225
+ /**
1226
+ * Where the branch of `baseName` belonging to `appName` lives. Derived only from those two names, so
1227
+ * every node in a cluster resolves the same application's branch to the same place — the identity an
1228
+ * application's data needs if it is to be addressed, and eventually replicated, cluster-wide.
1229
+ *
1230
+ * App and database are separate path segments: joining them (`<app>__<db>`) is not injective —
1231
+ * `(a__b, c)` and `(a, b__c)` collide — so two declarations could otherwise open one directory.
1232
+ */
1233
+ function resolveBranchPath(baseName, appName) {
1234
+ for (const [label, segment] of [
1235
+ ['application', appName],
1236
+ ['database', baseName],
1237
+ ]) {
1238
+ if (!segment || segment.includes('/') || segment.includes('\\') || segment === '.' || segment === '..') {
1239
+ throw new Error(`Invalid ${label} name for a branch path: ${JSON.stringify(segment)}`);
1240
+ }
1241
+ }
1242
+ return (0, path_1.join)(resolveDatabaseStorageRoot(baseName), exports.BRANCH_ROOT_DIR, appName, baseName);
1243
+ }
1244
+ /** `undefined` marks a path reserved by an open still in flight, which owns it just as firmly. */
1245
+ const openBranches = new Map();
1246
+ /** Store identities in use, so two branches cannot resolve one set of blob roots. */
1247
+ const openBranchIdentities = new Set();
1248
+ /**
1249
+ * Materialization renames its clone in from `<blobRoot>.staging`, so a branch owns two database
1250
+ * names rather than one: a database legally called `<storeName>.staging` resolves its own blob root
1251
+ * to exactly the path the clone removes and renames over. Every check, reservation and release
1252
+ * covers the pair, so the name cannot be claimed at any point where a branch operation may still
1253
+ * delete what it resolves to.
1254
+ */
1255
+ const BRANCH_STAGING_SUFFIX = '.staging';
1256
+ /**
1257
+ * Suffix of the sibling a branch is renamed to while being removed. A backtick, not a dot:
1258
+ * `schemaRegex` excludes 0x60, so no database can be named such that `<db>` + this suffix is another
1259
+ * branch's directory (with `.removing`, an application branching both `data` and `data.removing`, both
1260
+ * legal names, would destroy one by opening the other).
1261
+ */
1262
+ exports.BRANCH_REMOVING_SUFFIX = '`removing`';
1263
+ function branchIdentityPair(storeName) {
1264
+ return [storeName, storeName + BRANCH_STAGING_SUFFIX];
1265
+ }
1266
+ /**
1267
+ * Identities whose blob roots outlived the branch that owned them, because a removal or an abandoned
1268
+ * materialization could not delete them. A database created under such a name would resolve its own
1269
+ * fresh file ids onto files it never wrote, so the name stays refused -- but only against DATABASES.
1270
+ * The branch itself may take it back: materializing it replaces those roots wholesale, which is the
1271
+ * only route that clears the condition without an operator.
1272
+ */
1273
+ const quarantinedBranchIdentities = new Set();
1274
+ function quarantineBranchIdentity(storeName) {
1275
+ for (const name of branchIdentityPair(storeName)) {
1276
+ quarantinedBranchIdentities.add(name);
1277
+ openBranchIdentities.delete(name);
1278
+ }
1279
+ }
1280
+ /**
1281
+ * True when `dbPath` is a directory an open branch owns. The database scan opens any directory that
1282
+ * holds CURRENT + MANIFEST-*, and harper#643 places a branch inside the directory it walks, so
1283
+ * without this a rescan would rebuild the branch's tables into the global map, overwrite the store
1284
+ * identity its blob roots resolve from, and hand its store to `closeLoadedDatabases`.
1285
+ */
1286
+ function isOpenBranchPath(dbPath) {
1287
+ if (openBranches.size === 0)
1288
+ return false;
1289
+ // the literal path first: `rocksdbDatabaseEnvs` is keyed by it too, so a directory unlinked under
1290
+ // a live branch handle (realpathSync then throws) must not read as unowned
1291
+ if (openBranches.has(dbPath))
1292
+ return true;
1293
+ try {
1294
+ return openBranches.has((0, node_fs_1.realpathSync)(dbPath));
1295
+ }
1296
+ catch {
1297
+ return false;
1298
+ }
1299
+ }
1300
+ /**
1301
+ * A branch identity resolves its blob roots through `join(…, 'blobs', storeName)`, so it must be a
1302
+ * single path segment: `schemaRegex`, which every other database name is validated against, plus the
1303
+ * dot segments and backslash that regex permits but a path component must not be.
1304
+ */
1305
+ function assertLegalBranchName(name, description) {
1306
+ if (!name ||
1307
+ name.length > common_validators_ts_1.commonValidators.schema_length.maximum ||
1308
+ !common_validators_ts_1.schemaRegex.test(name) ||
1309
+ name.includes('\\') ||
1310
+ name === '.' ||
1311
+ name === '..') {
1312
+ throw new Error(`Cannot use '${name}' as a branch ${description}: it is not a legal database name`);
1313
+ }
1314
+ }
1315
+ /**
1316
+ * Refuse a branch store identity that something else already answers to.
1317
+ *
1318
+ * `storeName` picks the branch's blob roots, and blob file ids restart from each store's own counter,
1319
+ * so two holders of one identity write the same file paths and truncate each other. It must be
1320
+ * checked BEFORE anything destructive runs: materialization removes and replaces the blob root that
1321
+ * this name resolves to, and a real database may legally be called `5_myapp__data` -- `schemaRegex`
1322
+ * permits digits, `_` and `.`. The `.staging` sibling materialization writes is covered too, since a
1323
+ * database may legally carry that name as well.
1324
+ */
1325
+ function assertBranchIdentityAvailable(storeName) {
1326
+ // The on-disk scan, not just the in-memory maps: a database that exists on disk but has not been
1327
+ // loaded is absent from both, and it owns the blob root this identity would destroy.
1328
+ getDatabases();
1329
+ for (const name of branchIdentityPair(storeName)) {
1330
+ // The directory as well as the maps. `getDatabases` skips a database blocked by restore, so an
1331
+ // in-memory check alone reports its name as free while its blob root is very much real -- and
1332
+ // materialization would then remove and replace it.
1333
+ if (exports.databases[name] ||
1334
+ definedDatabases?.has(name) ||
1335
+ openBranchIdentities.has(name) ||
1336
+ (0, node_fs_1.existsSync)(resolveDatabasePath(name)) ||
1337
+ anotherBranchOwns(name, storeName)) {
1338
+ throw new Error(`Cannot use '${storeName}' as a branch store identity: '${name}' is already in use`);
1339
+ }
1340
+ }
1341
+ }
1342
+ /**
1343
+ * Does a branch OTHER than the one being opened already answer to this name on disk? `.staging` is
1344
+ * what makes the question two-sided: `<identity>.staging` is both the path a clone renames over and
1345
+ * a legal identity for a branch of a database literally named `<base>.staging`, so each of the pair
1346
+ * can belong to somebody else. Only the primary name read as itself is excluded -- that directory is
1347
+ * the very branch this call is opening.
1348
+ */
1349
+ function anotherBranchOwns(name, storeName) {
1350
+ if (name !== storeName)
1351
+ return branchDirectoryExistsFor(name);
1352
+ return name.endsWith(BRANCH_STAGING_SUFFIX)
1353
+ ? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
1354
+ : false;
1355
+ }
1356
+ /**
1357
+ * Claim the identity as well as checking it, so the window between the check and the branch actually
1358
+ * opening cannot be filled by a concurrent create or a second branch. `releaseBranchIdentity` hands
1359
+ * it back if materialization never gets as far as opening.
1360
+ */
1361
+ function reserveBranchIdentity(storeName) {
1362
+ assertBranchIdentityAvailable(storeName);
1363
+ retakeBranchIdentity(storeName);
1364
+ }
1365
+ /**
1366
+ * Take the pair back for an operation that owned it a statement ago -- cleanup, which has to keep
1367
+ * holding the names through the deletions its `close()` just released them for. Deliberately without
1368
+ * the availability check: nothing can have taken a name the caller held until now, and the check runs
1369
+ * the database scan, which at that exact moment would find the branch directory unowned.
1370
+ */
1371
+ function retakeBranchIdentity(storeName) {
1372
+ for (const name of branchIdentityPair(storeName)) {
1373
+ openBranchIdentities.add(name);
1374
+ quarantinedBranchIdentities.delete(name);
1375
+ }
1376
+ }
1377
+ function releaseBranchIdentity(storeName) {
1378
+ for (const name of branchIdentityPair(storeName))
1379
+ openBranchIdentities.delete(name);
1380
+ }
1381
+ /** Is this name spoken for by a branch? Database creation has to refuse it -- they share a blob root. */
1382
+ function isBranchIdentity(name) {
1383
+ if (openBranchIdentities.has(name) || quarantinedBranchIdentities.has(name))
1384
+ return true;
1385
+ // The in-memory set covers only branches open in THIS process, so after a restart -- or for an
1386
+ // application that is simply not loaded -- a database could take the name of an on-disk branch and
1387
+ // share its blob root. The staging sibling goes through the same route, because it names the path
1388
+ // materialization renames over -- but BOTH readings of a name ending in `.staging` have to be
1389
+ // tried: `schemaRegex` permits `.`, so `4_myapp__data.staging` is either the sibling of a branch of
1390
+ // `data` or a branch of a database actually called `data.staging`.
1391
+ if (branchDirectoryExistsFor(name))
1392
+ return true;
1393
+ return name.endsWith(BRANCH_STAGING_SUFFIX)
1394
+ ? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
1395
+ : false;
1396
+ }
1397
+ /**
1398
+ * Is there a branch directory answering to this store identity? The identity carries the application
1399
+ * name's length precisely so it can be taken apart again without guessing where the name ends.
1400
+ */
1401
+ function branchDirectoryExistsFor(storeName) {
1402
+ const prefix = /^(\d+)_/.exec(storeName);
1403
+ if (!prefix)
1404
+ return false;
1405
+ const appLength = Number(prefix[1]);
1406
+ const appName = storeName.slice(prefix[0].length, prefix[0].length + appLength);
1407
+ if (appName.length !== appLength ||
1408
+ storeName.slice(prefix[0].length + appLength, prefix[0].length + appLength + 2) !== '__')
1409
+ return false;
1410
+ const baseName = storeName.slice(prefix[0].length + appLength + 2);
1411
+ if (!baseName)
1412
+ return false;
1413
+ try {
1414
+ const branchPath = resolveBranchPath(baseName, appName);
1415
+ return (0, node_fs_1.existsSync)(branchPath) || (0, node_fs_1.existsSync)(branchPath + exports.BRANCH_REMOVING_SUFFIX);
1416
+ }
1417
+ catch {
1418
+ // Not a name a branch path could hold, so no branch owns it.
1419
+ return false;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Open a RocksDB directory as a **scope-private** database: its Table classes are built into an
1424
+ * object the caller owns and nothing is registered in the global `databases` map, so no enumerator
1425
+ * of that map — analytics, `describe_all`, worker teardown, replication — can observe it.
1426
+ *
1427
+ * `databaseName` is the *logical* name the application knows (`data`), so its schema and code need
1428
+ * no changes. `storeName` is the branch's own identity and is what `getRootBlobPathsForDB` resolves
1429
+ * blob directories from, which is how a branch gets its own blob roots rather than writing into the
1430
+ * base's.
1431
+ *
1432
+ * The caller owns the returned handle; the only thing that closes it on the caller's behalf is
1433
+ * `closeBranchDatabases`, run by an exiting job worker (via `closeLoadedDatabases`) and by an HTTP
1434
+ * worker's shutdown path, so a branch left open on an exiting worker does not linger in the
1435
+ * process-global RocksDB registry.
1436
+ *
1437
+ * Schema changes reach a branch only through its own bound factory (`scopedTableFactory`): a
1438
+ * declaration re-asserted against the branch's store. A branch's Table classes carry the base's
1439
+ * logical name, so the Table statics (`dropTable()`, `addAttributes()`) — which resolve the global
1440
+ * schema by that name and would act on the live base table — stay refused (`assertSchemaMutable`).
1441
+ *
1442
+ * A branch's blob roots are a hard-link clone of the base's, taken with the checkpoint, so a row
1443
+ * whose blob predates the branch reads back normally and the branch allocates new file ids in its own
1444
+ * directory (harper#644).
1445
+ *
1446
+ * A branch is the checkpoint's SST content plus its own transaction-log tail. This function opens
1447
+ * only the stores; replaying the tail is `openOrCreate`'s job (branchDatabase.ts), where the
1448
+ * cross-thread claim elects exactly one replayer and awaits it before any thread may open the
1449
+ * branch — the same recovery contract a base database gets at boot, without which a process that
1450
+ * died unflushed silently rewinds the branch to its last memtable flush (harper#643).
1451
+ *
1452
+ * Pass `blobRoots` to pin the handle to the roots the branch was published with; without it the
1453
+ * store resolves them from current configuration, which is only right for a branch being created.
1454
+ */
1455
+ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1456
+ assertLegalBranchName(databaseName, 'logical database name');
1457
+ assertLegalBranchName(storeName, 'store identity');
1458
+ if (!(0, node_fs_1.existsSync)(path))
1459
+ throw new Error(`Cannot open branch database: no directory at ${path}`);
1460
+ // the guards compare against env-map keys, so two spellings of one directory must not read as two
1461
+ path = (0, node_fs_1.realpathSync)(path);
1462
+ // FIRST: the guards below read the registry, and loading is itself what populates
1463
+ // `rocksdbDatabaseEnvs`. Claiming the path ahead of this scan would make the scan skip it, which
1464
+ // also means a directory that IS a real database no longer reads as one — so the pre-open window
1465
+ // where the scan can adopt a branch directory stays open, by choice (harper#643).
1466
+ getDatabases();
1467
+ // a rival graph over one shared root store; the two callers would disagree about who may close it
1468
+ if (openBranches.has(path))
1469
+ throw new Error(`Branch database at ${path} is already open`);
1470
+ // a loaded database's store is closed by `closeLoadedDatabases`, so adopting it would mean this
1471
+ // handle's `close()` tears down a live database
1472
+ if (rocksdbDatabaseEnvs.has(path))
1473
+ throw new Error(`Cannot branch ${path}: it is already open as a database`);
1474
+ assertBranchIdentityAvailable(storeName);
1475
+ const tables = Object.create(null);
1476
+ // initStores opens a table's column families well before `setTable` publishes it into `tables`,
1477
+ // so the graph is not a complete record of what a failed open must release
1478
+ const openedStores = [];
1479
+ // The boot-time hydration pass has already run by the time a branch opens, so anything this open
1480
+ // queues would never be drained. It is handed to the caller instead, which is the only place that
1481
+ // knows the application's other branches and can therefore resolve targets without leaking to base.
1482
+ const queuedRelationshipsAt = relationshipsToHydrate.length;
1483
+ let rootStore;
1484
+ // claim the path before the open, not after: readRocksMetaDb registers the store in
1485
+ // `rocksdbDatabaseEnvs` partway through, so anything re-entering `database()` during initStores
1486
+ // would otherwise find the branch's store on an unowned path
1487
+ openBranches.set(path, undefined);
1488
+ retakeBranchIdentity(storeName);
1489
+ try {
1490
+ rootStore = readRocksMetaDb(path, null, databaseName, { destination: tables, storeName, openedStores });
1491
+ // Pin the handle to the roots the caller proved this branch was published with, before it is
1492
+ // handed out. A row's `storageIndex` is a position in that list, so resolving through current
1493
+ // configuration instead would let an appended volume take writes at an index the branch's own
1494
+ // completion marker never recorded -- and a later change at that index would then silently
1495
+ // re-address them. `closeBranchHandles` clears the entry with the rest of the handle.
1496
+ if (blobRoots)
1497
+ blob_ts_1.databasePaths.set(rootStore, blobRoots);
1498
+ }
1499
+ catch (error) {
1500
+ openBranches.delete(path);
1501
+ releaseBranchIdentity(storeName);
1502
+ const stranded = rocksdbDatabaseEnvs.get(path);
1503
+ rocksdbDatabaseEnvs.delete(path);
1504
+ closeBranchHandles(path, stranded, openedStores, tables);
1505
+ throw error;
1506
+ }
1507
+ let closed = false;
1508
+ const branch = {
1509
+ tables,
1510
+ rootStore,
1511
+ path,
1512
+ databaseName,
1513
+ storeName,
1514
+ openedStores,
1515
+ pendingRelationships: relationshipsToHydrate.splice(queuedRelationshipsAt),
1516
+ close() {
1517
+ // guard on the handle, not on the registrations: those are keyed by path, and a closed
1518
+ // branch frees its path, so a stale handle would otherwise tear down its successor
1519
+ if (closed)
1520
+ return;
1521
+ closed = true;
1522
+ openBranches.delete(path);
1523
+ releaseBranchIdentity(storeName);
1524
+ rocksdbDatabaseEnvs.delete(path);
1525
+ closeBranchHandles(path, rootStore, openedStores, tables);
1526
+ },
1527
+ };
1528
+ openBranches.set(path, branch);
1529
+ return branch;
1530
+ }
1531
+ /**
1532
+ * Release everything a branch open created. Each table's primary store and each index is its own
1533
+ * column family, on top of the internal-dbis and audit families, so closing the root alone leaves
1534
+ * all of them behind — which is why `closeDatabase` walks them individually for a real database.
1535
+ * Two process-global registrations outlive the stores as well, neither with a lifetime of its own:
1536
+ * a storage-reclamation handler per store path, whose closure pins the now-closed store, and the
1537
+ * memoized blob roots in `databasePaths`. A real database is opened once per thread; harper#643
1538
+ * makes branch open/close routine, so both would grow with branch churn.
1539
+ */
1540
+ function closeBranchHandles(path, rootStore, openedStores = [], tables = {}) {
1541
+ const reclamationPaths = new Set([path]);
1542
+ rootStore?.auditStore?.stopAuditCleanup?.();
1543
+ const closeStore = (store, description) => {
1544
+ if (!store || store.status === 'closed')
1545
+ return;
1546
+ if (store.path)
1547
+ reclamationPaths.add(store.path);
1548
+ try {
1549
+ store.close?.();
1550
+ }
1551
+ catch (error) {
1552
+ logger.warn(`Error closing ${description} for branch database at ${path}`, error);
1553
+ }
1554
+ };
1555
+ // the class, before its stores: an expiration timer or a reclamation handler on a closed store
1556
+ // would otherwise keep firing against it for the life of the process
1557
+ for (const tableName in tables) {
1558
+ try {
1559
+ tables[tableName]?.cleanup?.();
1560
+ }
1561
+ catch (error) {
1562
+ logger.warn(`Error releasing table ${tableName} of branch database at ${path}`, error);
1563
+ }
1564
+ }
1565
+ for (const store of openedStores)
1566
+ closeStore(store, 'column family');
1567
+ closeStore(rootStore?.dbisDb, 'attributes store');
1568
+ closeStore(rootStore?.auditStore, 'audit store');
1569
+ closeStore(rootStore, 'root store');
1570
+ if (rootStore)
1571
+ blob_ts_1.databasePaths.delete(rootStore);
1572
+ for (const reclamationPath of reclamationPaths)
1573
+ (0, storageReclamation_ts_1.removeStorageReclamation)(reclamationPath);
1574
+ }
1575
+ /** Branches are process-local, so this is shutdown, not a data operation. */
1576
+ function closeBranchDatabases() {
1577
+ for (const branch of [...openBranches.values()])
1578
+ branch?.close();
1579
+ }
956
1580
  function resetDatabases() {
957
1581
  loadedDatabases = false;
958
1582
  for (const store of Object.values(lmdbDatabaseEnvs)) {
@@ -1063,6 +1687,14 @@ function database({ database: databaseName, table: tableName }) {
1063
1687
  const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
1064
1688
  if (useRocksdb) {
1065
1689
  const path = (0, path_1.join)(databasePath, tablePath ? tableName : databaseName);
1690
+ // the scan is not the only way to reach a branch's directory: a branch leaves its store in
1691
+ // `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
1692
+ // `definedDatabases` and the next `closeDatabase` would close it under the live handle
1693
+ if (isOpenBranchPath(path)) {
1694
+ const error = new Error(`Database '${databaseName}' is open as a scope-private branch`);
1695
+ error.statusCode = 409;
1696
+ throw error;
1697
+ }
1066
1698
  rootStore = rocksdbDatabaseEnvs.get(path);
1067
1699
  if (!rootStore || rootStore.status === 'closed') {
1068
1700
  // this on-demand open (create_table/create_database and friends) must not resurrect a
@@ -1290,8 +1922,12 @@ function closeDatabase(databaseName) {
1290
1922
  * database is closed). The `system` database is intentionally left open: it is non-enumerable here
1291
1923
  * (skipped by the loop), is never restored online, and the exiting worker may still touch the job
1292
1924
  * table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
1925
+ *
1926
+ * Branches are invisible to the loop below but hold handles from the same registry, so this — the
1927
+ * thread's one teardown entry point — closes them too.
1293
1928
  */
1294
1929
  function closeLoadedDatabases() {
1930
+ closeBranchDatabases();
1295
1931
  // snapshot the names first: closeDatabase() deletes from `databases` as it goes
1296
1932
  for (const databaseName of Object.keys(exports.databases)) {
1297
1933
  const dbTables = exports.databases[databaseName];
@@ -1394,24 +2030,38 @@ function openIndex(dbiKey, rootStore, attribute) {
1394
2030
  cache: isCustomObjectIndex,
1395
2031
  });
1396
2032
  dbi.rootStore = rootStore;
1397
- // Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
1398
- // transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
1399
- // Verification-Table cache can't track them. A versioned index initialises its encoder as a
1400
- // versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
1401
- // self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
1402
- // decode-free graph traversal. The format is resolved from the persisted attribute descriptor
1403
- // (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
1404
- if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
1405
- armVersionedIndexEncoder(dbi, rootStore);
2033
+ try {
2034
+ // Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
2035
+ // transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
2036
+ // Verification-Table cache can't track them. A versioned index initialises its encoder as a
2037
+ // versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
2038
+ // self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
2039
+ // decode-free graph traversal. The format is resolved from the persisted attribute descriptor
2040
+ // (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
2041
+ if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
2042
+ armVersionedIndexEncoder(dbi, rootStore);
2043
+ }
2044
+ installCustomIndex(dbi);
2045
+ }
2046
+ catch (error) {
2047
+ // the handle is not yet owned by any table, so nobody else can close it
2048
+ try {
2049
+ dbi.close();
2050
+ }
2051
+ catch { }
2052
+ throw error;
1406
2053
  }
1407
2054
  }
1408
2055
  else {
1409
2056
  dbi = rootStore.openDB(dbiKey, dbiInit);
2057
+ installCustomIndex(dbi);
1410
2058
  }
1411
- if (attribute.indexed.type) {
2059
+ function installCustomIndex(indexStore) {
2060
+ if (!attribute.indexed.type)
2061
+ return;
1412
2062
  const CustomIndex = customIndexes_ts_1.CUSTOM_INDEXES[attribute.indexed.type];
1413
2063
  if (CustomIndex) {
1414
- dbi.customIndex = new CustomIndex(dbi, attribute.indexed);
2064
+ indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
1415
2065
  }
1416
2066
  else {
1417
2067
  logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
@@ -1434,7 +2084,83 @@ function openIndex(dbiKey, rootStore, attribute) {
1434
2084
  * @param replicate
1435
2085
  */
1436
2086
  function table(tableDefinition) {
1437
- let { table: tableName, database: databaseName, expiration, eviction, scanInterval, attributes, audit, sealed, splitSegments, replicate, randomAccessFields, trackDeletes, schemaDefined, origin, description, properties, hidden, cacheControl, } = tableDefinition;
2087
+ return declareTable(GLOBAL_TARGET, tableDefinition);
2088
+ }
2089
+ const GLOBAL_TARGET = {
2090
+ rootStore: (databaseName, tableName) => database({ database: databaseName, table: tableName }),
2091
+ tables: (databaseName) => exports.databases[databaseName],
2092
+ reload: () => resetDatabases(),
2093
+ // a real database's stores live until the process (or `closeDatabase`, which walks the graph) ends
2094
+ adopt: () => { },
2095
+ };
2096
+ /**
2097
+ * The factory a branched application declares tables through: each declaration goes to the branch
2098
+ * 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.
2100
+ */
2101
+ function scopedTableFactory(branches) {
2102
+ if (!branches?.size)
2103
+ return table;
2104
+ return function scopedTable(tableDefinition) {
2105
+ // `||`, not `??`: `table()` resolves every falsy name to the default database
2106
+ const branch = branches.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
2107
+ return branch ? declareTable(branchTarget(branch), tableDefinition) : table(tableDefinition);
2108
+ };
2109
+ }
2110
+ function branchTarget(branch) {
2111
+ return {
2112
+ rootStore: () => branch.rootStore,
2113
+ tables: () => branch.tables,
2114
+ reload: () => reloadBranch(branch),
2115
+ adopt: (store) => branch.openedStores.push(store),
2116
+ branch,
2117
+ };
2118
+ }
2119
+ /**
2120
+ * Re-read a branch's catalog into its `tables`: tables and indexes another thread declared since the
2121
+ * open (or since the last reload) are opened here, the same way a schema-change rescan does for a
2122
+ * real database. Existing classes are kept and their attribute lists refreshed.
2123
+ */
2124
+ function reloadBranch(branch) {
2125
+ const { rootStore, tables, databaseName, storeName, openedStores } = branch;
2126
+ const queuedRelationshipsAt = relationshipsToHydrate.length;
2127
+ try {
2128
+ initStores(rootStore.path, rootStore, databaseName, { destination: tables, storeName, openedStores });
2129
+ }
2130
+ finally {
2131
+ for (const hydration of relationshipsToHydrate.splice(queuedRelationshipsAt))
2132
+ queueBranchHydration(branch, hydration);
2133
+ }
2134
+ // Until `prepareBranches` has the whole set, a cross-database target cannot be resolved without
2135
+ // falling through to the base; it hydrates the complete set once. After that, every sibling is
2136
+ // re-hydrated: the table this reload brought in may be the target a sibling's relationship waited for.
2137
+ if (!branch.relatedBranches)
2138
+ return;
2139
+ for (const sibling of branch.relatedBranches.values())
2140
+ hydrateBranchRelationships(sibling, branch.relatedBranches);
2141
+ }
2142
+ /** One pending hydration per table: a re-declaration replaces the entry the earlier declaration queued. */
2143
+ function queueBranchHydration(branch, hydration) {
2144
+ const existing = branch.pendingRelationships.findIndex((pending) => pending.databaseName === hydration.databaseName && pending.tableName === hydration.tableName);
2145
+ if (existing >= 0)
2146
+ branch.pendingRelationships[existing] = hydration;
2147
+ else
2148
+ branch.pendingRelationships.push(hydration);
2149
+ }
2150
+ /**
2151
+ * The receiving side of a branch's schema-change signal: a thread that holds this branch open reloads
2152
+ * it, any other thread has nothing to do. Returns the branch's tables so the caller can address the
2153
+ * table the signal named.
2154
+ */
2155
+ function reloadBranchAt(path) {
2156
+ const branch = openBranches.get(path);
2157
+ if (!branch)
2158
+ return undefined;
2159
+ reloadBranch(branch);
2160
+ return branch.tables;
2161
+ }
2162
+ 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;
1438
2164
  if (!databaseName)
1439
2165
  databaseName = DEFAULT_DATABASE_NAME;
1440
2166
  // Reject reserved names here too, not only at the operations API: a database
@@ -1447,8 +2173,14 @@ function table(tableDefinition) {
1447
2173
  if (hdbTerms_ts_1.RESERVED_DATABASE_NAMES.includes(databaseName)) {
1448
2174
  throw new hdbError_ts_1.ClientError(`'${databaseName}' is a reserved name and cannot be used as a database name`);
1449
2175
  }
1450
- const rootStore = database({ database: databaseName, table: tableName });
1451
- const tables = exports.databases[databaseName];
2176
+ // A branch resolves its blob root from its store identity, so a database created under that same
2177
+ // name would share the root: two allocators minting the same file paths and truncating each other,
2178
+ // and the branch's teardown removing the database's blobs.
2179
+ if (isBranchIdentity(databaseName)) {
2180
+ throw new hdbError_ts_1.ClientError(`'${databaseName}' is in use as a branch store identity and cannot be a database name`);
2181
+ }
2182
+ const rootStore = target.rootStore(databaseName, tableName);
2183
+ const tables = target.tables(databaseName);
1452
2184
  logger.trace(`Defining ${tableName} in ${databaseName}`);
1453
2185
  let Table = tables?.[tableName];
1454
2186
  if (rootStore.status === 'closed') {
@@ -1463,6 +2195,7 @@ function table(tableDefinition) {
1463
2195
  const schemaDefinedExplicit = tableDefinition.schemaDefined !== undefined;
1464
2196
  if (schemaDefined == undefined)
1465
2197
  schemaDefined = true;
2198
+ const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
1466
2199
  const internalDbiInit = createOpenDBIObject(false);
1467
2200
  for (const attribute of attributes) {
1468
2201
  if (attribute.attribute && !attribute.name) {
@@ -1476,124 +2209,161 @@ function table(tableDefinition) {
1476
2209
  attribute.indexed = true;
1477
2210
  }
1478
2211
  let hasChanges;
2212
+ let refreshRelationshipAttributes = false;
2213
+ let deferredPrimaryRow;
2214
+ let unpublishedPrimaryStore;
2215
+ let published = false;
1479
2216
  let releaseExclusiveLock;
1480
- if (Table) {
1481
- primaryKey = Table.primaryKey;
1482
- if (Table.primaryStore.rootStore.status === 'closed') {
1483
- throw new Error(`Can not use a closed data store from ${tableName} class`);
1484
- }
1485
- // Reject moving the primary key to a different attribute on a table that already has records.
1486
- // The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
1487
- // leave describe reporting the new attribute while every record — old and newly inserted — stays
1488
- // keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
1489
- // schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
1490
- // gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
1491
- // See HarperFast/studio#1199.
1492
- const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
1493
- if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
1494
- let hasRecords = false;
1495
- for (const _entry of Table.primaryStore.getRange({ start: true })) {
1496
- hasRecords = true;
1497
- break;
2217
+ const attributesToIndex = [];
2218
+ const indicesToRemove = [];
2219
+ try {
2220
+ if (Table) {
2221
+ primaryKey = Table.primaryKey;
2222
+ if (Table.primaryStore.rootStore.status === 'closed') {
2223
+ throw new Error(`Can not use a closed data store from ${tableName} class`);
1498
2224
  }
1499
- if (hasRecords) {
1500
- throw new hdbError_ts_1.ClientError(`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
1501
- `'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
1502
- `key, or migrate the existing records.`, 400);
1503
- }
1504
- }
1505
- // it table already exists, get the split segments setting
1506
- if (splitSegments == undefined)
1507
- splitSegments = Table.splitSegments;
1508
- Table.attributes.splice(0, Table.attributes.length, ...attributes);
1509
- // Re-assert from the live declaration so a stale value on disk (replicated event,
1510
- // v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
1511
- // callers that omit the flag (cluster schema-replication, data loader) don't flip a
1512
- // dynamic table to true via the default at the top of table().
1513
- if (schemaDefinedExplicit)
1514
- Table.schemaDefined = schemaDefined;
1515
- // Refresh class-level schema metadata to track docstring/directive changes across reloads.
1516
- Table.description = description;
1517
- Table.properties = properties;
1518
- Table.hidden = hidden;
1519
- // undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
1520
- if (cacheControl !== undefined)
1521
- Table.cacheControl = cacheControl;
1522
- }
1523
- else {
1524
- const auditStore = rootStore.auditStore;
1525
- primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
1526
- primaryKey = primaryKeyAttribute.name;
1527
- primaryKeyAttribute.isPrimaryKey = true;
1528
- primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
1529
- primaryKeyAttribute.schemaDefined = schemaDefined;
1530
- // can't change compression after the fact (except threshold), so save only when we create the table
1531
- primaryKeyAttribute.compression = getDefaultCompression();
1532
- if (trackDeletes)
1533
- primaryKeyAttribute.trackDeletes = true;
1534
- audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.LOGGING_AUDITLOG);
1535
- if (expiration)
1536
- primaryKeyAttribute.expiration = expiration;
1537
- if (eviction)
1538
- primaryKeyAttribute.eviction = eviction;
1539
- // persist cacheControl so all threads (and future boots) see it; undefined callers inherit
1540
- // a descriptor value carried by cluster schema events; null (schema has no directive)
1541
- // clears a stale value the carried descriptor may hold
1542
- if (cacheControl === undefined)
1543
- cacheControl = primaryKeyAttribute.cacheControl;
1544
- else if (cacheControl === null)
1545
- delete primaryKeyAttribute.cacheControl;
1546
- else
1547
- primaryKeyAttribute.cacheControl = cacheControl;
1548
- splitSegments ??= false;
1549
- primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
1550
- if (typeof sealed === 'boolean')
1551
- primaryKeyAttribute.sealed = sealed;
1552
- if (typeof replicate === 'boolean')
1553
- primaryKeyAttribute.replicate = replicate;
1554
- // An explicit directive PINS this table's encoding: we persist the boolean, so later changes
1555
- // to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
1556
- // directive are intentionally not persisted here — they follow the current global default on
1557
- // each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
1558
- // struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
1559
- // are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
1560
- if (typeof randomAccessFields === 'boolean')
1561
- primaryKeyAttribute.randomAccessFields = randomAccessFields;
1562
- if (origin) {
1563
- if (!primaryKeyAttribute.origins)
1564
- primaryKeyAttribute.origins = [origin];
1565
- else if (!primaryKeyAttribute.origins.includes(origin))
1566
- primaryKeyAttribute.origins.push(origin);
1567
- }
1568
- logger.trace(`${tableName} table loading, opening primary store`);
1569
- const dbiInit = createOpenDBIObject(false, true);
1570
- dbiInit.compression = primaryKeyAttribute.compression;
1571
- // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
1572
- if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
1573
- dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
1574
- const dbiName = tableName + '/';
1575
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1576
- attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
1577
- ...internalDbiInit,
1578
- disableWAL: false,
1579
- name: terms_ts_1.INTERNAL_DBIS_NAME,
1580
- });
2225
+ // Reject moving the primary key to a different attribute on a table that already has records.
2226
+ // The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
2227
+ // leave describe reporting the new attribute while every record — old and newly inserted — stays
2228
+ // keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
2229
+ // schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
2230
+ // gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
2231
+ // See HarperFast/studio#1199.
2232
+ const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
2233
+ if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
2234
+ let hasRecords = false;
2235
+ for (const _entry of Table.primaryStore.getRange({ start: true })) {
2236
+ hasRecords = true;
2237
+ break;
2238
+ }
2239
+ if (hasRecords) {
2240
+ throw new hdbError_ts_1.ClientError(`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
2241
+ `'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
2242
+ `key, or migrate the existing records.`, 400);
2243
+ }
2244
+ }
2245
+ // 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.
2249
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase)
2250
+ exclusiveLock();
2251
+ // it table already exists, get the split segments setting
2252
+ if (splitSegments == undefined)
2253
+ splitSegments = Table.splitSegments;
2254
+ if (origin === 'cluster') {
2255
+ const merged = Table.attributes.slice();
2256
+ for (const attribute of attributes) {
2257
+ const existing = merged.find((existingAttribute) => existingAttribute.name === attribute.name);
2258
+ if (!existing) {
2259
+ merged.push(attribute);
2260
+ continue;
2261
+ }
2262
+ // Nodes that apply the same peer definitions in a different order keep different index sets, and
2263
+ // this warn is the only signal of it. An absent field and an explicit falsy one declare the same
2264
+ // thing, so neither direction of that pair is a difference.
2265
+ const discarded = PEER_DECLARABLE_FIELDS.filter((field) => (attribute[field] || existing[field]) &&
2266
+ JSON.stringify(attribute[field]) !== JSON.stringify(existing[field]));
2267
+ if (discarded.length > 0)
2268
+ logger.warn(`Ignoring peer redefinition of ${databaseName}.${tableName}.${attribute.name} (${discarded
2269
+ .map((field) => `${field}: local ${JSON.stringify(existing[field])}, peer ${JSON.stringify(attribute[field])}`)
2270
+ .join('; ')}); the local schema is authoritative`);
2271
+ }
2272
+ attributes = merged;
2273
+ }
2274
+ Table.attributes.splice(0, Table.attributes.length, ...attributes);
2275
+ // Re-assert from the live declaration so a stale value on disk (replicated event,
2276
+ // v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
2277
+ // callers that omit the flag (cluster schema-replication, data loader) don't flip a
2278
+ // dynamic table to true via the default at the top of table(), and on origin so a
2279
+ // peer-derived definition never overrides the local declaration.
2280
+ if (schemaDefinedExplicit && origin !== 'cluster')
2281
+ Table.schemaDefined = schemaDefined;
2282
+ // Refresh class-level schema metadata to track docstring/directive changes across reloads.
2283
+ Table.description = description;
2284
+ Table.properties = properties;
2285
+ Table.hidden = hidden;
2286
+ // undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
2287
+ if (cacheControl !== undefined)
2288
+ Table.cacheControl = cacheControl;
1581
2289
  }
1582
2290
  else {
1583
- attributesDbi = rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
1584
- }
1585
- markInternalDbiNonVersioned(attributesDbi);
1586
- exclusiveLock(); // get an exclusive lock on the database so we can verify that we are the only thread creating the table (and assigning the table id)
1587
- const existingTableMeta = attributesDbi.getSync(dbiName);
1588
- if (existingTableMeta && !existingTableMeta.dropping) {
1589
- // table was created while we were setting up
1590
- if (releaseExclusiveLock)
1591
- releaseExclusiveLock();
1592
- resetDatabases();
1593
- return table(tableDefinition);
1594
- }
1595
- let primaryStore;
1596
- try {
2291
+ const auditStore = rootStore.auditStore;
2292
+ primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
2293
+ primaryKey = primaryKeyAttribute.name;
2294
+ primaryKeyAttribute.isPrimaryKey = true;
2295
+ primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
2296
+ primaryKeyAttribute.schemaDefined = schemaDefined;
2297
+ // Old readers treat every attribute row as live schema, so relationships stay on the ignored primary descriptor.
2298
+ if (relationshipDefinitions)
2299
+ primaryKeyAttribute.relationships = relationshipDefinitions;
2300
+ // can't change compression after the fact (except threshold), so save only when we create the table
2301
+ primaryKeyAttribute.compression = getDefaultCompression();
2302
+ if (trackDeletes)
2303
+ primaryKeyAttribute.trackDeletes = true;
2304
+ audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.LOGGING_AUDITLOG);
2305
+ if (expiration)
2306
+ primaryKeyAttribute.expiration = expiration;
2307
+ if (eviction)
2308
+ primaryKeyAttribute.eviction = eviction;
2309
+ // persist cacheControl so all threads (and future boots) see it; undefined callers inherit
2310
+ // a descriptor value carried by cluster schema events; null (schema has no directive)
2311
+ // clears a stale value the carried descriptor may hold
2312
+ if (cacheControl === undefined)
2313
+ cacheControl = primaryKeyAttribute.cacheControl;
2314
+ else if (cacheControl === null)
2315
+ delete primaryKeyAttribute.cacheControl;
2316
+ else
2317
+ primaryKeyAttribute.cacheControl = cacheControl;
2318
+ splitSegments ??= false;
2319
+ primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
2320
+ if (typeof sealed === 'boolean')
2321
+ primaryKeyAttribute.sealed = sealed;
2322
+ if (typeof replicate === 'boolean')
2323
+ primaryKeyAttribute.replicate = replicate;
2324
+ // An explicit directive PINS this table's encoding: we persist the boolean, so later changes
2325
+ // to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
2326
+ // directive are intentionally not persisted here — they follow the current global default on
2327
+ // each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
2328
+ // struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
2329
+ // are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
2330
+ if (typeof randomAccessFields === 'boolean')
2331
+ primaryKeyAttribute.randomAccessFields = randomAccessFields;
2332
+ if (origin) {
2333
+ if (!primaryKeyAttribute.origins)
2334
+ primaryKeyAttribute.origins = [origin];
2335
+ else if (!primaryKeyAttribute.origins.includes(origin))
2336
+ primaryKeyAttribute.origins.push(origin);
2337
+ }
2338
+ logger.trace(`${tableName} table loading, opening primary store`);
2339
+ const dbiInit = createOpenDBIObject(false, true);
2340
+ dbiInit.compression = primaryKeyAttribute.compression;
2341
+ // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
2342
+ if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
2343
+ dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
2344
+ const dbiName = tableName + '/';
2345
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2346
+ attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
2347
+ ...internalDbiInit,
2348
+ disableWAL: false,
2349
+ name: terms_ts_1.INTERNAL_DBIS_NAME,
2350
+ });
2351
+ }
2352
+ else {
2353
+ attributesDbi = rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
2354
+ }
2355
+ target.adopt(attributesDbi);
2356
+ markInternalDbiNonVersioned(attributesDbi);
2357
+ exclusiveLock(); // get an exclusive lock on the database so we can verify that we are the only thread creating the table (and assigning the table id)
2358
+ const existingTableMeta = attributesDbi.getSync(dbiName);
2359
+ if (existingTableMeta && !existingTableMeta.dropping) {
2360
+ // table was created while we were setting up; the lock is not reentrant, so release
2361
+ // before the recursive reload
2362
+ releaseLock();
2363
+ target.reload(databaseName);
2364
+ return declareTable(target, tableDefinition);
2365
+ }
2366
+ let primaryStore;
1597
2367
  if (existingTableMeta?.dropping) {
1598
2368
  // A previous drop of this table was interrupted after its tombstone
1599
2369
  // was written. Complete it now (under the exclusive lock) so the
@@ -1621,15 +2391,20 @@ function table(tableDefinition) {
1621
2391
  else {
1622
2392
  primaryStore = rootStore.openDB(dbiName, dbiInit);
1623
2393
  }
2394
+ target.adopt(primaryStore);
2395
+ unpublishedPrimaryStore = primaryStore;
1624
2396
  primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(primaryStore, rootStore);
1625
- rootStore.databaseName = databaseName;
2397
+ // only a store no table has loaded yet is unnamed; a branch's store carries its own store
2398
+ // identity here, which its blob roots resolve from, and must not take the logical name
2399
+ rootStore.databaseName ??= databaseName;
1626
2400
  primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
1627
2401
  logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
1628
2402
  if (!primaryStore.tableId)
1629
2403
  primaryStore.tableId = 1;
1630
2404
  attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
1631
2405
  primaryKeyAttribute.tableId = primaryStore.tableId;
1632
- Table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
2406
+ Table = (0, Table_ts_1.makeTable)({
2407
+ isBranch: Boolean(target.branch),
1633
2408
  primaryStore,
1634
2409
  auditStore,
1635
2410
  audit,
@@ -1652,96 +2427,99 @@ function table(tableDefinition) {
1652
2427
  properties,
1653
2428
  hidden,
1654
2429
  cacheControl,
1655
- }));
2430
+ });
1656
2431
  Table.schemaVersion = 1;
1657
2432
  hasChanges = true;
1658
- attributesDbi.put(dbiName, primaryKeyAttribute);
1659
- }
1660
- catch (error) {
1661
- // A failure while opening/creating the column family or writing the
1662
- // table id / catalog entry (e.g. into an env poisoned by a prior
1663
- // dangling column family) must NOT leak the exclusive
1664
- // 'update-attributes' spin lock. If it leaks, every subsequent
1665
- // create_table / attribute update on this database spins forever
1666
- // (a hard wedge that pins a worker at 100% CPU). Release before rethrow.
1667
- if (releaseExclusiveLock)
1668
- releaseExclusiveLock();
1669
- throw error;
2433
+ deferredPrimaryRow = primaryKeyAttribute;
1670
2434
  }
1671
- }
1672
- const indices = Table.indices;
1673
- if (!attributesDbi) {
1674
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1675
- rootStore.dbisDb = openRocksDatabase(rootStore.path, {
1676
- ...internalDbiInit,
1677
- disableWAL: false,
1678
- name: terms_ts_1.INTERNAL_DBIS_NAME,
1679
- });
1680
- }
1681
- else {
1682
- rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
1683
- }
1684
- attributesDbi = markInternalDbiNonVersioned(rootStore.dbisDb);
1685
- }
1686
- Table.dbisDB = attributesDbi;
1687
- const indicesToRemove = [];
1688
- for (const { key, value } of attributesDbi.getRange({ start: true })) {
1689
- if (value == null)
1690
- continue;
1691
- let [attributeTableName, attribute_name] = key.toString().split('/');
1692
- if (attribute_name === '')
1693
- attribute_name = value.name; // primary key
1694
- if (attribute_name) {
1695
- if (attributeTableName !== tableName)
2435
+ const indices = Table.indices;
2436
+ if (!attributesDbi) {
2437
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2438
+ rootStore.dbisDb = openRocksDatabase(rootStore.path, {
2439
+ ...internalDbiInit,
2440
+ disableWAL: false,
2441
+ name: terms_ts_1.INTERNAL_DBIS_NAME,
2442
+ });
2443
+ }
2444
+ else {
2445
+ rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
2446
+ }
2447
+ target.adopt(rootStore.dbisDb);
2448
+ attributesDbi = markInternalDbiNonVersioned(rootStore.dbisDb);
2449
+ }
2450
+ Table.dbisDB = attributesDbi;
2451
+ // A cluster-origin list can miss a descriptor another thread committed moments ago, so removal
2452
+ // reconciliation is reserved for local schema authoring; on a create the rows can only be aborted state.
2453
+ const reconcileRemovals = origin !== 'cluster' || Boolean(deferredPrimaryRow);
2454
+ for (const { key, value } of reconcileRemovals
2455
+ ? attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })
2456
+ : []) {
2457
+ if (value == null)
2458
+ continue;
2459
+ let [attributeTableName, attribute_name] = key.toString().split('/');
2460
+ if (attribute_name === '')
2461
+ attribute_name = value.name; // primary key
2462
+ if (attribute_name) {
2463
+ if (attributeTableName !== tableName)
2464
+ continue;
2465
+ }
2466
+ else {
2467
+ // table attribute for a table with no primary key, we don't want to remove this, so continue on
1696
2468
  continue;
1697
- }
1698
- else {
1699
- // table attribute for a table with no primary key, we don't want to remove this, so continue on
1700
- continue;
1701
- }
1702
- const attribute = attributes.find((attribute) => attribute.name === attribute_name);
1703
- const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
1704
- if (!attribute || removeIndex) {
1705
- exclusiveLock();
1706
- hasChanges = true;
1707
- if (!attribute)
1708
- attributesDbi.remove(key);
1709
- if (removeIndex) {
1710
- const indexDbi = Table.indices[attributeTableName];
1711
- if (indexDbi)
1712
- indicesToRemove.push(indexDbi);
2469
+ }
2470
+ const attribute = attributes.find((attribute) => attribute.name === attribute_name);
2471
+ const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
2472
+ // rows already present under a create are aborted state
2473
+ const staleRow = !attribute || Boolean(deferredPrimaryRow);
2474
+ if (staleRow || removeIndex) {
2475
+ exclusiveLock();
2476
+ hasChanges = true;
2477
+ if (staleRow)
2478
+ attributesDbi.remove(key);
2479
+ if (removeIndex) {
2480
+ const indexDbi = Table.indices[attributeTableName];
2481
+ if (indexDbi)
2482
+ indicesToRemove.push(indexDbi);
2483
+ }
1713
2484
  }
1714
2485
  }
1715
- }
1716
- const attributesToIndex = [];
1717
- try {
1718
2486
  // TODO: If we have attributes and the schemaDefined flag is not set, turn it on
1719
2487
  // iterate through the attributes to ensure that we have all the dbis created and indexed
1720
2488
  for (const attribute of attributes || []) {
1721
- if (attribute.relationship || attribute.computed) {
1722
- hasChanges = true; // need to update the table so the computed properties are translated to property resolvers
1723
- if (attribute.relationship)
1724
- continue;
2489
+ if (attribute.relationship) {
2490
+ refreshRelationshipAttributes = true;
2491
+ continue;
1725
2492
  }
2493
+ if (attribute.computed)
2494
+ hasChanges = true;
1726
2495
  let dbiKey = tableName + '/' + (attribute.name || '');
1727
2496
  Object.defineProperty(attribute, 'key', { value: dbiKey, configurable: true });
1728
2497
  let attributeDescriptor = attributesDbi.getSync(dbiKey);
1729
2498
  if (attribute.isPrimaryKey) {
2499
+ if (deferredPrimaryRow)
2500
+ continue;
1730
2501
  attributeDescriptor = attributeDescriptor || attributesDbi.getSync((dbiKey = tableName + '/')) || {};
1731
2502
  // Persist schemaDefined when the explicit live value disagrees with disk. Without this,
1732
2503
  // a stale `false` (from a v4-era write or replicated event) survives every reload: the
1733
2504
  // in-memory re-assert in the existing-Table branch only fixes the worker that ran @table,
1734
- // but other workers' next disk-load re-reads the stale value.
2505
+ // but other workers' next disk-load re-reads the stale value. The whole settings update is
2506
+ // gated off for cluster-origin callers: their values come from this worker's (possibly
2507
+ // stale) snapshot, so a rewrite could revert a newer local declaration already on disk.
1735
2508
  const schemaDefinedMismatch = schemaDefinedExplicit && attributeDescriptor.schemaDefined !== schemaDefined;
1736
2509
  // primary key can't change indexing, but settings can change
1737
- if (schemaDefinedMismatch ||
1738
- (audit !== undefined && audit !== Table.audit) ||
1739
- (sealed !== undefined && sealed !== Table.sealed) ||
1740
- (replicate !== undefined && replicate !== Table.replicate) ||
1741
- (+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
1742
- (+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
1743
- attribute.type !== attributeDescriptor.type) {
1744
- const updatedPrimaryAttribute = { ...attributeDescriptor };
2510
+ if (origin !== 'cluster' &&
2511
+ (schemaDefinedMismatch ||
2512
+ (audit !== undefined && audit !== Table.audit) ||
2513
+ (sealed !== undefined && sealed !== Table.sealed) ||
2514
+ (replicate !== undefined && replicate !== Table.replicate) ||
2515
+ (+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
2516
+ (+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
2517
+ attribute.type !== attributeDescriptor.type)) {
2518
+ exclusiveLock();
2519
+ const currentPrimaryAttribute = attributesDbi.getSync(dbiKey);
2520
+ if (!currentPrimaryAttribute || tableIsDropping(currentPrimaryAttribute, dbiKey))
2521
+ continue;
2522
+ const updatedPrimaryAttribute = { ...currentPrimaryAttribute };
1745
2523
  if (typeof audit === 'boolean') {
1746
2524
  if (audit)
1747
2525
  Table.enableAuditing();
@@ -1760,14 +2538,52 @@ function table(tableDefinition) {
1760
2538
  if (schemaDefinedMismatch)
1761
2539
  updatedPrimaryAttribute.schemaDefined = schemaDefined;
1762
2540
  hasChanges = true; // send out notification of the change
1763
- exclusiveLock();
1764
2541
  attributesDbi.put(dbiKey, updatedPrimaryAttribute);
1765
2542
  }
1766
2543
  continue;
1767
2544
  }
1768
- // note that non-indexed attributes do not need a dbi
1769
2545
  if (attributeDescriptor?.attribute && !attributeDescriptor.name)
1770
2546
  attributeDescriptor.indexed = true; // legacy descriptor
2547
+ if (origin === 'cluster' && attributeDescriptor) {
2548
+ // An existing descriptor is a local declaration this caller may not have seen yet, so it wins
2549
+ // over the incoming definition and is never written back from it.
2550
+ applyDurableDeclaration(attribute, attributeDescriptor);
2551
+ const abandonedIndexBuild = attribute.indexed &&
2552
+ (attributeDescriptor.indexingFailed ||
2553
+ (attributeDescriptor.indexingPID && attributeDescriptor.indexingPID !== process.pid) ||
2554
+ attributeDescriptor.restartNumber < (worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber));
2555
+ if (abandonedIndexBuild) {
2556
+ // Recovery is the exception to skipping the handling below, because without it `isIndexing`
2557
+ // stays pinned on with nothing left to clear it and every query on the attribute fails with
2558
+ // IndexRebuildingError for the life of the worker. It persists the attribute (here and again
2559
+ // from runIndexing), so restate the declaration from a descriptor read under the lock.
2560
+ exclusiveLock();
2561
+ applyDurableDeclaration(attribute, attributesDbi.getSync(dbiKey) ?? attributeDescriptor);
2562
+ }
2563
+ else {
2564
+ if (attribute.indexed) {
2565
+ const dbi = openIndex(dbiKey, rootStore, attribute);
2566
+ target.adopt(dbi);
2567
+ // Persisting the indexFormat openIndex just resolved adds a field the descriptor lacks
2568
+ // rather than rewriting one it has. Without it an empty index resolves 'versioned', writes
2569
+ // versioned nodes, then re-derives 'legacy' on the next load — see indexFormatNeedsPersist.
2570
+ if (attribute.indexFormat != null && attributeDescriptor.indexFormat == null) {
2571
+ exclusiveLock();
2572
+ const durableDescriptor = attributesDbi.getSync(dbiKey);
2573
+ if (durableDescriptor && durableDescriptor.indexFormat == null) {
2574
+ hasChanges = true;
2575
+ attributesDbi.put(dbiKey, { ...durableDescriptor, indexFormat: attribute.indexFormat });
2576
+ }
2577
+ }
2578
+ if (attributeDescriptor.indexingPID)
2579
+ dbi.isIndexing = true;
2580
+ dbi.indexNulls = attribute.indexNulls;
2581
+ indices[attribute.name] = dbi;
2582
+ }
2583
+ continue;
2584
+ }
2585
+ }
2586
+ // note that non-indexed attributes do not need a dbi
1771
2587
  // Some index options affect only search, not the stored structure (e.g. HNSW's
1772
2588
  // efConstructionSearch). Changing those should persist the new metadata but NOT trigger a
1773
2589
  // reindex. A custom index declares such keys via a static `searchOnlyOptions`.
@@ -1810,6 +2626,9 @@ function table(tableDefinition) {
1810
2626
  // on the main thread, where workerData is undefined (and it is initialized to 1).
1811
2627
  const currentRestartGeneration = worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber;
1812
2628
  const dbi = openIndex(dbiKey, rootStore, attribute);
2629
+ target.adopt(dbi);
2630
+ if (deferredPrimaryRow)
2631
+ indices[attribute.name] = dbi; // private until published; lets the rollback close it
1813
2632
  // openIndex resolves and stamps attribute.indexFormat for a versioned-capable (RocksDB
1814
2633
  // custom-object) index. An index created before this field existed has no indexFormat on
1815
2634
  // disk; persist the resolved value now — even when nothing else changed — so the format is
@@ -1821,13 +2640,15 @@ function table(tableDefinition) {
1821
2640
  if (changed ||
1822
2641
  indexFormatNeedsPersist ||
1823
2642
  attributeDescriptor?.indexingFailed ||
1824
- isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)) {
2643
+ (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
2644
+ attributeDescriptor?.restartNumber < currentRestartGeneration) {
1825
2645
  hasChanges = true;
1826
2646
  exclusiveLock();
1827
2647
  attributeDescriptor = attributesDbi.getSync(dbiKey);
1828
2648
  if (structurallyChanged ||
1829
2649
  attributeDescriptor?.indexingFailed ||
1830
- isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)) {
2650
+ (attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
2651
+ attributeDescriptor?.restartNumber < currentRestartGeneration) {
1831
2652
  hasChanges = true;
1832
2653
  if (attribute.indexNulls === undefined)
1833
2654
  attribute.indexNulls = true;
@@ -1846,17 +2667,9 @@ function table(tableDefinition) {
1846
2667
  // representation-only option difference — preserve the checkpoint so the backfill
1847
2668
  // resumes rather than restarts. Canonicalized to match structurallyChanged above.
1848
2669
  const indexOptionsChanged = canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
1849
- // Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
1850
- // lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
1851
- const uncertifiedCheckpoint = attributeDescriptor?.lastIndexedKey !== undefined &&
1852
- (attributeDescriptor.checkpointCertified === undefined ||
1853
- (0, lmdb_1.compareKeys)(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
1854
- attribute.lastIndexedKey =
1855
- indexOptionsChanged || uncertifiedCheckpoint
1856
- ? undefined
1857
- : (attributeDescriptor?.lastIndexedKey ?? undefined);
1858
- if (attribute.lastIndexedKey !== undefined)
1859
- attribute.checkpointCertified = attribute.lastIndexedKey;
2670
+ attribute.lastIndexedKey = indexOptionsChanged
2671
+ ? undefined
2672
+ : (attributeDescriptor?.lastIndexedKey ?? undefined);
1860
2673
  // Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
1861
2674
  // object store to the versioned, VT-cacheable format. A full rebuild from scratch
1862
2675
  // (lastIndexedKey === undefined) clears the store and rewrites every node, so the
@@ -1878,9 +2691,6 @@ function table(tableDefinition) {
1878
2691
  // the new process reuses the old PID. Cleared on clean completion; left in place
1879
2692
  // on failure/crash so the next, higher-numbered restart re-triggers the backfill.
1880
2693
  attribute.restartNumber = currentRestartGeneration;
1881
- if (manageThreads.processIncarnation != null)
1882
- attribute.indexingIncarnation = manageThreads.processIncarnation;
1883
- attribute.indexingBuildId = (0, node_crypto_1.randomBytes)(8).toString('hex');
1884
2694
  delete attribute.indexingFailed; // clear failure flag for the new run
1885
2695
  dbi.isIndexing = true;
1886
2696
  Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
@@ -1896,12 +2706,6 @@ function table(tableDefinition) {
1896
2706
  reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
1897
2707
  if (attributeDescriptor?.restartNumber < currentRestartGeneration)
1898
2708
  reindexReasons.push('restart-number');
1899
- if (uncertifiedCheckpoint)
1900
- reindexReasons.push('uncertified-checkpoint');
1901
- if (attributeDescriptor?.indexingPID === process.pid &&
1902
- manageThreads.processIncarnation != null &&
1903
- attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation)
1904
- reindexReasons.push('abandoned-build(previous process incarnation)');
1905
2709
  logger.info(`reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`);
1906
2710
  // we only set indexing nulls to true if new or reindexing, we can't have partial indexing of null
1907
2711
  attributesToIndex.push(attribute);
@@ -1914,13 +2718,9 @@ function table(tableDefinition) {
1914
2718
  // workers / a reload would treat the still-partial index as ready and return incomplete results.
1915
2719
  attribute.indexingPID = attributeDescriptor.indexingPID;
1916
2720
  attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
1917
- if (attributeDescriptor.checkpointCertified !== undefined)
1918
- attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
1919
2721
  // Carry the in-progress restart generation too, so persisting this metadata-only
1920
2722
  // change doesn't drop it and break the crash-recovery trigger for the running backfill.
1921
2723
  attribute.restartNumber = attributeDescriptor.restartNumber;
1922
- attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
1923
- attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
1924
2724
  if (attributeDescriptor.indexingFailed)
1925
2725
  attribute.indexingFailed = attributeDescriptor.indexingFailed;
1926
2726
  }
@@ -1943,26 +2743,61 @@ function table(tableDefinition) {
1943
2743
  attributesDbi.put(dbiKey, attribute);
1944
2744
  }
1945
2745
  }
2746
+ // The primary row is what makes a table loadable, so it lands last: a scan on another thread that
2747
+ // runs mid-create skips the table instead of building (and announcing) a partial one. It already
2748
+ // carries this table's relationships (set on primaryKeyAttribute above), so the persistence block
2749
+ // below is a no-op for a create — a table is never published with an incomplete relationship list.
2750
+ if (deferredPrimaryRow) {
2751
+ attributesDbi.put(tableName + '/', deferredPrimaryRow);
2752
+ // That write, not the registration below, is the publish point: it is durable from here
2753
+ // (on LMDB releaseLock()'s finally commits this create's write transaction even while an
2754
+ // error unwinds), so any later throw must leave the catalog alone. Rolling back past it
2755
+ // would delete the attribute rows out from under a live primary row and leave every
2756
+ // thread loading the primary-only schema this change exists to prevent.
2757
+ published = true;
2758
+ setTable(tables, tableName, Table);
2759
+ }
2760
+ // a table with no declared primary key has no attribute row to carry relationships, and the
2761
+ // loop above never visits its descriptor
2762
+ if (relationshipDefinitions) {
2763
+ const relationshipsKey = primaryDescriptorKey();
2764
+ if (!relationshipListsEqual(attributesDbi.getSync(relationshipsKey)?.relationships, relationshipDefinitions)) {
2765
+ exclusiveLock();
2766
+ const currentPrimaryAttribute = attributesDbi.getSync(relationshipsKey);
2767
+ // a missing row means a concurrent drop completed; writing one back would resurrect the table
2768
+ if (currentPrimaryAttribute &&
2769
+ !tableIsDropping(currentPrimaryAttribute, relationshipsKey) &&
2770
+ !relationshipListsEqual(currentPrimaryAttribute.relationships, relationshipDefinitions)) {
2771
+ attributesDbi.put(relationshipsKey, { ...currentPrimaryAttribute, relationships: relationshipDefinitions });
2772
+ hasChanges = true;
2773
+ }
2774
+ }
2775
+ }
2776
+ }
2777
+ catch (error) {
2778
+ if (unpublishedPrimaryStore && !published)
2779
+ discardUnpublishedTable();
2780
+ else if (published && tables[tableName] !== Table)
2781
+ discardUnregisteredClass();
2782
+ throw error;
1946
2783
  }
1947
2784
  finally {
1948
- if (releaseExclusiveLock)
1949
- releaseExclusiveLock();
2785
+ releaseLock();
1950
2786
  }
1951
- if (hasChanges) {
2787
+ if (hasChanges || refreshRelationshipAttributes) {
1952
2788
  Table.schemaVersion++;
1953
2789
  Table.updatedAttributes();
1954
2790
  }
1955
2791
  logger.trace(`${tableName} table loading, running index`);
2792
+ const branchPath = target.branch?.path;
1956
2793
  if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
1957
- // captured before the backfill can rewrite the attributes
1958
- const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
1959
- const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
1960
- Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
2794
+ Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath);
1961
2795
  }
1962
2796
  else if (hasChanges)
1963
- signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName));
2797
+ signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
1964
2798
  Table.origin = origin;
1965
- if (hasChanges) {
2799
+ // scope-private: replication and other global subscribers must not learn of a branch class
2800
+ if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
1966
2801
  exports.databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
1967
2802
  }
1968
2803
  if (expiration || eviction || scanInterval)
@@ -1973,15 +2808,68 @@ function table(tableDefinition) {
1973
2808
  });
1974
2809
  logger.trace(`${tableName} table loaded`);
1975
2810
  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.
2813
+ function tableIsDropping(descriptor, descriptorKey) {
2814
+ if (descriptor?.dropping)
2815
+ return true;
2816
+ return descriptorKey !== tableName + '/' && attributesDbi.getSync(tableName + '/')?.dropping;
2817
+ }
2818
+ // The catalog row initStores() reads a table's settings from: the primary key's own row when it
2819
+ // has one, and the bare table row otherwise.
2820
+ 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 + '/';
2828
+ }
2829
+ // The catalog of a published table stays, but a class the registration never accepted is
2830
+ // unreachable, so release what makeTable() registered process-wide instead of leaving its timers
2831
+ // and reclamation handler live for the process. The stores stay open: the table is durable, and
2832
+ // whichever scan reloads it opens its own handles.
2833
+ function discardUnregisteredClass() {
2834
+ try {
2835
+ Table.cleanup();
2836
+ }
2837
+ catch (discardError) {
2838
+ logger.warn(`Error releasing the unregistered class of ${databaseName}.${tableName}`, discardError);
2839
+ }
2840
+ }
2841
+ function discardUnpublishedTable() {
2842
+ const discard = (description, action) => {
2843
+ try {
2844
+ action();
2845
+ }
2846
+ catch (discardError) {
2847
+ logger.warn(`Error discarding ${description} of the failed create of ${databaseName}.${tableName}`, discardError);
2848
+ }
2849
+ };
2850
+ discard('catalog rows', () => {
2851
+ for (const attribute of attributes) {
2852
+ if (!attribute.isPrimaryKey && !attribute.relationship)
2853
+ attributesDbi.remove(tableName + '/' + attribute.name);
2854
+ }
2855
+ });
2856
+ if (Table)
2857
+ discard('callbacks', () => Table.cleanup());
2858
+ // an LMDB store is a per-environment handle slot shared with every thread and still inside this
2859
+ // create's write transaction; only RocksDB column-family handles hold native state to release
2860
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2861
+ for (const indexName in Table?.indices ?? {})
2862
+ discard(`index ${indexName}`, () => Table.indices[indexName].close());
2863
+ discard('primary store', () => unpublishedPrimaryStore.close());
2864
+ }
2865
+ }
1976
2866
  // Acquire an exclusive lock for attribute updates
1977
2867
  function exclusiveLock() {
1978
2868
  if (releaseExclusiveLock)
1979
2869
  return;
1980
2870
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1981
- while (!rootStore.tryLock('update-attributes')) { } // use a spin lock, we really need an synchronous exclusive lock here
1982
- releaseExclusiveLock = () => {
1983
- rootStore.unlock('update-attributes');
1984
- };
2871
+ (0, Table_ts_1.acquireUpdateAttributesLock)(rootStore, `table '${databaseName}.${tableName}'`);
2872
+ releaseExclusiveLock = () => (0, Table_ts_1.releaseUpdateAttributesLock)(rootStore);
1985
2873
  }
1986
2874
  else {
1987
2875
  // we only need an exclusive transaction lock in lmdb
@@ -1994,6 +2882,14 @@ function table(tableDefinition) {
1994
2882
  });
1995
2883
  }
1996
2884
  }
2885
+ // idempotent: the early release before the recursive reload and the finally both run, and a
2886
+ // second unlock could release another thread's lock
2887
+ function releaseLock() {
2888
+ const release = releaseExclusiveLock;
2889
+ releaseExclusiveLock = undefined;
2890
+ if (release)
2891
+ release();
2892
+ }
1997
2893
  }
1998
2894
  /**
1999
2895
  * Canonical form used ONLY for the structural (reindex-triggering) comparison of index options.
@@ -2031,163 +2927,28 @@ function canonicalizeIndexOptions(value) {
2031
2927
  }
2032
2928
  const MAX_OUTSTANDING_INDEXING = 1000;
2033
2929
  const MIN_OUTSTANDING_INDEXING = 10;
2034
- const INDEXING_YIELD_INTERVAL = 100;
2035
- // A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
2036
- // and never before this many more records: the flush seals every column family in the database, so a
2037
- // slow backfill must not impose the period's flush rate on unrelated tables.
2038
- let indexingCheckpointPeriodMs = 5000;
2039
- let indexingCheckpointMinRecords = 10000;
2040
- function setIndexingCheckpointPeriod(ms, minRecords = indexingCheckpointMinRecords) {
2041
- const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
2042
- indexingCheckpointPeriodMs = ms;
2043
- indexingCheckpointMinRecords = minRecords;
2044
- return previous;
2045
- }
2046
- const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
2047
- // RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
2048
- // entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
2049
- // never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
2050
- // shares — at most one in flight and one queued.
2051
- const indexingFlushes = new WeakMap();
2052
- function flushIndexStores(rootStore) {
2053
- if (!(rootStore instanceof rocksdb_js_1.RocksDatabase))
2054
- return;
2055
- let flushes = indexingFlushes.get(rootStore);
2056
- if (!flushes)
2057
- indexingFlushes.set(rootStore, (flushes = {}));
2058
- if (flushes.queued)
2059
- return flushes.queued;
2060
- const start = () => {
2061
- flushes.queued = undefined;
2062
- const flush = rootStore.flush().finally(() => {
2063
- if (flushes.inFlight === flush)
2064
- flushes.inFlight = undefined;
2065
- });
2066
- flushes.inFlight = flush;
2067
- return flush;
2068
- };
2069
- if (!flushes.inFlight)
2070
- return start();
2071
- return (flushes.queued = flushes.inFlight.then(start, start));
2072
- }
2073
- function resumeStartKey(attributes) {
2074
- let start;
2075
- for (const attribute of attributes) {
2076
- if (attribute.lastIndexedKey == undefined)
2077
- return undefined;
2078
- if (start === undefined || (0, lmdb_1.compareKeys)(attribute.lastIndexedKey, start) < 0)
2079
- start = attribute.lastIndexedKey;
2080
- }
2081
- return start;
2082
- }
2083
- // Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
2084
- // caller is waiting on the result and there is nothing useful to do without the lock. This one runs
2085
- // after a backfill has already settled, so a holder that never releases would wedge the worker's
2086
- // event loop for nothing. Giving up costs only the marker, and the next load of the table
2087
- // re-triggers the build regardless.
2088
- exports.ABANDONED_MARK_LOCK_TIMEOUT = 10000;
2089
- const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
2090
- // `timeout` is the test seam; production callers take the default.
2091
- function tryAcquireUpdateAttributesLock(rootStore, timeout = exports.ABANDONED_MARK_LOCK_TIMEOUT) {
2092
- if (rootStore.tryLock('update-attributes'))
2093
- return true;
2094
- const startTime = performance.now();
2095
- let waitTime = 1;
2096
- while (!rootStore.tryLock('update-attributes')) {
2097
- const elapsed = performance.now() - startTime;
2098
- if (elapsed >= timeout)
2099
- return false;
2100
- // Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
2101
- // blocks this thread either way, but sleeping does not burn a core while it does.
2102
- if (elapsed >= 2) {
2103
- Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
2104
- if (waitTime < 16)
2105
- waitTime *= 2;
2106
- }
2107
- }
2108
- return true;
2109
- }
2110
- /**
2111
- * Persists the failure marker for a build that ended without running one of runIndexing's own exit
2112
- * paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
2113
- * serialization boundary, because a replacement generation (or another thread declaring different index
2114
- * options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
2115
- * a live build. The fence read and write stay synchronous, and nothing here may throw because
2116
- * `Table.indexingOperation` reaches operations-API callers.
2117
- */
2118
- async function markAbandonedIndexBuild(Table, rootStore, buildIds) {
2119
- for (const [attribute, buildId] of buildIds) {
2120
- try {
2121
- let marked;
2122
- if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId)
2123
- continue;
2124
- const markIfOwned = () => {
2125
- const descriptor = Table.dbisDB.getSync(attribute.key);
2126
- if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
2127
- Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
2128
- marked = true;
2129
- }
2130
- };
2131
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2132
- if (!tryAcquireUpdateAttributesLock(rootStore)) {
2133
- logger.warn(`Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
2134
- `timed out after ${exports.ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
2135
- `The index stays incomplete and the next load of the table re-triggers the backfill.`);
2136
- continue;
2137
- }
2138
- try {
2139
- markIfOwned();
2140
- }
2141
- finally {
2142
- rootStore.unlock('update-attributes');
2143
- }
2144
- }
2145
- else {
2146
- rootStore.transactionSync(markIfOwned);
2147
- }
2148
- if (marked)
2149
- logger.warn(`Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
2150
- `The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
2151
- `the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`);
2152
- }
2153
- catch (error) {
2154
- // A store closed by shutdown is the common case, and it cannot be written to at all.
2155
- try {
2156
- logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
2157
- }
2158
- catch { }
2159
- }
2160
- }
2161
- }
2162
- async function runIndexing(Table, attributes, indicesToRemove) {
2163
- let checkpointing;
2164
- let hadIndexingErrors = false;
2165
- const attributeErrorReported = {};
2166
- const onIndexPutRejected = (property, error) => {
2167
- hadIndexingErrors = true;
2168
- if (attributeErrorReported[property])
2169
- return;
2170
- attributeErrorReported[property] = true;
2171
- logger.error(`Error indexing attribute ${property}`, error);
2172
- };
2173
- const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
2930
+ async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
2174
2931
  try {
2175
2932
  logger.info(`Indexing ${Table.tableName} attributes`, attributes);
2176
- await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName));
2933
+ await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
2177
2934
  let lastResolution;
2178
2935
  for (const index of indicesToRemove) {
2179
2936
  lastResolution = index.drop();
2180
- if (lastResolution?.then)
2181
- lastResolution.then(undefined, (error) => onIndexPutRejected(index.name, error));
2182
2937
  }
2183
2938
  let interrupted;
2939
+ let hadIndexingErrors = false;
2940
+ const attributeErrorReported = {};
2184
2941
  let indexed = 0;
2185
2942
  const attributesLength = attributes.length;
2186
2943
  await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
2187
2944
  if (attributesLength > 0) {
2188
- const start = resumeStartKey(attributes);
2189
- if (start === undefined) {
2190
- for (const attribute of attributes) {
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) {
2951
+ // if we are starting from the beginning, clear out any previous index entries since we are rewriting
2191
2952
  if (attribute.dbi.clearAsync) {
2192
2953
  // LMDB, note that we don't need to wait for this to complete, just gets enqueued in front of the other writes
2193
2954
  attribute.dbi.clearAsync();
@@ -2198,28 +2959,6 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2198
2959
  }
2199
2960
  }
2200
2961
  let outstanding = 0;
2201
- // A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
2202
- // durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
2203
- // record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
2204
- const persistCheckpoint = async (key) => {
2205
- if (hadIndexingErrors)
2206
- return;
2207
- try {
2208
- await flushIndexStores(Table.primaryStore.rootStore);
2209
- const puts = [];
2210
- for (const attribute of attributes) {
2211
- attribute.lastIndexedKey = key;
2212
- attribute.checkpointCertified = key;
2213
- puts.push(Table.dbisDB.put(attribute.key, attribute));
2214
- }
2215
- await Promise.all(puts);
2216
- }
2217
- catch (error) {
2218
- logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
2219
- }
2220
- };
2221
- let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
2222
- let nextCheckpointRecord = indexingCheckpointMinRecords;
2223
2962
  // this means that a new attribute has been introduced that needs to be indexed
2224
2963
  for (const { key, value: record } of Table.primaryStore.getRange({
2225
2964
  start,
@@ -2227,7 +2966,8 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2227
2966
  versions: true,
2228
2967
  snapshot: false, // don't hold a read transaction this whole time
2229
2968
  })) {
2230
- const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
2969
+ if (!record)
2970
+ continue; // deletion entry
2231
2971
  // TODO: Do we ever need to interrupt due to a schema change that was not a restart?
2232
2972
  //if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
2233
2973
  outstanding++;
@@ -2239,75 +2979,72 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2239
2979
  // we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
2240
2980
  // during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
2241
2981
  // performed indexing and we don't need to do anything further
2242
- if (record) {
2243
- for (let i = 0; i < attributesLength; i++) {
2244
- const attribute = attributes[i];
2245
- const property = attribute.name;
2246
- const index = attribute.dbi;
2247
- const onPutRejected = putRejectionHandlers[i];
2248
- try {
2249
- const resolver = attribute.resolve;
2250
- const value = record && (resolver ? resolver(record) : record[property]);
2251
- if (index.customIndex) {
2252
- index.customIndex.index(key, value);
2253
- didSynchronousIndexing = true;
2254
- continue;
2255
- }
2256
- const values = (0, commonUtility_ts_1.getIndexedValues)(value, index.indexNulls);
2257
- if (values) {
2258
- for (let i = 0, l = values.length; i < l; i++) {
2259
- lastResolution = index.put(values[i], key);
2260
- if (lastResolution?.then)
2261
- lastResolution.then(undefined, onPutRejected);
2262
- }
2263
- }
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;
2264
2993
  }
2265
- catch (error) {
2266
- hadIndexingErrors = true;
2267
- if (!attributeErrorReported[property]) {
2268
- // just report an indexing error once per attribute so we don't spam the logs.
2269
- // A store closed by worker shutdown surfaces here as "Database not open"; that is
2270
- // a benign interruption (the next generation re-runs the backfill), so don't log
2271
- // it as an error — the outer catch returns quietly once the iterator also throws.
2272
- attributeErrorReported[property] = true;
2273
- if (Table.primaryStore?.rootStore?.status === 'closed')
2274
- logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
2275
- else
2276
- logger.error(`Error indexing attribute ${property}`, error);
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);
2277
2998
  }
2278
2999
  }
2279
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);
3013
+ }
3014
+ }
2280
3015
  }
2281
- (0, when_ts_1.when)(lastResolution, () => outstanding--, () => outstanding--);
3016
+ (0, when_ts_1.when)(lastResolution, () => outstanding--, (error) => {
3017
+ outstanding--;
3018
+ hadIndexingErrors = true;
3019
+ logger.error(error);
3020
+ });
2282
3021
  if (worker_threads_1.workerData && worker_threads_1.workerData.restartNumber !== manageThreads.restartNumber) {
2283
3022
  interrupted = true;
2284
3023
  }
2285
- if (interrupted) {
2286
- try {
2287
- await lastResolution;
2288
- }
2289
- catch {
2290
- // already counted and logged by the rejection handler above
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);
2291
3029
  }
2292
- await checkpointing;
2293
- await persistCheckpoint(key);
2294
- return;
2295
- }
2296
- if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
2297
- nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
2298
- nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
2299
- await checkpointing;
2300
- checkpointing = (0, when_ts_1.when)(lastResolution, () => persistCheckpoint(key), () => { });
3030
+ if (interrupted)
3031
+ return;
2301
3032
  }
2302
3033
  if (outstanding > MAX_OUTSTANDING_INDEXING)
2303
3034
  await lastResolution;
2304
- if (atInterval || didSynchronousIndexing || outstanding > MIN_OUTSTANDING_INDEXING)
2305
- await yieldEventTurn();
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
2306
3039
  }
2307
3040
  }
2308
- await checkpointing;
2309
- // Await the last pending put. If it rejects, that is also an indexing error (already counted by
2310
- // onIndexPutRejected); catching it here keeps it from escaping to the outer catch.
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.
2311
3048
  try {
2312
3049
  await lastResolution;
2313
3050
  }
@@ -2319,17 +3056,6 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2319
3056
  // microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
2320
3057
  // before we decide whether to mark indexing as complete.
2321
3058
  await new Promise((resolve) => setImmediate(resolve));
2322
- // the tail since the last checkpoint is not durable until flushed; announcing the index complete
2323
- // before that would outlive a crash that loses it
2324
- if (!hadIndexingErrors) {
2325
- try {
2326
- await flushIndexStores(Table.primaryStore.rootStore);
2327
- }
2328
- catch (error) {
2329
- hadIndexingErrors = true;
2330
- logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
2331
- }
2332
- }
2333
3059
  if (hadIndexingErrors) {
2334
3060
  // Some records failed to index. Persist the failure marker in the descriptor so
2335
3061
  // the next call to table() (including after a restart with a fresh PID) re-triggers
@@ -2358,12 +3084,9 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2358
3084
  // update the attributes to indicate that we are finished
2359
3085
  for (const attribute of attributes) {
2360
3086
  delete attribute.lastIndexedKey;
2361
- delete attribute.checkpointCertified;
2362
3087
  delete attribute.indexingPID;
2363
3088
  delete attribute.indexingFailed;
2364
3089
  delete attribute.restartNumber;
2365
- delete attribute.indexingIncarnation;
2366
- delete attribute.indexingBuildId;
2367
3090
  attribute.dbi.isIndexing = false;
2368
3091
  // Also clear isIndexing on the currently-active dbi in Table.indices, which may
2369
3092
  // differ from attribute.dbi if a resetDatabases() call during this migration
@@ -2375,12 +3098,11 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2375
3098
  }
2376
3099
  await lastResolution;
2377
3100
  // now notify all the threads that we are done and the index is ready to use
2378
- await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName));
3101
+ await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName, undefined, branchPath));
2379
3102
  logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
2380
3103
  }
2381
3104
  }
2382
3105
  catch (error) {
2383
- await checkpointing;
2384
3106
  // A worker shutting down closes its stores mid-backfill, so the range iterator or a
2385
3107
  // put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
2386
3108
  // interruption, not a data error: the next worker generation re-runs the backfill via