@harperfast/harper 5.2.9 → 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 (668) 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 -0
  150. package/dist/resources/DatabaseTransaction.js +459 -45
  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 +29 -0
  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 -9
  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 +1073 -191
  174. package/dist/resources/Table.js.map +1 -1
  175. package/dist/resources/auditStore.d.ts +11 -2
  176. package/dist/resources/auditStore.js +325 -71
  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 -0
  187. package/dist/resources/databases.js +1200 -239
  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 -6
  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 +12 -0
  341. package/dist/server/storageReclamation.js +26 -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 -3
  347. package/dist/server/threads/manageThreads.js +517 -65
  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 +196 -194
  423. package/package.json +7 -6
  424. package/resources/DESIGN.md +114 -52
  425. package/resources/DatabaseTransaction.ts +521 -43
  426. package/resources/LMDBTransaction.ts +26 -4
  427. package/resources/PrimaryRocksDatabase.ts +28 -1
  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 -9
  433. package/resources/RocksTransactionLogStore.ts +78 -21
  434. package/resources/Table.ts +1110 -159
  435. package/resources/auditStore.ts +331 -73
  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 +1310 -252
  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 -7
  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 +23 -0
  498. package/server/threads/itc.js +11 -1
  499. package/server/threads/manageThreads.js +526 -56
  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-BnCBegQz.js +2267 -0
  505. package/studio/web/assets/FloatingChat-CoDW1ySS.js +23 -0
  506. package/studio/web/assets/{abnfDiagram-VCTEODGH-C0_BAZyO.js → abnfDiagram-VCTEODGH-B0BebmD2.js} +1 -1
  507. package/studio/web/assets/{alertDialog-DIHt7Z0r.js → alertDialog-CQyAJJhl.js} +1 -1
  508. package/studio/web/assets/{apiToken-c3Rd-w6g.js → apiToken-Bwk5BLXW.js} +1 -1
  509. package/studio/web/assets/applications-DHxGi7JH.js +296 -0
  510. package/studio/web/assets/architecture-7GRP2DOG-LB-MLAAb.js +1 -0
  511. package/studio/web/assets/{architectureDiagram-5GKGNRK7-BWzrASgm.js → architectureDiagram-5GKGNRK7-7SW3GD-K.js} +1 -1
  512. package/studio/web/assets/authStore-C3Nfubqr.js +3 -0
  513. package/studio/web/assets/{blockDiagram-NRAW4CY4-BdJX9Khj.js → blockDiagram-I7D4REHJ-BqguiadH.js} +2 -2
  514. package/studio/web/assets/{button-DhiX-njv.js → button-BIsUKRZq.js} +2 -2
  515. package/studio/web/assets/{c4Diagram-UCG6FXSJ-CI6MzGmQ.js → c4Diagram-7LVT6UL2-LBNf8t_X.js} +1 -1
  516. package/studio/web/assets/channel-yictG-U-.js +1 -0
  517. package/studio/web/assets/{chevron-up-Df2c8uoU.js → chevron-up-DtKGqDn3.js} +1 -1
  518. package/studio/web/assets/{chunk-TEH6E4GO-P87k5mNi.js → chunk-4HAMMTFA-DWtTut21.js} +1 -1
  519. package/studio/web/assets/{chunk-75Z2AOVW-BT8tVmks.js → chunk-75Z2AOVW-QGQD6th2.js} +1 -1
  520. package/studio/web/assets/{chunk-DU6HZSFF-9kAOOmI6.js → chunk-DU6HZSFF-Chq20Ba5.js} +1 -1
  521. package/studio/web/assets/{chunk-F27PBJKO-BW7ao8AY.js → chunk-F27PBJKO-BVA5EPhV.js} +1 -1
  522. package/studio/web/assets/{chunk-GMAD6QVW-BNyXpoQO.js → chunk-GMAD6QVW-BeS7S07A.js} +1 -1
  523. package/studio/web/assets/{chunk-OBVCFTLP-D4wWOqDQ.js → chunk-GVQU2GXP-sbwVIQ8i.js} +1 -1
  524. package/studio/web/assets/{chunk-G27WJ6UU-COyLMcgK.js → chunk-IMKFNOWR-Bnh3tAVd.js} +1 -1
  525. package/studio/web/assets/{chunk-JQ64N6SF-Cyz1IeLf.js → chunk-L3NEJ4N5-COfUyKII.js} +1 -1
  526. package/studio/web/assets/chunk-OSK3NFVY-ByciRftO.js +10 -0
  527. package/studio/web/assets/{chunk-P2QGCYS3-DmIFY4d7.js → chunk-P2QGCYS3-CP1VhG_c.js} +1 -1
  528. package/studio/web/assets/{chunk-POPQ4Y6H-BPrvMyKz.js → chunk-POPQ4Y6H-ClWhhkwW.js} +1 -1
  529. package/studio/web/assets/{chunk-PWAF6VOD-2zB6IW9i.js → chunk-PWAF6VOD-1z1THyS5.js} +1 -1
  530. package/studio/web/assets/{chunk-RHFEMEQ7-2FgyI8YU.js → chunk-SHT3W25Y-LpQkMsah.js} +2 -2
  531. package/studio/web/assets/{chunk-SVP7TREG-FwtbH2QC.js → chunk-SVP7TREG-jtdAHw0S.js} +1 -1
  532. package/studio/web/assets/{chunk-LCL6LL3I-HOzK_ppE.js → chunk-TICWLB2K-VOwzetX-.js} +1 -1
  533. package/studio/web/assets/classDiagram-ZZMXUADV-VaEwSy_g.js +1 -0
  534. package/studio/web/assets/classDiagram-v2-VYDZK3BY-VaEwSy_g.js +1 -0
  535. package/studio/web/assets/{createLucideIcon-BKGPfjm2.js → createLucideIcon-CzW9508A.js} +1 -1
  536. package/studio/web/assets/{cssMode-CEN2mzSA.js → cssMode-s0cWI-_M.js} +1 -1
  537. package/studio/web/assets/{cynefin-OW5HDTMX-BRkpLFQV.js → cynefin-OW5HDTMX-BbdbCvub.js} +1 -1
  538. package/studio/web/assets/{cynefinDiagram-5FMLGOSQ-CHT1DaX6.js → cynefinDiagram-5FMLGOSQ-TP-aIqbt.js} +1 -1
  539. package/studio/web/assets/{dagre-3AP2YEHR-DpUXBh63.js → dagre-GXQ25YYZ-DShnGpGo.js} +1 -1
  540. package/studio/web/assets/{diagram-S7CK7UJ4-BuymVFZT.js → diagram-S7CK7UJ4-aoCVTtcy.js} +1 -1
  541. package/studio/web/assets/{diagram-UQ7AKVKN-CyP148RM.js → diagram-UQ7AKVKN-DglXtQ6x.js} +1 -1
  542. package/studio/web/assets/{diagram-VSXAHHWV-CoCAg3M9.js → diagram-VSXAHHWV-fhEdmkwM.js} +1 -1
  543. package/studio/web/assets/{diagram-VX7I27RA-BpOqCFca.js → diagram-VX7I27RA-DccVJet6.js} +1 -1
  544. package/studio/web/assets/{diagram-Z3DM3KII-Bfpw7Vbj.js → diagram-Z3DM3KII-D-RyJJb7.js} +1 -1
  545. package/studio/web/assets/{dialog-CBf0Mr1d.js → dialog-Cn2uWgD4.js} +1 -1
  546. package/studio/web/assets/{dist-lkA3O3eM.js → dist-DP8UjMB_.js} +1 -1
  547. package/studio/web/assets/{download-BtTOBem-.js → download-B5T5r7ss.js} +1 -1
  548. package/studio/web/assets/{ebnfDiagram-PWID7BFC-DS_6aWqL.js → ebnfDiagram-PWID7BFC-DJGpIpz_.js} +1 -1
  549. package/studio/web/assets/{editor-D8oDeCTL.js → editor-DNcRHK54.js} +1 -1
  550. package/studio/web/assets/{erDiagram-SSCWMZ5O-DJNk6Fgw.js → erDiagram-RLTQ6QDP-CIfNlgkC.js} +1 -1
  551. package/studio/web/assets/eventmodeling-NTZA5JFV-CLxnp2CR.js +1 -0
  552. package/studio/web/assets/flowDiagram-HODETNUW-BIbhmz9f.js +1 -0
  553. package/studio/web/assets/{ganttDiagram-EL5Y4UJY-2pOExxMY.js → ganttDiagram-EL5Y4UJY-BxToTzzD.js} +1 -1
  554. package/studio/web/assets/{getAnalytics-D4LKGeVy.js → getAnalytics-GHK8ORfM.js} +1 -1
  555. package/studio/web/assets/{gitGraph-4MIJSDKK-CH5ZxwzF.js → gitGraph-4MIJSDKK-D2s2w8lE.js} +1 -1
  556. package/studio/web/assets/{gitGraphDiagram-WWUBYQGX-DVIsIhbO.js → gitGraphDiagram-WWUBYQGX-Dwntd4-x.js} +1 -1
  557. package/studio/web/assets/{html-u3vOg7LJ.js → html-Bdssedlg.js} +1 -1
  558. package/studio/web/assets/{htmlMode-DyO31v-P.js → htmlMode-CoDlJ3fw.js} +1 -1
  559. package/studio/web/assets/{index-Cxj2_wsl.css → index-7RMEgVG1.css} +1 -1
  560. package/studio/web/assets/index-D6sxmFLR.js +824 -0
  561. package/studio/web/assets/index.lazy-tmU5BS8s.js +2 -0
  562. package/studio/web/assets/{info-A6RAGUB7-CPQfTnaG.js → info-A6RAGUB7-DYjkvb0C.js} +1 -1
  563. package/studio/web/assets/{infoDiagram-RXCK75RN-DlwLYlwm.js → infoDiagram-27XIBGKW-Bnp1FJE5.js} +1 -1
  564. package/studio/web/assets/{ishikawaDiagram-5VMMS53U-BRXRp29U.js → ishikawaDiagram-5VMMS53U-D9Xh2r6X.js} +1 -1
  565. package/studio/web/assets/{javascript-CUvxOyTC.js → javascript-B8meVSTH.js} +1 -1
  566. package/studio/web/assets/{journeyDiagram-EYS64GPL-B0ou8k0n.js → journeyDiagram-3NMN7TZE-CokIi6ll.js} +2 -2
  567. package/studio/web/assets/{jsonMode-f_IwbF3D.js → jsonMode-DpIPd35T.js} +1 -1
  568. package/studio/web/assets/{kanban-definition-3QL26DDD-uYg7iYzp.js → kanban-definition-UXKFOSKX-CukSFJfX.js} +1 -1
  569. package/studio/web/assets/{languageServices-DXtZ6rEF.js → languageServices-C_5FMJzQ.js} +1 -1
  570. package/studio/web/assets/{lspLanguageFeatures-B4pCF1zO.js → lspLanguageFeatures-BIzNBzPK.js} +1 -1
  571. package/studio/web/assets/{mermaid-parser.core-Ck-fC8b7.js → mermaid-parser.core-BlEsOWNO.js} +3 -3
  572. package/studio/web/assets/{mermaid.core-CP8aNNYm.js → mermaid.core-BlkGaMIH.js} +5 -5
  573. package/studio/web/assets/{mindmap-definition-FBJOCRG2-CgTZ-rit.js → mindmap-definition-YA3MSWOX-IprMc_0j.js} +1 -1
  574. package/studio/web/assets/{notifications-D3tIQ4sg.js → notifications-CQf18QKb.js} +1 -1
  575. package/studio/web/assets/notifications-CvZivSbh.js +1 -0
  576. package/studio/web/assets/{packet-AYTQ26CC-DEyoPtPb.js → packet-AYTQ26CC-Bi3V04Zi.js} +1 -1
  577. package/studio/web/assets/{pegDiagram-XKGWAZYB-DrD-7sD9.js → pegDiagram-XKGWAZYB-BNuPDLZY.js} +1 -1
  578. package/studio/web/assets/{pie-WAS4IAKB-wjj-EI1d.js → pie-WAS4IAKB-_6DoDbng.js} +1 -1
  579. package/studio/web/assets/{pieDiagram-E7YTZNPT-GntqDCzv.js → pieDiagram-E7YTZNPT-DqNb6Ht2.js} +1 -1
  580. package/studio/web/assets/{profile-DZWU7MgT.js → profile-DdOwtntb.js} +1 -1
  581. package/studio/web/assets/{quadrantDiagram-AXDQQJYC-0UeqXQGd.js → quadrantDiagram-AXDQQJYC-BGH9E2YR.js} +1 -1
  582. package/studio/web/assets/{radar-RG4KPBEZ-DFSA5h7k.js → radar-RG4KPBEZ-DDdVczcL.js} +1 -1
  583. package/studio/web/assets/{railroad-74A4TZTK-CaOUG9wR.js → railroad-74A4TZTK-BJUP4Jds.js} +1 -1
  584. package/studio/web/assets/railroad-abnf-HS5TGJTU-Bm3L1L0h.js +1 -0
  585. package/studio/web/assets/railroad-ebnf-LZEXJU2U-CKzLGlkw.js +1 -0
  586. package/studio/web/assets/railroad-peg-WCYAUIDC-S8xLjslx.js +1 -0
  587. package/studio/web/assets/{railroadDiagram-O6MQD6OU-yHUELZaV.js → railroadDiagram-O6MQD6OU-DGTPh2KZ.js} +1 -1
  588. package/studio/web/assets/{regions-CkyurXzE.js → regions-n69fwagr.js} +1 -1
  589. package/studio/web/assets/{register-BUyhWjBO.js → register-PfWTCXWB.js} +3 -3
  590. package/studio/web/assets/{requirementDiagram-EFPCY7ZU-DNEGFjuW.js → requirementDiagram-BXWQKSXE-BJnO6uLz.js} +1 -1
  591. package/studio/web/assets/{sankeyDiagram-P5KCCOFB-DpyAmSVR.js → sankeyDiagram-P5KCCOFB-0vSOdymH.js} +1 -1
  592. package/studio/web/assets/{sequenceDiagram-WJ2MYXX4-TyaT7xNk.js → sequenceDiagram-WJ2MYXX4-hETizDWE.js} +1 -1
  593. package/studio/web/assets/{setComponentFile-CeyKSZAa.js → setComponentFile-Bg6O7X0S.js} +1 -1
  594. package/studio/web/assets/{setup-D2kn7cAA.js → setup-CUx_aUDl.js} +2 -2
  595. package/studio/web/assets/{stateDiagram-HBIQ2CUA-CeEdTArZ.js → stateDiagram-D77RDMKH-CdYQ_KtC.js} +1 -1
  596. package/studio/web/assets/stateDiagram-v2-MP3YSRHH-CdKuzQMT.js +1 -0
  597. package/studio/web/assets/status-D7BVKqX9.js +61 -0
  598. package/studio/web/assets/{swimlanes-XN3QIQJK-B54FmF46.js → swimlanes-42K2YHIH-B8cHIpU4.js} +1 -1
  599. package/studio/web/assets/swimlanesDiagram-VR7AAH4N-DmOSJwaH.js +8 -0
  600. package/studio/web/assets/{tabs-B_G5zscN.js → tabs-BrHu7gJi.js} +1 -1
  601. package/studio/web/assets/{timeline-definition-24CTP7MA-D-a9ujbo.js → timeline-definition-24CTP7MA-BJWYSXqF.js} +1 -1
  602. package/studio/web/assets/{toggleHighContrast-C0UW6rI2.js → toggleHighContrast-DBSyXzMr.js} +1 -1
  603. package/studio/web/assets/{treeView-Q6P3EWNA-CrW_6JnS.js → treeView-Q6P3EWNA-qxe_v6CQ.js} +1 -1
  604. package/studio/web/assets/{treemap-WGGIJYW6-BxyYLdP_.js → treemap-WGGIJYW6-dDo97XXF.js} +1 -1
  605. package/studio/web/assets/{tsMode-DBC0zmDx.js → tsMode-BByKCjBS.js} +1 -1
  606. package/studio/web/assets/{typescript-DApRQir3.js → typescript-DDLnLpw9.js} +1 -1
  607. package/studio/web/assets/{useEntityRestURL-DB6JStU1.js → useEntityRestURL-31CHGaHk.js} +1 -1
  608. package/studio/web/assets/{useLocalStorage-Dtj1QS8_.js → useLocalStorage-BqMR3D8_.js} +1 -1
  609. package/studio/web/assets/vendor-core-c2JRRJpV.js +58 -0
  610. package/studio/web/assets/vendor-datadog-CLUcJXOo.js +6 -0
  611. package/studio/web/assets/{vendor-react-Dyj4O3HE.js → vendor-react-CJV_K1u4.js} +1 -1
  612. package/studio/web/assets/vendor-tanstack-DxzraizX.js +1 -0
  613. package/studio/web/assets/{vendor-ui-vhu-UHhF.js → vendor-ui-BUjK0h8a.js} +2 -2
  614. package/studio/web/assets/{vennDiagram-4TSXK5OY-Cy7s7Mpy.js → vennDiagram-4TSXK5OY-A3i-lCdl.js} +1 -1
  615. package/studio/web/assets/{wardley-WFR3VGLG-BeBL35g2.js → wardley-WFR3VGLG-B0ik-_6g.js} +1 -1
  616. package/studio/web/assets/{wardleyDiagram-VM6X3IG4-BylmIGSg.js → wardleyDiagram-VM6X3IG4-CjrkKWUR.js} +1 -1
  617. package/studio/web/assets/{workers-C0bFIedw.js → workers-pR3jRY9D.js} +1 -1
  618. package/studio/web/assets/x-DIzaLEdK.js +1 -0
  619. package/studio/web/assets/{xml-HWd01lU-.js → xml-2iRnMhQO.js} +1 -1
  620. package/studio/web/assets/{xychartDiagram-S5SC5T6Z-CoKALMXr.js → xychartDiagram-S5SC5T6Z-Biok4GYV.js} +1 -1
  621. package/studio/web/assets/{yaml-CIH0Nt-h.js → yaml-Bf92gJpd.js} +1 -1
  622. package/studio/web/index.html +14 -14
  623. package/upgrade/directives/5-3-0.ts +132 -0
  624. package/upgrade/directives/directivesController.ts +2 -1
  625. package/utility/OperationFunctionCaller.ts +2 -1
  626. package/utility/common_utils.ts +30 -5
  627. package/utility/componentNames.ts +12 -0
  628. package/utility/environment/environmentManager.ts +3 -7
  629. package/utility/environment/systemInformation.ts +7 -0
  630. package/utility/errors/commonErrors.ts +4 -0
  631. package/utility/errors/hdbError.ts +29 -0
  632. package/utility/hdbTerms.ts +17 -0
  633. package/utility/logging/harper_logger.ts +87 -14
  634. package/utility/logging/logRotator.ts +72 -45
  635. package/utility/nodeIdentity.ts +45 -0
  636. package/utility/npmUtilities.ts +10 -8
  637. package/utility/operationPermissions.ts +18 -1
  638. package/utility/operation_authorization.ts +231 -42
  639. package/utility/watchPath.ts +63 -0
  640. package/utility/watcherFallback.ts +270 -0
  641. package/validation/configValidator.ts +209 -72
  642. package/validation/installValidator.ts +15 -0
  643. package/validation/validationWrapper.ts +18 -4
  644. package/studio/web/assets/Chat-JpO8EtUu.js +0 -2067
  645. package/studio/web/assets/FloatingChat-Bcj3xSZu.js +0 -23
  646. package/studio/web/assets/applications-ByqLRKyZ.js +0 -296
  647. package/studio/web/assets/architecture-7GRP2DOG-DNdx5tEU.js +0 -1
  648. package/studio/web/assets/authStore-qKmCZcaf.js +0 -3
  649. package/studio/web/assets/channel-DtCV8PTL.js +0 -1
  650. package/studio/web/assets/chunk-R7TYR2AO-Irip67yr.js +0 -10
  651. package/studio/web/assets/classDiagram-DTDB5LWJ-DbO_dCNE.js +0 -1
  652. package/studio/web/assets/classDiagram-v2-JRS7N3AN-DbO_dCNE.js +0 -1
  653. package/studio/web/assets/eventmodeling-NTZA5JFV-5jbe4A5P.js +0 -1
  654. package/studio/web/assets/flowDiagram-A5DVABFB-Dp9Ezlow.js +0 -1
  655. package/studio/web/assets/index-aSt5tY-L.js +0 -824
  656. package/studio/web/assets/index.lazy-B9jiPwT8.js +0 -2
  657. package/studio/web/assets/notifications-CUoYgU98.js +0 -1
  658. package/studio/web/assets/railroad-abnf-HS5TGJTU-Bc0Qi0WH.js +0 -1
  659. package/studio/web/assets/railroad-ebnf-LZEXJU2U-G8rVVZ2C.js +0 -1
  660. package/studio/web/assets/railroad-peg-WCYAUIDC-CrehKBhC.js +0 -1
  661. package/studio/web/assets/stateDiagram-v2-4QOOHH4V-D4tuw9Su.js +0 -1
  662. package/studio/web/assets/status-D7Xn5ePA.js +0 -61
  663. package/studio/web/assets/swimlanesDiagram-VK2B7HYN-XOhmNEvq.js +0 -8
  664. package/studio/web/assets/vendor-core-RCcadM3e.js +0 -73
  665. package/studio/web/assets/vendor-datadog-BRv-mOv1.js +0 -6
  666. package/studio/web/assets/vendor-tanstack-BiFWSB3W.js +0 -1
  667. package/studio/web/assets/x-B9o9hsep.js +0 -1
  668. /package/studio/web/assets/{sizeCapture-X5ZJPWSS-B0uUizjq.js → sizeCapture-INFHLROL-B0uUizjq.js} +0 -0
@@ -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.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,6 +62,8 @@ 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
68
  exports.dropTableMeta = dropTableMeta;
57
69
  exports.onUpdatedTable = onUpdatedTable;
@@ -83,6 +95,8 @@ const manageThreads = __importStar(require("../server/threads/manageThreads.js")
83
95
  const auditStore_ts_1 = require("./auditStore.js");
84
96
  const RecordEncoder_ts_1 = require("./RecordEncoder.js");
85
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");
86
100
  const customIndexes_ts_1 = require("./indexes/customIndexes.js");
87
101
  const OpenDBIObject_ts_1 = require("../utility/lmdb/OpenDBIObject.js");
88
102
  const rocksdb_js_1 = require("@harperfast/rocksdb-js");
@@ -139,8 +153,60 @@ function markInternalDbiNonVersioned(dbisDb) {
139
153
  const logger = forComponent('storage');
140
154
  const DEFAULT_DATABASE_NAME = 'data';
141
155
  const DEFINED_TABLES = Symbol('defined-tables');
156
+ const CATALOG_RELATIONSHIP = Symbol('catalog-relationship');
142
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
143
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
+ }
144
210
  /**
145
211
  * The RocksDB block/blob codec for every column family this process opens (`storage.rocks.compression`),
146
212
  * or `undefined` to leave rocksdb-js on its own default (lz4 wherever the native build has it).
@@ -307,9 +373,12 @@ function openRocksDatabase(path, options) {
307
373
  else {
308
374
  db = new PrimaryRocksDatabase_ts_1.PrimaryRocksDatabase(path, options).open();
309
375
  // the RocksDB put and remove return promises, which masks thrown errors in non-awaiting calls to put/remove,
310
- // 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.
311
378
  db.put = db.putSync;
312
- db.remove = db.removeSync;
379
+ db.remove = ((id, removeOptions) => typeof removeOptions === 'number'
380
+ ? db.removeIfVersion(id, removeOptions)
381
+ : db.removeSync(id, removeOptions));
313
382
  db.encoder.name = options.name;
314
383
  }
315
384
  db.env = {};
@@ -321,6 +390,33 @@ const rocksdbDatabaseEnvs = new Map();
321
390
  (0, globals_js_1._assignPackageExport)('databases', exports.databases);
322
391
  (0, globals_js_1._assignPackageExport)('tables', exports.tables);
323
392
  const NEXT_TABLE_ID = Symbol.for('next-table-id');
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
+ }
419
+ }
324
420
  // How many times the schema load will try to finish a tombstoned drop before
325
421
  // giving up for the rest of this process's lifetime. A drop that fails once
326
422
  // almost always fails identically forever - the usual cause is a RocksDB
@@ -394,6 +490,7 @@ function getDatabases() {
394
490
  }
395
491
  loadedDatabases = true;
396
492
  definedDatabases = new Map();
493
+ relationshipsToHydrate = [];
397
494
  const hdbBasePath = (0, environmentManager_ts_1.getHdbBasePath)();
398
495
  let databasePath = hdbBasePath && (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
399
496
  const schemaConfigs = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
@@ -418,10 +515,15 @@ function getDatabases() {
418
515
  // create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
419
516
  if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
420
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;
421
521
  const dbName = (0, path_1.basename)(databaseEntry.name, '.mdb');
422
522
  const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
423
523
  if (blockedByRestore.has(dbName))
424
524
  continue;
525
+ if (isOpenBranchPath(dbPath))
526
+ continue;
425
527
  if (databaseEntry.isFile() &&
426
528
  (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
427
529
  !schemaConfigs[dbName]?.path) {
@@ -473,8 +575,12 @@ function getDatabases() {
473
575
  continue; // migration staging dir
474
576
  if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
475
577
  continue; // reserved restore-metadata dir
578
+ if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
579
+ continue; // reserved branch root
476
580
  if (blockedByRestore.has((0, path_1.basename)(databaseEntry.name, '.mdb')))
477
581
  continue;
582
+ if (isOpenBranchPath((0, path_1.join)(databasePath, databaseEntry.name)))
583
+ continue;
478
584
  if (databaseEntry.isFile() && (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
479
585
  readMetaDb((0, path_1.join)(databasePath, databaseEntry.name), (0, path_1.basename)(databaseEntry.name, '.mdb'), dbName);
480
586
  }
@@ -519,11 +625,15 @@ function getDatabases() {
519
625
  for (const tableName in tables) {
520
626
  if (!definedTables.has(tableName)) {
521
627
  logger.trace(`delete table class ${tableName}`);
628
+ tables[tableName]?.cleanup?.();
522
629
  delete tables[tableName];
523
630
  }
524
631
  }
525
632
  }
526
633
  else {
634
+ const removedTables = exports.databases[dbName];
635
+ for (const tableName in removedTables)
636
+ removedTables[tableName]?.cleanup?.();
527
637
  delete exports.databases[dbName];
528
638
  if (dbName === 'data') {
529
639
  for (const tableName in exports.tables) {
@@ -533,6 +643,7 @@ function getDatabases() {
533
643
  }
534
644
  }
535
645
  }
646
+ hydrateCatalogRelationships();
536
647
  if ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.ANALYTICS_REPLICATE) === false) {
537
648
  if (!exports.NON_REPLICATING_SYSTEM_TABLES.includes('hdb_analytics'))
538
649
  exports.NON_REPLICATING_SYSTEM_TABLES.push('hdb_analytics');
@@ -551,6 +662,148 @@ function getDatabases() {
551
662
  }
552
663
  return exports.databases;
553
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
+ }
554
807
  /**
555
808
  * Scan a databases directory's entries for restore lock/marker files and return the names of
556
809
  * databases that must not be loaded: a held restore lock means a restore is in progress in some
@@ -590,14 +843,14 @@ function readMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, au
590
843
  rootStore = (0, lmdb_1.open)(envInit);
591
844
  lmdbDatabaseEnvs.set(path, rootStore);
592
845
  }
593
- return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
846
+ return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
594
847
  }
595
848
  catch (error) {
596
849
  error.message += ` opening database ${path}`;
597
850
  throw error;
598
851
  }
599
852
  }
600
- function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME) {
853
+ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, { destination, storeName, openedStores } = {}) {
601
854
  try {
602
855
  logger.trace(`loading rocksdb database: ${path}`);
603
856
  if (process.env.HARPER_PARENT_PROCESS_PID) {
@@ -608,14 +861,18 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
608
861
  }
609
862
  let rootStore = rocksdbDatabaseEnvs.get(path);
610
863
  if (rootStore) {
611
- initStores(path, rootStore, databaseName, defaultTable);
864
+ initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
612
865
  }
613
866
  else {
614
867
  rootStore = openRocksDatabase(path, { disableWAL: false, enableStats: true });
615
868
  rocksdbDatabaseEnvs.set(path, rootStore);
616
- initStores(path, rootStore, databaseName, defaultTable);
617
- // Skip transaction log replay in read-only mode
618
- 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) {
619
876
  (0, replayLogs_ts_1.replayLogs)(rootStore, exports.databases[databaseName]);
620
877
  }
621
878
  }
@@ -626,7 +883,9 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
626
883
  throw error;
627
884
  }
628
885
  }
629
- 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;
630
889
  const envInit = new OpenEnvironmentObject_ts_1.default(path, isReadOnlyMode());
631
890
  const internalDbiInit = createOpenDBIObject(false);
632
891
  let attributesDbi = rootStore.dbisDb;
@@ -641,6 +900,7 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
641
900
  else {
642
901
  attributesDbi = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
643
902
  }
903
+ openedStores?.push(attributesDbi);
644
904
  rootStore.dbisDb = markInternalDbiNonVersioned(attributesDbi);
645
905
  }
646
906
  let auditStore = rootStore.auditStore;
@@ -667,7 +927,9 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
667
927
  auditStore = (0, auditStore_ts_1.openAuditStore)(rootStore);
668
928
  }
669
929
  }
670
- const tables = ensureDB(databaseName);
930
+ const tables = destination ?? ensureDB(databaseName);
931
+ if (destination && !destination[DEFINED_TABLES])
932
+ destination[DEFINED_TABLES] = new Set();
671
933
  const definedTables = tables[DEFINED_TABLES];
672
934
  definedTables.rootStore = rootStore;
673
935
  const tablesToLoad = new Map();
@@ -766,10 +1028,20 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
766
1028
  }
767
1029
  }
768
1030
  if (!primaryAttribute) {
769
- 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);
770
1040
  continue;
771
1041
  }
772
1042
  }
1043
+ if (reportedIncompleteCatalogs.size)
1044
+ reportedIncompleteCatalogs.delete(`${databaseName}/${tableName}`);
773
1045
  // if the table has already been defined, use that class, don't create a new one
774
1046
  let table = tables[tableName];
775
1047
  // unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
@@ -815,13 +1087,13 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
815
1087
  // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
816
1088
  if (typeof primaryAttribute.randomAccessFields === 'boolean')
817
1089
  dbiInit.randomAccessStructure = primaryAttribute.randomAccessFields;
818
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
819
- primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true }), rootStore);
820
- }
821
- else {
822
- primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(rootStore.openDB(primaryAttribute.key, dbiInit), rootStore);
823
- }
824
- 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);
825
1097
  primaryStore.tableId = tableId;
826
1098
  }
827
1099
  let attributesUpdated;
@@ -832,6 +1104,7 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
832
1104
  if (!attribute.isPrimaryKey && (attribute.indexed || (attribute.attribute && !attribute.name))) {
833
1105
  if (!indices[attribute.name]) {
834
1106
  const dbi = openIndex(attribute.key, rootStore, attribute);
1107
+ openedStores?.push(dbi);
835
1108
  indices[attribute.name] = dbi;
836
1109
  indices[attribute.name].indexNulls = attribute.indexNulls;
837
1110
  }
@@ -902,6 +1175,8 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
902
1175
  }
903
1176
  else {
904
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),
905
1180
  primaryStore,
906
1181
  auditStore,
907
1182
  audit,
@@ -923,11 +1198,385 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
923
1198
  dbisDB: attributesDbi,
924
1199
  }));
925
1200
  table.schemaVersion = 1;
926
- 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: [] });
927
1210
  }
928
1211
  }
929
1212
  return rootStore;
930
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
+ }
931
1580
  function resetDatabases() {
932
1581
  loadedDatabases = false;
933
1582
  for (const store of Object.values(lmdbDatabaseEnvs)) {
@@ -1038,6 +1687,14 @@ function database({ database: databaseName, table: tableName }) {
1038
1687
  const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
1039
1688
  if (useRocksdb) {
1040
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
+ }
1041
1698
  rootStore = rocksdbDatabaseEnvs.get(path);
1042
1699
  if (!rootStore || rootStore.status === 'closed') {
1043
1700
  // this on-demand open (create_table/create_database and friends) must not resurrect a
@@ -1147,6 +1804,10 @@ async function dropDatabase(databaseName) {
1147
1804
  delete exports.databases[databaseName];
1148
1805
  exports.databaseEventsEmitter.emit('dropDatabase', databaseName);
1149
1806
  if (rootStore) {
1807
+ // awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
1808
+ // already running has released the stores this is about to close and unlink
1809
+ await rootStore.auditStore?.stopAuditCleanup?.();
1810
+ (0, storageReclamation_ts_1.removeStorageReclamation)(rootStore.path);
1150
1811
  if (rootStore.status === 'open') {
1151
1812
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1152
1813
  rootStore.close();
@@ -1164,6 +1825,8 @@ async function dropDatabase(databaseName) {
1164
1825
  // the drop lock now (still before any destructive step)
1165
1826
  if (rootStore instanceof rocksdb_js_1.RocksDatabase)
1166
1827
  lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
1828
+ await rootStore.auditStore?.stopAuditCleanup?.();
1829
+ (0, storageReclamation_ts_1.removeStorageReclamation)(rootStore.path);
1167
1830
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1168
1831
  rootStore.close();
1169
1832
  rootStore.destroy();
@@ -1206,10 +1869,6 @@ function closeDatabase(databaseName) {
1206
1869
  continue;
1207
1870
  if (table.primaryStore.rootStore)
1208
1871
  rootStores.add(table.primaryStore.rootStore);
1209
- for (const indexName in table.indices || {}) {
1210
- closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
1211
- }
1212
- closeStore(table.primaryStore, `table ${tableName}`);
1213
1872
  }
1214
1873
  // a database with no tables (an empty schema, or one whose tables were all dropped) still holds
1215
1874
  // an open root store, tracked only on the defined-database entry rather than any table — include
@@ -1217,7 +1876,23 @@ function closeDatabase(databaseName) {
1217
1876
  const definedRoot = definedDatabases?.get(databaseName)?.rootStore;
1218
1877
  if (definedRoot)
1219
1878
  rootStores.add(definedRoot);
1879
+ // before any table store closes, so no further pass is admitted. This is synchronous, so it cannot
1880
+ // await the drain barrier stopAuditCleanup() returns; what covers it is the in-pass status checks,
1881
+ // plus the fact that its production callers reach it only for RocksDB databases, whose pass is one
1882
+ // synchronous purgeLogs() call with nothing suspended mid-removal.
1883
+ for (const rootStore of rootStores)
1884
+ rootStore.auditStore?.stopAuditCleanup?.();
1885
+ for (const tableName in dbTables) {
1886
+ const table = dbTables[tableName];
1887
+ if (!table?.primaryStore)
1888
+ continue;
1889
+ for (const indexName in table.indices || {}) {
1890
+ closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
1891
+ }
1892
+ closeStore(table.primaryStore, `table ${tableName}`);
1893
+ }
1220
1894
  for (const rootStore of rootStores) {
1895
+ (0, storageReclamation_ts_1.removeStorageReclamation)(rootStore.path);
1221
1896
  closeStore(rootStore.dbisDb, 'attributes store');
1222
1897
  closeStore(rootStore, 'root store');
1223
1898
  lmdbDatabaseEnvs.delete(rootStore.path);
@@ -1247,8 +1922,12 @@ function closeDatabase(databaseName) {
1247
1922
  * database is closed). The `system` database is intentionally left open: it is non-enumerable here
1248
1923
  * (skipped by the loop), is never restored online, and the exiting worker may still touch the job
1249
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.
1250
1928
  */
1251
1929
  function closeLoadedDatabases() {
1930
+ closeBranchDatabases();
1252
1931
  // snapshot the names first: closeDatabase() deletes from `databases` as it goes
1253
1932
  for (const databaseName of Object.keys(exports.databases)) {
1254
1933
  const dbTables = exports.databases[databaseName];
@@ -1351,24 +2030,38 @@ function openIndex(dbiKey, rootStore, attribute) {
1351
2030
  cache: isCustomObjectIndex,
1352
2031
  });
1353
2032
  dbi.rootStore = rootStore;
1354
- // Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
1355
- // transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
1356
- // Verification-Table cache can't track them. A versioned index initialises its encoder as a
1357
- // versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
1358
- // self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
1359
- // decode-free graph traversal. The format is resolved from the persisted attribute descriptor
1360
- // (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
1361
- if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
1362
- 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;
1363
2053
  }
1364
2054
  }
1365
2055
  else {
1366
2056
  dbi = rootStore.openDB(dbiKey, dbiInit);
2057
+ installCustomIndex(dbi);
1367
2058
  }
1368
- if (attribute.indexed.type) {
2059
+ function installCustomIndex(indexStore) {
2060
+ if (!attribute.indexed.type)
2061
+ return;
1369
2062
  const CustomIndex = customIndexes_ts_1.CUSTOM_INDEXES[attribute.indexed.type];
1370
2063
  if (CustomIndex) {
1371
- dbi.customIndex = new CustomIndex(dbi, attribute.indexed);
2064
+ indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
1372
2065
  }
1373
2066
  else {
1374
2067
  logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
@@ -1391,7 +2084,83 @@ function openIndex(dbiKey, rootStore, attribute) {
1391
2084
  * @param replicate
1392
2085
  */
1393
2086
  function table(tableDefinition) {
1394
- 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;
1395
2164
  if (!databaseName)
1396
2165
  databaseName = DEFAULT_DATABASE_NAME;
1397
2166
  // Reject reserved names here too, not only at the operations API: a database
@@ -1404,8 +2173,14 @@ function table(tableDefinition) {
1404
2173
  if (hdbTerms_ts_1.RESERVED_DATABASE_NAMES.includes(databaseName)) {
1405
2174
  throw new hdbError_ts_1.ClientError(`'${databaseName}' is a reserved name and cannot be used as a database name`);
1406
2175
  }
1407
- const rootStore = database({ database: databaseName, table: tableName });
1408
- 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);
1409
2184
  logger.trace(`Defining ${tableName} in ${databaseName}`);
1410
2185
  let Table = tables?.[tableName];
1411
2186
  if (rootStore.status === 'closed') {
@@ -1420,6 +2195,7 @@ function table(tableDefinition) {
1420
2195
  const schemaDefinedExplicit = tableDefinition.schemaDefined !== undefined;
1421
2196
  if (schemaDefined == undefined)
1422
2197
  schemaDefined = true;
2198
+ const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
1423
2199
  const internalDbiInit = createOpenDBIObject(false);
1424
2200
  for (const attribute of attributes) {
1425
2201
  if (attribute.attribute && !attribute.name) {
@@ -1433,124 +2209,161 @@ function table(tableDefinition) {
1433
2209
  attribute.indexed = true;
1434
2210
  }
1435
2211
  let hasChanges;
2212
+ let refreshRelationshipAttributes = false;
2213
+ let deferredPrimaryRow;
2214
+ let unpublishedPrimaryStore;
2215
+ let published = false;
1436
2216
  let releaseExclusiveLock;
1437
- if (Table) {
1438
- primaryKey = Table.primaryKey;
1439
- if (Table.primaryStore.rootStore.status === 'closed') {
1440
- throw new Error(`Can not use a closed data store from ${tableName} class`);
1441
- }
1442
- // Reject moving the primary key to a different attribute on a table that already has records.
1443
- // The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
1444
- // leave describe reporting the new attribute while every record — old and newly inserted — stays
1445
- // keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
1446
- // schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
1447
- // gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
1448
- // See HarperFast/studio#1199.
1449
- const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
1450
- if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
1451
- let hasRecords = false;
1452
- for (const _entry of Table.primaryStore.getRange({ start: true })) {
1453
- hasRecords = true;
1454
- 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`);
1455
2224
  }
1456
- if (hasRecords) {
1457
- throw new hdbError_ts_1.ClientError(`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
1458
- `'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
1459
- `key, or migrate the existing records.`, 400);
1460
- }
1461
- }
1462
- // it table already exists, get the split segments setting
1463
- if (splitSegments == undefined)
1464
- splitSegments = Table.splitSegments;
1465
- Table.attributes.splice(0, Table.attributes.length, ...attributes);
1466
- // Re-assert from the live declaration so a stale value on disk (replicated event,
1467
- // v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
1468
- // callers that omit the flag (cluster schema-replication, data loader) don't flip a
1469
- // dynamic table to true via the default at the top of table().
1470
- if (schemaDefinedExplicit)
1471
- Table.schemaDefined = schemaDefined;
1472
- // Refresh class-level schema metadata to track docstring/directive changes across reloads.
1473
- Table.description = description;
1474
- Table.properties = properties;
1475
- Table.hidden = hidden;
1476
- // undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
1477
- if (cacheControl !== undefined)
1478
- Table.cacheControl = cacheControl;
1479
- }
1480
- else {
1481
- const auditStore = rootStore.auditStore;
1482
- primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
1483
- primaryKey = primaryKeyAttribute.name;
1484
- primaryKeyAttribute.isPrimaryKey = true;
1485
- primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
1486
- primaryKeyAttribute.schemaDefined = schemaDefined;
1487
- // can't change compression after the fact (except threshold), so save only when we create the table
1488
- primaryKeyAttribute.compression = getDefaultCompression();
1489
- if (trackDeletes)
1490
- primaryKeyAttribute.trackDeletes = true;
1491
- audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.LOGGING_AUDITLOG);
1492
- if (expiration)
1493
- primaryKeyAttribute.expiration = expiration;
1494
- if (eviction)
1495
- primaryKeyAttribute.eviction = eviction;
1496
- // persist cacheControl so all threads (and future boots) see it; undefined callers inherit
1497
- // a descriptor value carried by cluster schema events; null (schema has no directive)
1498
- // clears a stale value the carried descriptor may hold
1499
- if (cacheControl === undefined)
1500
- cacheControl = primaryKeyAttribute.cacheControl;
1501
- else if (cacheControl === null)
1502
- delete primaryKeyAttribute.cacheControl;
1503
- else
1504
- primaryKeyAttribute.cacheControl = cacheControl;
1505
- splitSegments ??= false;
1506
- primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
1507
- if (typeof sealed === 'boolean')
1508
- primaryKeyAttribute.sealed = sealed;
1509
- if (typeof replicate === 'boolean')
1510
- primaryKeyAttribute.replicate = replicate;
1511
- // An explicit directive PINS this table's encoding: we persist the boolean, so later changes
1512
- // to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
1513
- // directive are intentionally not persisted here — they follow the current global default on
1514
- // each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
1515
- // struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
1516
- // are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
1517
- if (typeof randomAccessFields === 'boolean')
1518
- primaryKeyAttribute.randomAccessFields = randomAccessFields;
1519
- if (origin) {
1520
- if (!primaryKeyAttribute.origins)
1521
- primaryKeyAttribute.origins = [origin];
1522
- else if (!primaryKeyAttribute.origins.includes(origin))
1523
- primaryKeyAttribute.origins.push(origin);
1524
- }
1525
- logger.trace(`${tableName} table loading, opening primary store`);
1526
- const dbiInit = createOpenDBIObject(false, true);
1527
- dbiInit.compression = primaryKeyAttribute.compression;
1528
- // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
1529
- if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
1530
- dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
1531
- const dbiName = tableName + '/';
1532
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1533
- attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
1534
- ...internalDbiInit,
1535
- disableWAL: false,
1536
- name: terms_ts_1.INTERNAL_DBIS_NAME,
1537
- });
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;
1538
2289
  }
1539
2290
  else {
1540
- attributesDbi = rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
1541
- }
1542
- markInternalDbiNonVersioned(attributesDbi);
1543
- 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)
1544
- const existingTableMeta = attributesDbi.getSync(dbiName);
1545
- if (existingTableMeta && !existingTableMeta.dropping) {
1546
- // table was created while we were setting up
1547
- if (releaseExclusiveLock)
1548
- releaseExclusiveLock();
1549
- resetDatabases();
1550
- return table(tableDefinition);
1551
- }
1552
- let primaryStore;
1553
- 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;
1554
2367
  if (existingTableMeta?.dropping) {
1555
2368
  // A previous drop of this table was interrupted after its tombstone
1556
2369
  // was written. Complete it now (under the exclusive lock) so the
@@ -1578,15 +2391,20 @@ function table(tableDefinition) {
1578
2391
  else {
1579
2392
  primaryStore = rootStore.openDB(dbiName, dbiInit);
1580
2393
  }
2394
+ target.adopt(primaryStore);
2395
+ unpublishedPrimaryStore = primaryStore;
1581
2396
  primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(primaryStore, rootStore);
1582
- 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;
1583
2400
  primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
1584
2401
  logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
1585
2402
  if (!primaryStore.tableId)
1586
2403
  primaryStore.tableId = 1;
1587
2404
  attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
1588
2405
  primaryKeyAttribute.tableId = primaryStore.tableId;
1589
- Table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
2406
+ Table = (0, Table_ts_1.makeTable)({
2407
+ isBranch: Boolean(target.branch),
1590
2408
  primaryStore,
1591
2409
  auditStore,
1592
2410
  audit,
@@ -1609,96 +2427,99 @@ function table(tableDefinition) {
1609
2427
  properties,
1610
2428
  hidden,
1611
2429
  cacheControl,
1612
- }));
2430
+ });
1613
2431
  Table.schemaVersion = 1;
1614
2432
  hasChanges = true;
1615
- attributesDbi.put(dbiName, primaryKeyAttribute);
2433
+ deferredPrimaryRow = primaryKeyAttribute;
1616
2434
  }
1617
- catch (error) {
1618
- // A failure while opening/creating the column family or writing the
1619
- // table id / catalog entry (e.g. into an env poisoned by a prior
1620
- // dangling column family) must NOT leak the exclusive
1621
- // 'update-attributes' spin lock. If it leaks, every subsequent
1622
- // create_table / attribute update on this database spins forever
1623
- // (a hard wedge that pins a worker at 100% CPU). Release before rethrow.
1624
- if (releaseExclusiveLock)
1625
- releaseExclusiveLock();
1626
- throw error;
1627
- }
1628
- }
1629
- const indices = Table.indices;
1630
- if (!attributesDbi) {
1631
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1632
- rootStore.dbisDb = openRocksDatabase(rootStore.path, {
1633
- ...internalDbiInit,
1634
- disableWAL: false,
1635
- name: terms_ts_1.INTERNAL_DBIS_NAME,
1636
- });
1637
- }
1638
- else {
1639
- rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
1640
- }
1641
- attributesDbi = markInternalDbiNonVersioned(rootStore.dbisDb);
1642
- }
1643
- Table.dbisDB = attributesDbi;
1644
- const indicesToRemove = [];
1645
- for (const { key, value } of attributesDbi.getRange({ start: true })) {
1646
- if (value == null)
1647
- continue;
1648
- let [attributeTableName, attribute_name] = key.toString().split('/');
1649
- if (attribute_name === '')
1650
- attribute_name = value.name; // primary key
1651
- if (attribute_name) {
1652
- 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)
1653
2458
  continue;
1654
- }
1655
- else {
1656
- // table attribute for a table with no primary key, we don't want to remove this, so continue on
1657
- continue;
1658
- }
1659
- const attribute = attributes.find((attribute) => attribute.name === attribute_name);
1660
- const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
1661
- if (!attribute || removeIndex) {
1662
- exclusiveLock();
1663
- hasChanges = true;
1664
- if (!attribute)
1665
- attributesDbi.remove(key);
1666
- if (removeIndex) {
1667
- const indexDbi = Table.indices[attributeTableName];
1668
- if (indexDbi)
1669
- indicesToRemove.push(indexDbi);
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
2468
+ continue;
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
+ }
1670
2484
  }
1671
2485
  }
1672
- }
1673
- const attributesToIndex = [];
1674
- try {
1675
2486
  // TODO: If we have attributes and the schemaDefined flag is not set, turn it on
1676
2487
  // iterate through the attributes to ensure that we have all the dbis created and indexed
1677
2488
  for (const attribute of attributes || []) {
1678
- if (attribute.relationship || attribute.computed) {
1679
- hasChanges = true; // need to update the table so the computed properties are translated to property resolvers
1680
- if (attribute.relationship)
1681
- continue;
2489
+ if (attribute.relationship) {
2490
+ refreshRelationshipAttributes = true;
2491
+ continue;
1682
2492
  }
2493
+ if (attribute.computed)
2494
+ hasChanges = true;
1683
2495
  let dbiKey = tableName + '/' + (attribute.name || '');
1684
2496
  Object.defineProperty(attribute, 'key', { value: dbiKey, configurable: true });
1685
2497
  let attributeDescriptor = attributesDbi.getSync(dbiKey);
1686
2498
  if (attribute.isPrimaryKey) {
2499
+ if (deferredPrimaryRow)
2500
+ continue;
1687
2501
  attributeDescriptor = attributeDescriptor || attributesDbi.getSync((dbiKey = tableName + '/')) || {};
1688
2502
  // Persist schemaDefined when the explicit live value disagrees with disk. Without this,
1689
2503
  // a stale `false` (from a v4-era write or replicated event) survives every reload: the
1690
2504
  // in-memory re-assert in the existing-Table branch only fixes the worker that ran @table,
1691
- // 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.
1692
2508
  const schemaDefinedMismatch = schemaDefinedExplicit && attributeDescriptor.schemaDefined !== schemaDefined;
1693
2509
  // primary key can't change indexing, but settings can change
1694
- if (schemaDefinedMismatch ||
1695
- (audit !== undefined && audit !== Table.audit) ||
1696
- (sealed !== undefined && sealed !== Table.sealed) ||
1697
- (replicate !== undefined && replicate !== Table.replicate) ||
1698
- (+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
1699
- (+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
1700
- attribute.type !== attributeDescriptor.type) {
1701
- 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 };
1702
2523
  if (typeof audit === 'boolean') {
1703
2524
  if (audit)
1704
2525
  Table.enableAuditing();
@@ -1717,14 +2538,52 @@ function table(tableDefinition) {
1717
2538
  if (schemaDefinedMismatch)
1718
2539
  updatedPrimaryAttribute.schemaDefined = schemaDefined;
1719
2540
  hasChanges = true; // send out notification of the change
1720
- exclusiveLock();
1721
2541
  attributesDbi.put(dbiKey, updatedPrimaryAttribute);
1722
2542
  }
1723
2543
  continue;
1724
2544
  }
1725
- // note that non-indexed attributes do not need a dbi
1726
2545
  if (attributeDescriptor?.attribute && !attributeDescriptor.name)
1727
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
1728
2587
  // Some index options affect only search, not the stored structure (e.g. HNSW's
1729
2588
  // efConstructionSearch). Changing those should persist the new metadata but NOT trigger a
1730
2589
  // reindex. A custom index declares such keys via a static `searchOnlyOptions`.
@@ -1767,6 +2626,9 @@ function table(tableDefinition) {
1767
2626
  // on the main thread, where workerData is undefined (and it is initialized to 1).
1768
2627
  const currentRestartGeneration = worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber;
1769
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
1770
2632
  // openIndex resolves and stamps attribute.indexFormat for a versioned-capable (RocksDB
1771
2633
  // custom-object) index. An index created before this field existed has no indexFormat on
1772
2634
  // disk; persist the resolved value now — even when nothing else changed — so the format is
@@ -1881,23 +2743,61 @@ function table(tableDefinition) {
1881
2743
  attributesDbi.put(dbiKey, attribute);
1882
2744
  }
1883
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;
1884
2783
  }
1885
2784
  finally {
1886
- if (releaseExclusiveLock)
1887
- releaseExclusiveLock();
2785
+ releaseLock();
1888
2786
  }
1889
- if (hasChanges) {
2787
+ if (hasChanges || refreshRelationshipAttributes) {
1890
2788
  Table.schemaVersion++;
1891
2789
  Table.updatedAttributes();
1892
2790
  }
1893
2791
  logger.trace(`${tableName} table loading, running index`);
2792
+ const branchPath = target.branch?.path;
1894
2793
  if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
1895
- Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove);
2794
+ Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath);
1896
2795
  }
1897
2796
  else if (hasChanges)
1898
- 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));
1899
2798
  Table.origin = origin;
1900
- if (hasChanges) {
2799
+ // scope-private: replication and other global subscribers must not learn of a branch class
2800
+ if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
1901
2801
  exports.databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
1902
2802
  }
1903
2803
  if (expiration || eviction || scanInterval)
@@ -1908,15 +2808,68 @@ function table(tableDefinition) {
1908
2808
  });
1909
2809
  logger.trace(`${tableName} table loaded`);
1910
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
+ }
1911
2866
  // Acquire an exclusive lock for attribute updates
1912
2867
  function exclusiveLock() {
1913
2868
  if (releaseExclusiveLock)
1914
2869
  return;
1915
2870
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1916
- while (!rootStore.tryLock('update-attributes')) { } // use a spin lock, we really need an synchronous exclusive lock here
1917
- releaseExclusiveLock = () => {
1918
- rootStore.unlock('update-attributes');
1919
- };
2871
+ (0, Table_ts_1.acquireUpdateAttributesLock)(rootStore, `table '${databaseName}.${tableName}'`);
2872
+ releaseExclusiveLock = () => (0, Table_ts_1.releaseUpdateAttributesLock)(rootStore);
1920
2873
  }
1921
2874
  else {
1922
2875
  // we only need an exclusive transaction lock in lmdb
@@ -1929,6 +2882,14 @@ function table(tableDefinition) {
1929
2882
  });
1930
2883
  }
1931
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
+ }
1932
2893
  }
1933
2894
  /**
1934
2895
  * Canonical form used ONLY for the structural (reindex-triggering) comparison of index options.
@@ -1966,10 +2927,10 @@ function canonicalizeIndexOptions(value) {
1966
2927
  }
1967
2928
  const MAX_OUTSTANDING_INDEXING = 1000;
1968
2929
  const MIN_OUTSTANDING_INDEXING = 10;
1969
- async function runIndexing(Table, attributes, indicesToRemove) {
2930
+ async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
1970
2931
  try {
1971
2932
  logger.info(`Indexing ${Table.tableName} attributes`, attributes);
1972
- 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));
1973
2934
  let lastResolution;
1974
2935
  for (const index of indicesToRemove) {
1975
2936
  lastResolution = index.drop();
@@ -2137,7 +3098,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2137
3098
  }
2138
3099
  await lastResolution;
2139
3100
  // now notify all the threads that we are done and the index is ready to use
2140
- 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));
2141
3102
  logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
2142
3103
  }
2143
3104
  }