@harperfast/harper 5.2.13 → 5.3.0-beta.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 (655) hide show
  1. package/agent/mcpTools.ts +1 -1
  2. package/agent/session.ts +25 -14
  3. package/bin/cliOperations.ts +58 -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 +120 -4
  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 +3436 -242
  14. package/components/ApplicationScope.ts +8 -0
  15. package/components/EntryHandler.ts +59 -39
  16. package/components/OptionsWatcher.ts +440 -98
  17. package/components/RuntimeModuleTracker.ts +38 -7
  18. package/components/Scope.ts +56 -15
  19. package/components/awaitRestart.ts +84 -0
  20. package/components/componentLoader.ts +381 -32
  21. package/components/componentPreparationLock.ts +16 -5
  22. package/components/deploymentOperations.ts +4 -1
  23. package/components/deploymentRecorder.ts +9 -2
  24. package/components/mcp/adapters/harperHttp.ts +4 -0
  25. package/components/mcp/listChanged.ts +4 -0
  26. package/components/mcp/toolRegistry.ts +2 -0
  27. package/components/mcp/tools/operations.ts +9 -0
  28. package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
  29. package/components/operations.js +537 -113
  30. package/components/operationsValidation.js +98 -3
  31. package/components/packageComponent.ts +25 -1
  32. package/components/requestRestart.ts +11 -0
  33. package/components/status/ComponentStatusRegistry.ts +59 -0
  34. package/config/RootConfigWatcher.ts +240 -40
  35. package/config/configReadRetry.ts +62 -0
  36. package/config/configUtils.ts +357 -48
  37. package/config/harperConfigEnvVars.ts +170 -27
  38. package/config/parseConfigFile.ts +34 -0
  39. package/config/readConfigFileSync.ts +44 -0
  40. package/config/watcherArming.ts +59 -0
  41. package/config-root.schema.json +33 -0
  42. package/dataLayer/blobBackup.ts +160 -50
  43. package/dataLayer/delete.ts +6 -1
  44. package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
  45. package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  46. package/dataLayer/hdbInfoController.ts +34 -1
  47. package/dataLayer/insert.ts +44 -1
  48. package/dataLayer/rocksdbBackup.ts +53 -10
  49. package/dataLayer/schema.ts +11 -1
  50. package/dataLayer/schemaDescribe.ts +8 -1
  51. package/dist/agent/mcpTools.js +1 -1
  52. package/dist/agent/mcpTools.js.map +1 -1
  53. package/dist/agent/session.d.ts +22 -0
  54. package/dist/agent/session.js +26 -15
  55. package/dist/agent/session.js.map +1 -1
  56. package/dist/bin/cliOperations.js +61 -9
  57. package/dist/bin/cliOperations.js.map +1 -1
  58. package/dist/bin/copyDb.d.ts +12 -1
  59. package/dist/bin/copyDb.js +248 -60
  60. package/dist/bin/copyDb.js.map +1 -1
  61. package/dist/bin/deploySetup.d.ts +2 -0
  62. package/dist/bin/deploySetup.js +11 -3
  63. package/dist/bin/deploySetup.js.map +1 -1
  64. package/dist/bin/harper.js +1 -1
  65. package/dist/bin/harper.js.map +1 -1
  66. package/dist/bin/help.js +4 -1
  67. package/dist/bin/help.js.map +1 -1
  68. package/dist/bin/lite.js +4 -1
  69. package/dist/bin/lite.js.map +1 -1
  70. package/dist/bin/restart.js +91 -6
  71. package/dist/bin/restart.js.map +1 -1
  72. package/dist/bin/run.js +4 -10
  73. package/dist/bin/run.js.map +1 -1
  74. package/dist/bin/upgrade.js +4 -3
  75. package/dist/bin/upgrade.js.map +1 -1
  76. package/dist/bin/workloadIdentity.d.ts +18 -0
  77. package/dist/bin/workloadIdentity.js +100 -0
  78. package/dist/bin/workloadIdentity.js.map +1 -0
  79. package/dist/components/Application.d.ts +235 -17
  80. package/dist/components/Application.js +3038 -238
  81. package/dist/components/Application.js.map +1 -1
  82. package/dist/components/ApplicationScope.d.ts +8 -0
  83. package/dist/components/ApplicationScope.js +7 -0
  84. package/dist/components/ApplicationScope.js.map +1 -1
  85. package/dist/components/EntryHandler.js +26 -10
  86. package/dist/components/EntryHandler.js.map +1 -1
  87. package/dist/components/OptionsWatcher.d.ts +4 -0
  88. package/dist/components/OptionsWatcher.js +440 -99
  89. package/dist/components/OptionsWatcher.js.map +1 -1
  90. package/dist/components/RuntimeModuleTracker.js +40 -6
  91. package/dist/components/RuntimeModuleTracker.js.map +1 -1
  92. package/dist/components/Scope.js +52 -13
  93. package/dist/components/Scope.js.map +1 -1
  94. package/dist/components/awaitRestart.d.ts +33 -0
  95. package/dist/components/awaitRestart.js +61 -0
  96. package/dist/components/awaitRestart.js.map +1 -0
  97. package/dist/components/componentLoader.d.ts +38 -1
  98. package/dist/components/componentLoader.js +313 -24
  99. package/dist/components/componentLoader.js.map +1 -1
  100. package/dist/components/componentPreparationLock.d.ts +5 -0
  101. package/dist/components/componentPreparationLock.js +14 -6
  102. package/dist/components/componentPreparationLock.js.map +1 -1
  103. package/dist/components/deploymentOperations.js +4 -1
  104. package/dist/components/deploymentOperations.js.map +1 -1
  105. package/dist/components/deploymentRecorder.d.ts +4 -2
  106. package/dist/components/deploymentRecorder.js +1 -0
  107. package/dist/components/deploymentRecorder.js.map +1 -1
  108. package/dist/components/mcp/adapters/harperHttp.js +4 -0
  109. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  110. package/dist/components/mcp/listChanged.js +5 -0
  111. package/dist/components/mcp/listChanged.js.map +1 -1
  112. package/dist/components/mcp/toolRegistry.d.ts +1 -0
  113. package/dist/components/mcp/toolRegistry.js.map +1 -1
  114. package/dist/components/mcp/tools/operations.d.ts +5 -0
  115. package/dist/components/mcp/tools/operations.js +9 -0
  116. package/dist/components/mcp/tools/operations.js.map +1 -1
  117. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
  118. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  119. package/dist/components/operations.d.ts +28 -0
  120. package/dist/components/operations.js +476 -113
  121. package/dist/components/operations.js.map +1 -1
  122. package/dist/components/operationsValidation.js +97 -3
  123. package/dist/components/operationsValidation.js.map +1 -1
  124. package/dist/components/packageComponent.js +24 -0
  125. package/dist/components/packageComponent.js.map +1 -1
  126. package/dist/components/requestRestart.d.ts +1 -0
  127. package/dist/components/requestRestart.js +7 -0
  128. package/dist/components/requestRestart.js.map +1 -1
  129. package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
  130. package/dist/components/status/ComponentStatusRegistry.js +63 -0
  131. package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
  132. package/dist/config/RootConfigWatcher.d.ts +9 -1
  133. package/dist/config/RootConfigWatcher.js +228 -37
  134. package/dist/config/RootConfigWatcher.js.map +1 -1
  135. package/dist/config/configReadRetry.d.ts +8 -0
  136. package/dist/config/configReadRetry.js +62 -0
  137. package/dist/config/configReadRetry.js.map +1 -0
  138. package/dist/config/configUtils.d.ts +11 -2
  139. package/dist/config/configUtils.js +304 -47
  140. package/dist/config/configUtils.js.map +1 -1
  141. package/dist/config/harperConfigEnvVars.d.ts +16 -0
  142. package/dist/config/harperConfigEnvVars.js +162 -25
  143. package/dist/config/harperConfigEnvVars.js.map +1 -1
  144. package/dist/config/parseConfigFile.d.ts +4 -0
  145. package/dist/config/parseConfigFile.js +35 -0
  146. package/dist/config/parseConfigFile.js.map +1 -0
  147. package/dist/config/readConfigFileSync.d.ts +1 -0
  148. package/dist/config/readConfigFileSync.js +47 -0
  149. package/dist/config/readConfigFileSync.js.map +1 -0
  150. package/dist/config/watcherArming.d.ts +15 -0
  151. package/dist/config/watcherArming.js +59 -0
  152. package/dist/config/watcherArming.js.map +1 -0
  153. package/dist/dataLayer/blobBackup.d.ts +49 -20
  154. package/dist/dataLayer/blobBackup.js +139 -50
  155. package/dist/dataLayer/blobBackup.js.map +1 -1
  156. package/dist/dataLayer/delete.js +1 -1
  157. package/dist/dataLayer/delete.js.map +1 -1
  158. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
  159. package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
  160. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  161. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
  162. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  163. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
  164. package/dist/dataLayer/hdbInfoController.d.ts +10 -0
  165. package/dist/dataLayer/hdbInfoController.js +30 -1
  166. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  167. package/dist/dataLayer/insert.d.ts +9 -1
  168. package/dist/dataLayer/insert.js +30 -0
  169. package/dist/dataLayer/insert.js.map +1 -1
  170. package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
  171. package/dist/dataLayer/rocksdbBackup.js +45 -8
  172. package/dist/dataLayer/rocksdbBackup.js.map +1 -1
  173. package/dist/dataLayer/schema.js +8 -0
  174. package/dist/dataLayer/schema.js.map +1 -1
  175. package/dist/dataLayer/schemaDescribe.js +8 -1
  176. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  177. package/dist/index.d.ts +2 -1
  178. package/dist/index.js +4 -1
  179. package/dist/index.js.map +1 -1
  180. package/dist/json/systemSchema.json +55 -0
  181. package/dist/resources/DatabaseTransaction.d.ts +77 -0
  182. package/dist/resources/DatabaseTransaction.js +586 -54
  183. package/dist/resources/DatabaseTransaction.js.map +1 -1
  184. package/dist/resources/LMDBTransaction.d.ts +2 -1
  185. package/dist/resources/LMDBTransaction.js +43 -6
  186. package/dist/resources/LMDBTransaction.js.map +1 -1
  187. package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
  188. package/dist/resources/PrimaryRocksDatabase.js +49 -5
  189. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  190. package/dist/resources/RecordEncoder.d.ts +20 -0
  191. package/dist/resources/RecordEncoder.js +110 -9
  192. package/dist/resources/RecordEncoder.js.map +1 -1
  193. package/dist/resources/RequestTarget.d.ts +2 -0
  194. package/dist/resources/RequestTarget.js.map +1 -1
  195. package/dist/resources/Resource.js +117 -24
  196. package/dist/resources/Resource.js.map +1 -1
  197. package/dist/resources/ResourceInterface.d.ts +28 -1
  198. package/dist/resources/ResourceInterface.js.map +1 -1
  199. package/dist/resources/RocksIndexStore.d.ts +6 -1
  200. package/dist/resources/RocksIndexStore.js +24 -9
  201. package/dist/resources/RocksIndexStore.js.map +1 -1
  202. package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
  203. package/dist/resources/RocksTransactionLogStore.js +160 -49
  204. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  205. package/dist/resources/Table.d.ts +165 -10
  206. package/dist/resources/Table.js +1805 -303
  207. package/dist/resources/Table.js.map +1 -1
  208. package/dist/resources/analytics/write.js +10 -3
  209. package/dist/resources/analytics/write.js.map +1 -1
  210. package/dist/resources/auditStore.d.ts +181 -2
  211. package/dist/resources/auditStore.js +640 -29
  212. package/dist/resources/auditStore.js.map +1 -1
  213. package/dist/resources/blob.d.ts +129 -9
  214. package/dist/resources/blob.js +992 -114
  215. package/dist/resources/blob.js.map +1 -1
  216. package/dist/resources/branchDatabase.d.ts +48 -0
  217. package/dist/resources/branchDatabase.js +892 -0
  218. package/dist/resources/branchDatabase.js.map +1 -0
  219. package/dist/resources/crdt.js +50 -12
  220. package/dist/resources/crdt.js.map +1 -1
  221. package/dist/resources/dataLoader.js +3 -4
  222. package/dist/resources/dataLoader.js.map +1 -1
  223. package/dist/resources/databases.d.ts +157 -14
  224. package/dist/resources/databases.js +1462 -325
  225. package/dist/resources/databases.js.map +1 -1
  226. package/dist/resources/defineTable.d.ts +10 -2
  227. package/dist/resources/defineTable.js +9 -1
  228. package/dist/resources/defineTable.js.map +1 -1
  229. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  230. package/dist/resources/derivedIndexRegistry.js +68 -0
  231. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  232. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  233. package/dist/resources/derivedIndexRuntime.js +2027 -0
  234. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  235. package/dist/resources/graphql.d.ts +1 -1
  236. package/dist/resources/graphql.js +52 -16
  237. package/dist/resources/graphql.js.map +1 -1
  238. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
  239. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
  240. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  241. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  242. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  243. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  244. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  245. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  246. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  247. package/dist/resources/longLivedTransactions.d.ts +71 -0
  248. package/dist/resources/longLivedTransactions.js +358 -0
  249. package/dist/resources/longLivedTransactions.js.map +1 -0
  250. package/dist/resources/models/backendRegistry.d.ts +26 -0
  251. package/dist/resources/models/backendRegistry.js +60 -2
  252. package/dist/resources/models/backendRegistry.js.map +1 -1
  253. package/dist/resources/models/bootstrap.d.ts +33 -1
  254. package/dist/resources/models/bootstrap.js +416 -31
  255. package/dist/resources/models/bootstrap.js.map +1 -1
  256. package/dist/resources/nodeIdMapping.d.ts +5 -0
  257. package/dist/resources/nodeIdMapping.js +49 -0
  258. package/dist/resources/nodeIdMapping.js.map +1 -1
  259. package/dist/resources/recordLock.d.ts +123 -0
  260. package/dist/resources/recordLock.js +315 -0
  261. package/dist/resources/recordLock.js.map +1 -0
  262. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  263. package/dist/resources/recordLockCoordinator.js +2565 -0
  264. package/dist/resources/recordLockCoordinator.js.map +1 -0
  265. package/dist/resources/replayLogs.d.ts +14 -1
  266. package/dist/resources/replayLogs.js +157 -24
  267. package/dist/resources/replayLogs.js.map +1 -1
  268. package/dist/resources/replayLogsGuards.d.ts +94 -7
  269. package/dist/resources/replayLogsGuards.js +111 -7
  270. package/dist/resources/replayLogsGuards.js.map +1 -1
  271. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  272. package/dist/resources/replicatedApplyFailure.js +63 -0
  273. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  274. package/dist/resources/scheduler/scheduler.js +3 -3
  275. package/dist/resources/scheduler/scheduler.js.map +1 -1
  276. package/dist/resources/search.d.ts +10 -4
  277. package/dist/resources/search.js +283 -37
  278. package/dist/resources/search.js.map +1 -1
  279. package/dist/resources/tracked.d.ts +5 -1
  280. package/dist/resources/tracked.js +74 -23
  281. package/dist/resources/tracked.js.map +1 -1
  282. package/dist/resources/transactionBroadcast.d.ts +1 -1
  283. package/dist/resources/transactionBroadcast.js +2 -2
  284. package/dist/resources/transactionBroadcast.js.map +1 -1
  285. package/dist/security/auth.js +61 -30
  286. package/dist/security/auth.js.map +1 -1
  287. package/dist/security/authn/oidc/claims.d.ts +22 -0
  288. package/dist/security/authn/oidc/claims.js +71 -0
  289. package/dist/security/authn/oidc/claims.js.map +1 -0
  290. package/dist/security/authn/oidc/identityToken.d.ts +27 -0
  291. package/dist/security/authn/oidc/identityToken.js +111 -0
  292. package/dist/security/authn/oidc/identityToken.js.map +1 -0
  293. package/dist/security/authn/oidc/jwks.d.ts +25 -0
  294. package/dist/security/authn/oidc/jwks.js +261 -0
  295. package/dist/security/authn/oidc/jwks.js.map +1 -0
  296. package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
  297. package/dist/security/authn/oidc/providers/generic.js +34 -0
  298. package/dist/security/authn/oidc/providers/generic.js.map +1 -0
  299. package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
  300. package/dist/security/authn/oidc/providers/githubActions.js +129 -0
  301. package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
  302. package/dist/security/authn/oidc/providers/index.d.ts +37 -0
  303. package/dist/security/authn/oidc/providers/index.js +24 -0
  304. package/dist/security/authn/oidc/providers/index.js.map +1 -0
  305. package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
  306. package/dist/security/authn/oidc/tokenExchange.js +306 -0
  307. package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
  308. package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
  309. package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
  310. package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
  311. package/dist/security/authn/oidc/types.d.ts +38 -0
  312. package/dist/security/authn/oidc/types.js +6 -0
  313. package/dist/security/authn/oidc/types.js.map +1 -0
  314. package/dist/security/certificateVerification/index.js +40 -11
  315. package/dist/security/certificateVerification/index.js.map +1 -1
  316. package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
  317. package/dist/security/certificateVerification/trustedIssuers.js +79 -0
  318. package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
  319. package/dist/security/certificateVerification/types.d.ts +1 -0
  320. package/dist/security/credentialProvenance.d.ts +35 -0
  321. package/dist/security/credentialProvenance.js +51 -0
  322. package/dist/security/credentialProvenance.js.map +1 -0
  323. package/dist/security/credentialRejection.d.ts +4 -0
  324. package/dist/security/credentialRejection.js +24 -0
  325. package/dist/security/credentialRejection.js.map +1 -0
  326. package/dist/security/deferredAuthentication.d.ts +36 -0
  327. package/dist/security/deferredAuthentication.js +70 -0
  328. package/dist/security/deferredAuthentication.js.map +1 -0
  329. package/dist/security/impersonation.d.ts +21 -0
  330. package/dist/security/impersonation.js +108 -9
  331. package/dist/security/impersonation.js.map +1 -1
  332. package/dist/security/jsLoader.d.ts +6 -0
  333. package/dist/security/jsLoader.js +77 -15
  334. package/dist/security/jsLoader.js.map +1 -1
  335. package/dist/security/keys.js +301 -71
  336. package/dist/security/keys.js.map +1 -1
  337. package/dist/security/operationScope.d.ts +21 -0
  338. package/dist/security/operationScope.js +36 -0
  339. package/dist/security/operationScope.js.map +1 -0
  340. package/dist/security/permissionsTranslator.js +21 -0
  341. package/dist/security/permissionsTranslator.js.map +1 -1
  342. package/dist/security/tokenAuthentication.d.ts +19 -1
  343. package/dist/security/tokenAuthentication.js +191 -10
  344. package/dist/security/tokenAuthentication.js.map +1 -1
  345. package/dist/security/user.js +4 -3
  346. package/dist/security/user.js.map +1 -1
  347. package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
  348. package/dist/server/DurableSubscriptionsSession.js +67 -10
  349. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  350. package/dist/server/REST.js +106 -2
  351. package/dist/server/REST.js.map +1 -1
  352. package/dist/server/graphqlQuerying.js +4 -0
  353. package/dist/server/graphqlQuerying.js.map +1 -1
  354. package/dist/server/http.d.ts +16 -1
  355. package/dist/server/http.js +122 -17
  356. package/dist/server/http.js.map +1 -1
  357. package/dist/server/itc/serverHandlers.js +8 -1
  358. package/dist/server/itc/serverHandlers.js.map +1 -1
  359. package/dist/server/jobs/jobProcess.js +6 -2
  360. package/dist/server/jobs/jobProcess.js.map +1 -1
  361. package/dist/server/jobs/jobs.js +4 -1
  362. package/dist/server/jobs/jobs.js.map +1 -1
  363. package/dist/server/liveSubscriptionAuth.d.ts +26 -4
  364. package/dist/server/liveSubscriptionAuth.js +105 -39
  365. package/dist/server/liveSubscriptionAuth.js.map +1 -1
  366. package/dist/server/loadRootComponents.js +49 -10
  367. package/dist/server/loadRootComponents.js.map +1 -1
  368. package/dist/server/mqtt.d.ts +2 -0
  369. package/dist/server/mqtt.js +165 -30
  370. package/dist/server/mqtt.js.map +1 -1
  371. package/dist/server/nodeName.d.ts +2 -0
  372. package/dist/server/nodeName.js +107 -23
  373. package/dist/server/nodeName.js.map +1 -1
  374. package/dist/server/serverHelpers/Headers.d.ts +27 -0
  375. package/dist/server/serverHelpers/Headers.js +145 -1
  376. package/dist/server/serverHelpers/Headers.js.map +1 -1
  377. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  378. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  379. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  380. package/dist/server/serverHelpers/Request.d.ts +5 -10
  381. package/dist/server/serverHelpers/Request.js +38 -136
  382. package/dist/server/serverHelpers/Request.js.map +1 -1
  383. package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
  384. package/dist/server/serverHelpers/contentTypes.js +221 -40
  385. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  386. package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
  387. package/dist/server/serverHelpers/deployValidationState.js +9 -19
  388. package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
  389. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
  390. package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
  391. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
  392. package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
  393. package/dist/server/serverHelpers/registeredOperations.js +74 -21
  394. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  395. package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
  396. package/dist/server/serverHelpers/requestSanitization.js +20 -0
  397. package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
  398. package/dist/server/serverHelpers/serverHandlers.js +6 -3
  399. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  400. package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
  401. package/dist/server/serverHelpers/serverUtilities.js +178 -29
  402. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  403. package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
  404. package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
  405. package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
  406. package/dist/server/serverHelpers/uwsServer.js +19 -1
  407. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  408. package/dist/server/static.js +24 -28
  409. package/dist/server/static.js.map +1 -1
  410. package/dist/server/storageReclamation.d.ts +5 -0
  411. package/dist/server/storageReclamation.js +17 -1
  412. package/dist/server/storageReclamation.js.map +1 -1
  413. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  414. package/dist/server/threads/isolatedApplications.js +171 -0
  415. package/dist/server/threads/isolatedApplications.js.map +1 -0
  416. package/dist/server/threads/itc.d.ts +7 -2
  417. package/dist/server/threads/itc.js +5 -1
  418. package/dist/server/threads/itc.js.map +1 -1
  419. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  420. package/dist/server/threads/logRotationTransport.js +33 -0
  421. package/dist/server/threads/logRotationTransport.js.map +1 -0
  422. package/dist/server/threads/manageThreads.d.ts +83 -3
  423. package/dist/server/threads/manageThreads.js +769 -75
  424. package/dist/server/threads/manageThreads.js.map +1 -1
  425. package/dist/server/threads/socketRouter.d.ts +1 -0
  426. package/dist/server/threads/socketRouter.js +277 -31
  427. package/dist/server/threads/socketRouter.js.map +1 -1
  428. package/dist/server/threads/threadHeapMemory.d.ts +2 -0
  429. package/dist/server/threads/threadHeapMemory.js +31 -0
  430. package/dist/server/threads/threadHeapMemory.js.map +1 -0
  431. package/dist/server/threads/threadServer.js +76 -22
  432. package/dist/server/threads/threadServer.js.map +1 -1
  433. package/dist/sqlEngine/config.d.ts +1 -3
  434. package/dist/sqlEngine/config.js +19 -16
  435. package/dist/sqlEngine/config.js.map +1 -1
  436. package/dist/sqlTranslator/index.d.ts +1 -1
  437. package/dist/sqlTranslator/index.js +30 -7
  438. package/dist/sqlTranslator/index.js.map +1 -1
  439. package/dist/upgrade/directives/5-3-0.d.ts +7 -0
  440. package/dist/upgrade/directives/5-3-0.js +148 -0
  441. package/dist/upgrade/directives/5-3-0.js.map +1 -0
  442. package/dist/upgrade/directives/directivesController.js +2 -1
  443. package/dist/upgrade/directives/directivesController.js.map +1 -1
  444. package/dist/utility/OperationFunctionCaller.js +2 -1
  445. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  446. package/dist/utility/common_utils.d.ts +16 -0
  447. package/dist/utility/common_utils.js +32 -6
  448. package/dist/utility/common_utils.js.map +1 -1
  449. package/dist/utility/componentNames.d.ts +8 -0
  450. package/dist/utility/componentNames.js +12 -1
  451. package/dist/utility/componentNames.js.map +1 -1
  452. package/dist/utility/environment/environmentManager.js +3 -6
  453. package/dist/utility/environment/environmentManager.js.map +1 -1
  454. package/dist/utility/environment/systemInformation.d.ts +1 -0
  455. package/dist/utility/environment/systemInformation.js +1 -0
  456. package/dist/utility/environment/systemInformation.js.map +1 -1
  457. package/dist/utility/errors/commonErrors.d.ts +2 -0
  458. package/dist/utility/errors/commonErrors.js +2 -0
  459. package/dist/utility/errors/commonErrors.js.map +1 -1
  460. package/dist/utility/errors/hdbError.d.ts +33 -0
  461. package/dist/utility/errors/hdbError.js +58 -1
  462. package/dist/utility/errors/hdbError.js.map +1 -1
  463. package/dist/utility/globalSchema.d.ts +18 -0
  464. package/dist/utility/hdbTerms.d.ts +18 -0
  465. package/dist/utility/hdbTerms.js +20 -2
  466. package/dist/utility/hdbTerms.js.map +1 -1
  467. package/dist/utility/logging/harper_logger.d.ts +2 -0
  468. package/dist/utility/logging/harper_logger.js +286 -30
  469. package/dist/utility/logging/harper_logger.js.map +1 -1
  470. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  471. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  472. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  473. package/dist/utility/logging/logRotation.d.ts +46 -0
  474. package/dist/utility/logging/logRotation.js +365 -0
  475. package/dist/utility/logging/logRotation.js.map +1 -0
  476. package/dist/utility/logging/logRotator.d.ts +1 -1
  477. package/dist/utility/logging/logRotator.js +192 -85
  478. package/dist/utility/logging/logRotator.js.map +1 -1
  479. package/dist/utility/nodeIdentity.d.ts +9 -0
  480. package/dist/utility/nodeIdentity.js +58 -0
  481. package/dist/utility/nodeIdentity.js.map +1 -0
  482. package/dist/utility/npmUtilities.js +11 -7
  483. package/dist/utility/npmUtilities.js.map +1 -1
  484. package/dist/utility/operationPermissions.d.ts +3 -1
  485. package/dist/utility/operationPermissions.js +16 -1
  486. package/dist/utility/operationPermissions.js.map +1 -1
  487. package/dist/utility/operation_authorization.d.ts +10 -7
  488. package/dist/utility/operation_authorization.js +212 -41
  489. package/dist/utility/operation_authorization.js.map +1 -1
  490. package/dist/utility/watchPath.d.ts +29 -0
  491. package/dist/utility/watchPath.js +68 -0
  492. package/dist/utility/watchPath.js.map +1 -0
  493. package/dist/utility/watcherFallback.d.ts +41 -0
  494. package/dist/utility/watcherFallback.js +153 -0
  495. package/dist/utility/watcherFallback.js.map +1 -1
  496. package/dist/validation/configValidator.d.ts +12 -0
  497. package/dist/validation/configValidator.js +205 -75
  498. package/dist/validation/configValidator.js.map +1 -1
  499. package/dist/validation/installValidator.js +12 -0
  500. package/dist/validation/installValidator.js.map +1 -1
  501. package/dist/validation/validationWrapper.d.ts +11 -0
  502. package/dist/validation/validationWrapper.js +16 -3
  503. package/dist/validation/validationWrapper.js.map +1 -1
  504. package/index.ts +8 -0
  505. package/json/systemSchema.json +55 -0
  506. package/npm-shrinkwrap.json +286 -194
  507. package/package.json +15 -7
  508. package/resources/DESIGN.md +219 -52
  509. package/resources/DatabaseTransaction.ts +657 -52
  510. package/resources/LMDBTransaction.ts +46 -6
  511. package/resources/PrimaryRocksDatabase.ts +46 -7
  512. package/resources/RecordEncoder.ts +124 -9
  513. package/resources/RequestTarget.ts +2 -0
  514. package/resources/Resource.ts +114 -22
  515. package/resources/ResourceInterface.ts +31 -0
  516. package/resources/RocksIndexStore.ts +30 -9
  517. package/resources/RocksTransactionLogStore.ts +188 -51
  518. package/resources/Table.ts +1960 -301
  519. package/resources/analytics/write.ts +10 -3
  520. package/resources/auditStore.ts +647 -32
  521. package/resources/blob.ts +1029 -110
  522. package/resources/branchDatabase.ts +941 -0
  523. package/resources/crdt.ts +76 -12
  524. package/resources/dataLoader.ts +3 -4
  525. package/resources/databases.ts +1584 -320
  526. package/resources/defineTable.ts +18 -2
  527. package/resources/derivedIndexRegistry.ts +56 -0
  528. package/resources/derivedIndexRuntime.ts +2292 -0
  529. package/resources/graphql.ts +72 -17
  530. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
  531. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  532. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  533. package/resources/longLivedTransactions.ts +360 -0
  534. package/resources/models/backendRegistry.ts +84 -2
  535. package/resources/models/bootstrap.ts +473 -28
  536. package/resources/nodeIdMapping.ts +50 -0
  537. package/resources/recordLock.ts +419 -0
  538. package/resources/recordLockCoordinator.ts +3043 -0
  539. package/resources/replayLogs.ts +152 -26
  540. package/resources/replayLogsGuards.ts +171 -8
  541. package/resources/replicatedApplyFailure.ts +77 -0
  542. package/resources/scheduler/scheduler.ts +4 -4
  543. package/resources/search.ts +288 -46
  544. package/resources/tracked.ts +73 -22
  545. package/resources/transactionBroadcast.ts +3 -3
  546. package/security/auth.ts +68 -29
  547. package/security/authn/oidc/claims.ts +72 -0
  548. package/security/authn/oidc/identityToken.ts +129 -0
  549. package/security/authn/oidc/jwks.ts +260 -0
  550. package/security/authn/oidc/providers/generic.ts +40 -0
  551. package/security/authn/oidc/providers/githubActions.ts +137 -0
  552. package/security/authn/oidc/providers/index.ts +52 -0
  553. package/security/authn/oidc/tokenExchange.ts +300 -0
  554. package/security/authn/oidc/trustPolicyOperations.ts +343 -0
  555. package/security/authn/oidc/types.ts +41 -0
  556. package/security/certificateVerification/index.ts +54 -13
  557. package/security/certificateVerification/trustedIssuers.ts +76 -0
  558. package/security/certificateVerification/types.ts +1 -0
  559. package/security/credentialProvenance.ts +47 -0
  560. package/security/credentialRejection.ts +22 -0
  561. package/security/deferredAuthentication.ts +71 -0
  562. package/security/impersonation.ts +117 -12
  563. package/security/jsLoader.ts +83 -18
  564. package/security/keys.ts +298 -72
  565. package/security/operationScope.ts +33 -0
  566. package/security/permissionsTranslator.js +23 -0
  567. package/security/tokenAuthentication.ts +233 -12
  568. package/security/user.ts +4 -3
  569. package/server/DESIGN.md +194 -16
  570. package/server/DurableSubscriptionsSession.ts +71 -11
  571. package/server/REST.ts +115 -4
  572. package/server/graphqlQuerying.ts +4 -0
  573. package/server/http.ts +133 -20
  574. package/server/itc/serverHandlers.js +8 -1
  575. package/server/jobs/jobProcess.ts +8 -2
  576. package/server/jobs/jobs.ts +4 -1
  577. package/server/liveSubscriptionAuth.ts +129 -46
  578. package/server/loadRootComponents.js +50 -8
  579. package/server/mqtt.ts +179 -38
  580. package/server/nodeName.ts +103 -21
  581. package/server/serverHelpers/Headers.ts +135 -0
  582. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  583. package/server/serverHelpers/Request.ts +33 -131
  584. package/server/serverHelpers/contentTypes.ts +217 -36
  585. package/server/serverHelpers/deployValidationState.ts +24 -13
  586. package/server/serverHelpers/operationAuthorizationState.ts +34 -3
  587. package/server/serverHelpers/registeredOperations.ts +79 -22
  588. package/server/serverHelpers/requestSanitization.ts +15 -0
  589. package/server/serverHelpers/serverHandlers.js +6 -3
  590. package/server/serverHelpers/serverUtilities.ts +232 -40
  591. package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
  592. package/server/serverHelpers/uwsServer.ts +17 -2
  593. package/server/static.ts +23 -29
  594. package/server/storageReclamation.ts +15 -2
  595. package/server/threads/isolatedApplications.ts +157 -0
  596. package/server/threads/itc.js +11 -1
  597. package/server/threads/logRotationTransport.ts +40 -0
  598. package/server/threads/manageThreads.js +771 -66
  599. package/server/threads/socketRouter.ts +291 -30
  600. package/server/threads/threadHeapMemory.ts +26 -0
  601. package/server/threads/threadServer.js +73 -22
  602. package/sqlTranslator/index.ts +31 -8
  603. package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
  604. package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
  605. package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
  606. package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
  607. package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
  608. package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
  609. package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
  610. package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
  611. package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
  612. package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
  613. package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
  614. package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
  615. package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
  616. package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  617. package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
  618. package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
  619. package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
  620. package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
  621. package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
  622. package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
  623. package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
  624. package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
  625. package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
  626. package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
  627. package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
  628. package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  629. package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
  630. package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
  631. package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
  632. package/studio/web/index.html +1 -1
  633. package/upgrade/directives/5-3-0.ts +132 -0
  634. package/upgrade/directives/directivesController.ts +2 -1
  635. package/utility/OperationFunctionCaller.ts +2 -1
  636. package/utility/common_utils.ts +30 -5
  637. package/utility/componentNames.ts +12 -0
  638. package/utility/environment/environmentManager.ts +3 -7
  639. package/utility/environment/systemInformation.ts +7 -0
  640. package/utility/errors/commonErrors.ts +4 -0
  641. package/utility/errors/hdbError.ts +57 -0
  642. package/utility/hdbTerms.ts +19 -0
  643. package/utility/logging/harper_logger.ts +278 -26
  644. package/utility/logging/logGenerationCoordinator.ts +196 -0
  645. package/utility/logging/logRotation.ts +367 -0
  646. package/utility/logging/logRotator.ts +213 -81
  647. package/utility/nodeIdentity.ts +45 -0
  648. package/utility/npmUtilities.ts +12 -8
  649. package/utility/operationPermissions.ts +18 -1
  650. package/utility/operation_authorization.ts +231 -42
  651. package/utility/watchPath.ts +63 -0
  652. package/utility/watcherFallback.ts +148 -0
  653. package/validation/configValidator.ts +215 -75
  654. package/validation/installValidator.ts +15 -0
  655. package/validation/validationWrapper.ts +18 -4
@@ -1,7 +1,8 @@
1
1
  import { cleanupUnusedBlobs, collectRetainedFileIds } from './blob.ts';
2
2
  import { Transaction as LMDBTransaction } from 'lmdb';
3
3
  import { getNextMonotonicTime } from '../utility/lmdb/commonUtility.ts';
4
- import { ServerError } from '../utility/errors/hdbError.ts';
4
+ import { ServerError, TransactionCommitConflictTimeoutError } from '../utility/errors/hdbError.ts';
5
+ import { lockNotHeldError, type RecordLockHandle } from './recordLock.ts';
5
6
  import * as harperLogger from '../utility/logging/harper_logger.ts';
6
7
  import type { Context, Id } from './ResourceInterface.ts';
7
8
  import * as envMngr from '../utility/environment/environmentManager.ts';
@@ -14,6 +15,15 @@ const RETRY_NOW_VALUE = constants.RETRY_NOW_VALUE;
14
15
  import type { RootDatabaseKind } from './databases.ts';
15
16
  import type { Entry } from './RecordEncoder.ts';
16
17
  import { toBufferKey } from 'ordered-binary';
18
+ import {
19
+ createLongLivedHolderReportBudget,
20
+ describeHolderCandidates,
21
+ finishLongLivedHolderReports,
22
+ getReportThresholdMs,
23
+ rebaseLongLivedTransactionReports,
24
+ reportLongLivedHolder,
25
+ type LongLivedHolderReportBudget,
26
+ } from './longLivedTransactions.ts';
17
27
 
18
28
  const trackedTxns = new Set<DatabaseTransaction>();
19
29
  const readTransactionOwners = new WeakMap<ReadTransaction, DatabaseTransaction>();
@@ -24,7 +34,7 @@ const SNAPSHOT_FREE = Object.freeze({ disableSnapshot: true });
24
34
  // is what the read-queue-depth metric counts, while this holds one entry per logical transaction — the
25
35
  // chain root — so a chain child can never become its own timeout root (issue #2231).
26
36
  const supervisedWriteRoots = new Set<DatabaseTransaction>();
27
- const MAX_OUTSTANDING_TXN_DURATION = convertToMS(envMngr.get(CONFIG_PARAMS.STORAGE_MAXTRANSACTIONQUEUETIME)) || 45000; // Allow write transactions to be queued for up to 45 seconds before we start rejecting them
37
+ let MAX_OUTSTANDING_TXN_DURATION = convertToMS(envMngr.get(CONFIG_PARAMS.STORAGE_MAXTRANSACTIONQUEUETIME)) || 45000; // Allow write transactions to be queued for up to 45 seconds before we start rejecting them
28
38
  const DEBUG_LONG_TXNS = envMngr.get(CONFIG_PARAMS.STORAGE_DEBUGLONGTRANSACTIONS);
29
39
  export const TRANSACTION_STATE = {
30
40
  CLOSED: 0, // the transaction has been committed or aborted and can no longer be used for writes (if read txn is active, it can be used for reads)
@@ -36,6 +46,12 @@ export const TRANSACTION_STATE = {
36
46
  LINGERING: 2,
37
47
  };
38
48
  const MAX_RETRIES = 40;
49
+ // Over-limit monitor ticks a transaction parked in its commit phase is spared before it is aborted
50
+ // like any other over-time transaction. Sparing re-arms `timeout`, so each of these costs two
51
+ // monitor intervals — roughly 10 minutes at the 30s default. Generous, so a legitimately large blob
52
+ // write finishes, but bounded, so a pre-commit source that stalls instead of finishing can't pin its
53
+ // read snapshot forever (issue #2062).
54
+ export const COMMIT_PHASE_GRACE = 10;
39
55
  // Cap the per-retry backoff so replication-applied transactions, which retry conflicts without a
40
56
  // cap (see the commit rejection handler), don't grow the delay unbounded.
41
57
  const MAX_RETRY_DELAY_MS = 1000;
@@ -69,7 +85,33 @@ let outstandingCommitCount = 0;
69
85
  // the whole thread, regardless of how many distinct commits individually cross the threshold — see
70
86
  // the comment at the log site for why a per-commit-only dedup isn't enough under sustained overload.
71
87
  const OVERLOAD_LOG_MIN_INTERVAL_MS = 1000;
72
- let lastOverloadLogAt = -Infinity;
88
+ // One cooldown per reporting site, not one shared: `shed` fires on every bystander write during a
89
+ // wedge and would starve `abandon`, which names a different transaction and is the only server-side
90
+ // record of why a request was failed. Mutates only when it grants, so a caller with its own
91
+ // suppression as well (checkOverloaded's per-node `logged`) must test that first.
92
+ const lastStuckCommitLogAt = { shed: -Infinity, abandon: -Infinity };
93
+ function allowStuckCommitLog(site: 'shed' | 'abandon', now: number): boolean {
94
+ if (now - lastStuckCommitLogAt[site] <= OVERLOAD_LOG_MIN_INTERVAL_MS) return false;
95
+ lastStuckCommitLogAt[site] = now;
96
+ return true;
97
+ }
98
+
99
+ // Which database/table, which native transaction, and which request a stuck commit belongs to —
100
+ // without it a wedge gives no indication of what to investigate (harper#2001).
101
+ function describeCommitIdentity(
102
+ store: any,
103
+ startedFrom: { resourceName: string; method: string } | undefined,
104
+ nativeTransaction: any
105
+ ): string {
106
+ const nativeTransactionId = nativeTransaction?.id;
107
+ return (
108
+ `from table: ${store?.rootStore?.databaseName ?? '?'}.${store?.name ?? '?'}` +
109
+ (nativeTransactionId !== undefined ? ` (transaction ${nativeTransactionId})` : '') +
110
+ (startedFrom?.resourceName
111
+ ? `, started from ${startedFrom.resourceName}${startedFrom.method ? '.' + startedFrom.method : ''}`
112
+ : '')
113
+ );
114
+ }
73
115
 
74
116
  // Track a submitted commit until it settles. Every attempt is tracked unconditionally: a
75
117
  // coordinated retry round and a chained second-store commit are both issued from inside the
@@ -146,6 +188,20 @@ export function getOutstandingCommits(): { count: number; oldestAgeMs: number |
146
188
  // about the caller's pattern, not the individual commit.
147
189
  let replayedWritesWarned = false;
148
190
 
191
+ /**
192
+ * Abort a detached native handle. RocksTransaction.abort() throws on one that was already
193
+ * committed or aborted, and every caller is a cleanup path whose own callers have no handler — a
194
+ * throw there would abandon the rest of the cleanup.
195
+ */
196
+ function abortNativeTransaction(transaction: RocksTransaction | null | undefined, context: string): void {
197
+ if (transaction == null) return;
198
+ try {
199
+ transaction.abort();
200
+ } catch (error) {
201
+ harperLogger.debug?.(context, error);
202
+ }
203
+ }
204
+
149
205
  // The analytics module registers a recorder here at load (dependency inversion, mirroring
150
206
  // `replicationConfirmation` below) so the storage layer doesn't statically import the analytics/server
151
207
  // modules. Unset until analytics loads, and when analytics is disabled the recorder call is cheap.
@@ -244,15 +300,21 @@ class ReadSnapshotExpiredError extends ServerError {
244
300
  }
245
301
  }
246
302
 
247
- export function trackReadRange(transaction: ReadTransaction, createRange: () => any): any {
303
+ export function getReadTransactionGuard(transaction: ReadTransaction): (() => void) | undefined {
248
304
  const owner = readTransactionOwners.get(transaction);
249
- if (!owner) return createRange();
250
- function checkActive() {
305
+ if (!owner) return;
306
+ return function checkActive() {
251
307
  if (owner.timedOut) throw transactionOpenTooLongError();
252
308
  if (owner.transaction !== transaction) {
253
309
  throw new ReadSnapshotExpiredError();
254
310
  }
255
- }
311
+ };
312
+ }
313
+
314
+ export function trackReadRange(transaction: ReadTransaction, createRange: () => any): any {
315
+ const owner = readTransactionOwners.get(transaction);
316
+ if (!owner) return createRange();
317
+ const checkActive = getReadTransactionGuard(transaction)!;
256
318
  checkActive();
257
319
  const range = createRange();
258
320
  const iterate = range.iterate;
@@ -324,6 +386,11 @@ type ReadTransaction = (LMDBTransaction | RocksTransaction) & {
324
386
  isCommitted?: boolean;
325
387
  };
326
388
 
389
+ export type WriteGeneration = {
390
+ closed: boolean;
391
+ internalWrites: number;
392
+ };
393
+
327
394
  export type TransactionWrite = {
328
395
  key: Id;
329
396
  store: any; // using any here because of circular dependency and complex RootDatabaseKind
@@ -336,6 +403,9 @@ export type TransactionWrite = {
336
403
  // overload accounting, the replay marker and a no-op write's removal all belong to the committer.
337
404
  validate?: (txnTime: number, committedBy: DatabaseTransaction) => void;
338
405
  fullUpdate?: boolean;
406
+ // The origin record version carried by an applied or replayed write. Bound it by the origin's
407
+ // transaction-log key so malformed or historical overloaded values cannot move ordering past the write.
408
+ recordVersion?: number;
339
409
  saved?: boolean;
340
410
  deferSave?: boolean;
341
411
  skipReplicationConfirmation?: boolean;
@@ -374,8 +444,53 @@ export type TransactionWrite = {
374
444
  // this write appended an audit entry, which references its saved blobs — they then belong to the
375
445
  // audit trail (audit pruning deletes them), so the superseded-write cleanup must leave them alone
376
446
  blobsAuditReferenced?: boolean;
447
+ // Lock handle set by Table._writeUpdate when the write is staged through a held record lock;
448
+ // used in DatabaseTransaction.save() to assign and track that handle's write versions.
449
+ lockHandle?: RecordLockHandle;
450
+ // Per-operation holder version, set once on first save() and reused on retry so that
451
+ // ImmediateTransaction's sequential immediateCommit saves don't collide: each write carries its
452
+ // own stamp independently of this.timestamp, which may not reset to 0 between saves.
453
+ lockStamp?: number;
454
+ // Version staged by a write that actually changed the record this round. It advances any
455
+ // same-key held handle's floor only after the native transaction commits successfully.
456
+ appliedRecordVersion?: number;
457
+ // Present only while a transaction owns record locks, keeping bookkeeping off ordinary writes.
458
+ trackRecordVersion?: boolean;
459
+ // Set by a table commit handler only when this retry round staged a record change.
460
+ recordVersionApplied?: boolean;
461
+ // Set by DatabaseTransaction.save() on the immediateCommit path: the Promise returned by the
462
+ // inner this.commit({ transaction }) call. The ImmediateTransaction outer commit resolves before
463
+ // this settles (fire-and-forget from the if-branch), so Table.save()'s lock-writable path awaits
464
+ // it to ensure the write is durable before resolving to the caller.
465
+ innerCommit?: MaybePromise<CommitResolution>;
466
+ // the commit derives stored state (folds, index diffs, residency) from its base entry, so
467
+ // save() must reload that base through the committing transaction's snapshot
468
+ reloadCommitBase?: boolean;
469
+ writeGeneration?: WriteGeneration;
470
+ instanceClosed?: boolean;
377
471
  };
378
472
 
473
+ export function closeWriteInstance(operation: TransactionWrite | null | undefined): void {
474
+ if (operation && !operation.instanceClosed) {
475
+ operation.instanceClosed = true;
476
+ if (operation.writeGeneration) operation.writeGeneration.closed = true;
477
+ }
478
+ }
479
+
480
+ export function validateWrite(operation: TransactionWrite, txnTime: number, transaction: DatabaseTransaction): any {
481
+ const generation = operation.writeGeneration;
482
+ if (generation) generation.internalWrites++;
483
+ try {
484
+ return operation.validate?.(txnTime, transaction);
485
+ } finally {
486
+ if (generation) generation.internalWrites--;
487
+ }
488
+ }
489
+
490
+ export function getAppliedWriteVersion(recordVersion: number | undefined, txnLogKey: number): number {
491
+ return recordVersion == null ? txnLogKey : Math.min(recordVersion, txnLogKey);
492
+ }
493
+
379
494
  /**
380
495
  * The state a preceding write in this transaction left for `operation`'s key, or undefined if this
381
496
  * is the first write to it. Within a transaction the writes are ordered by program order, and
@@ -420,6 +535,7 @@ export class DatabaseTransaction implements Transaction {
420
535
  this.#scopeOwned = options?.scopeOwned === true;
421
536
  }
422
537
  writes: TransactionWrite[] = []; // the set of writes to commit if the conditions are met
538
+ ownedWrites?: WeakSet<TransactionWrite>;
423
539
  // the last staged write per store and key, used to chain repeat writes to the same key (linkWrite)
424
540
  declare writesByKey?: Map<any, Map<unknown, TransactionWrite>>;
425
541
  completions: Promise<void>[] = []; // the set of outstanding async operations to complete
@@ -435,7 +551,11 @@ export class DatabaseTransaction implements Transaction {
435
551
  // link with no writes that is being read in a loop would otherwise masquerade as write activity and
436
552
  // keep a write-holding head immortal.
437
553
  declare writeTimeout: number;
554
+ writeTick = -1;
438
555
  timeoutBudget = 0;
556
+ // Initialized rather than `declare`d so the class shape stays monomorphic on the write path. One clock
557
+ // read per handle is what lets the monitor age a handle without calling into the registry every tick.
558
+ handleOpenedAt = 0;
439
559
  // save() only stages here; ImmediateTransaction overrides it to commit, which addWrite must not defer
440
560
  saveCommits = false;
441
561
  // True where save() puts the write into this transaction's native handle, which is what lets a scope
@@ -449,6 +569,13 @@ export class DatabaseTransaction implements Transaction {
449
569
  declare next: DatabaseTransaction;
450
570
  // The head of this multi-store chain, set when the link is created; absent on the head itself.
451
571
  declare root?: DatabaseTransaction;
572
+ // When this logical commit first reached the storage engine, held on the chain root so every
573
+ // retry round and every chained store measures ONE elapsed wait (issue #2450). Deliberately not
574
+ // the per-attempt clock trackOutstandingCommit() keeps: that one drives thread-wide load
575
+ // shedding and must stay per-attempt, or a long uncapped source-apply retry would 503 every
576
+ // unrelated request on the thread. Cleared when the logical commit settles, so a reused
577
+ // transaction's next batch starts on a fresh budget.
578
+ declare commitStartedAt?: number;
452
579
  // Whether this link is why its chain root is write-supervised (see endWriteSupervision).
453
580
  declare writeSupervised?: boolean;
454
581
  declare stale: boolean;
@@ -489,6 +616,37 @@ export class DatabaseTransaction implements Transaction {
489
616
  // asks to stop reading a pinned snapshot, so re-pinning one for the rest of the scope would take
490
617
  // back what it asked for.
491
618
  snapshotFree = false;
619
+ // Set while commit() is parked in its pre-commit await (`before`/`beforeIntermediate` completions —
620
+ // in practice a blob's durable file write). The write set is sealed and the caller is awaiting the
621
+ // commit, so this is core's own I/O rather than an application holding a transaction open, which is
622
+ // what the open-transaction limit polices: the monitor spares it for COMMIT_PHASE_GRACE ticks
623
+ // instead of poisoning it (issue #2062).
624
+ committing = false;
625
+ commitPhaseTicks = 0;
626
+ declare commitChainHead?: DatabaseTransaction;
627
+ // O(1) lookup in recordLockFor; only lock() handles are registered here (no gate handles).
628
+ declare recordLocks?: Map<any, Map<unknown, RecordLockHandle>>;
629
+ /**
630
+ * Whether any staged write was made through a record lock handle. The commit-time lease fence
631
+ * below is skipped entirely when this is false, which is every transaction in a core-only
632
+ * deployment: without it a bulk transaction of 100,000 plain writes pays 100,000 property checks
633
+ * before submission, on a path that is supposed to be untouched when no lock is involved. It
634
+ * latches rather than tracking a count, because a write whose handle was released or expired after
635
+ * staging is exactly what the fence exists to catch.
636
+ */
637
+ declare hasLeaseProtectedWrite?: boolean;
638
+ // Tracks in-flight acquireRecordKey calls so concurrent lock() calls for the same key in one
639
+ // link (e.g. Promise.all([T.lock(id), T.lock(id)])) can coalesce rather than self-block.
640
+ declare pendingLocks?: Map<any, Map<unknown, Promise<RecordLockHandle>>>;
641
+
642
+ setCommitPhase(committing: boolean): void {
643
+ // A commit phase covers the sealed write set across the whole multi-store chain.
644
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
645
+ txn.committing = committing;
646
+ txn.commitChainHead = committing ? this : undefined;
647
+ if (committing) txn.commitPhaseTicks = 0;
648
+ }
649
+ }
492
650
 
493
651
  rangeReadActive = false;
494
652
 
@@ -507,6 +665,20 @@ export class DatabaseTransaction implements Transaction {
507
665
  }
508
666
  }
509
667
 
668
+ // Each engine keeps its own expiration; LMDBTransaction overrides this with its own.
669
+ renewIdleTimeout(): void {
670
+ this.timeout = Math.max(txnExpiration, this.timeoutBudget ?? 0);
671
+ }
672
+
673
+ // The links after this one wait for its native commit before their own commit() is entered; each
674
+ // hop starts with a full idle window and a stalled native commit is still bounded by one.
675
+ renewChainForNativeCommit(): void {
676
+ for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
677
+ if (txn.timedOut || txn.open === TRANSACTION_STATE.CLOSED) continue;
678
+ txn.renewIdleTimeout();
679
+ }
680
+ }
681
+
510
682
  getReadTxn(disableSnapshot?: boolean): ReadTransaction {
511
683
  this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
512
684
  this.renewReadTimeout();
@@ -548,6 +720,7 @@ export class DatabaseTransaction implements Transaction {
548
720
  this.rangeReadActive = false;
549
721
  this.readTxnsUsed = 1;
550
722
  this.baseReadRefConsumed = false;
723
+ this.handleOpenedAt = performance.now();
551
724
  }
552
725
 
553
726
  /**
@@ -589,6 +762,7 @@ export class DatabaseTransaction implements Transaction {
589
762
  this.rangeReadActive = false;
590
763
  this.readTxnsUsed = 0;
591
764
  this.readTxnRefCount = 0;
765
+ this.handleOpenedAt = 0;
592
766
  return transaction;
593
767
  }
594
768
 
@@ -635,11 +809,7 @@ export class DatabaseTransaction implements Transaction {
635
809
  */
636
810
  releaseReadTxn(): void {
637
811
  const transaction = this.detachOwnedTransaction();
638
- try {
639
- transaction?.abort();
640
- } catch (error) {
641
- harperLogger.debug?.('releasing timed-out read transaction', error);
642
- }
812
+ abortNativeTransaction(transaction, 'releasing timed-out read transaction');
643
813
  this.completeDeferredContextRelease();
644
814
  }
645
815
 
@@ -688,6 +858,7 @@ export class DatabaseTransaction implements Transaction {
688
858
  detachWrite(operation: TransactionWrite): void {
689
859
  const index = this.writes.indexOf(operation);
690
860
  if (index > -1) this.writes[index] = null;
861
+ this.ownedWrites?.delete(operation);
691
862
  if (operation.key === undefined) return;
692
863
  const writesForStore = this.writesByKey?.get(operation.store);
693
864
  if (!writesForStore) return;
@@ -695,7 +866,7 @@ export class DatabaseTransaction implements Transaction {
695
866
  // Membership, not `stagedIn`, which every commit handler clears and so cannot tell a takeover from a
696
867
  // write done in place; a prior already taken over must not become this transaction's basis again.
697
868
  let prior = operation.priorWrite;
698
- while (prior && !this.writes.includes(prior)) prior = prior.priorWrite;
869
+ while (prior && !this.ownedWrites?.has(prior)) prior = prior.priorWrite;
699
870
  const tail = writesForStore.get(keyId);
700
871
  if (tail === operation) {
701
872
  if (prior) writesForStore.set(keyId, prior);
@@ -712,6 +883,70 @@ export class DatabaseTransaction implements Transaction {
712
883
  }
713
884
  }
714
885
 
886
+ registerRecordLock(handle: RecordLockHandle): void {
887
+ if (!this.recordLocks) this.recordLocks = new Map();
888
+ let storeMap = this.recordLocks.get(handle.store);
889
+ if (!storeMap) this.recordLocks.set(handle.store, (storeMap = new Map()));
890
+ storeMap.set(handle.keyId, handle);
891
+ }
892
+
893
+ recordLockFor(store: any, keyId: unknown): RecordLockHandle | undefined {
894
+ const storeMap = this.recordLocks?.get(store);
895
+ if (!storeMap) return undefined;
896
+ const h = storeMap.get(keyId);
897
+ if (!h) return undefined;
898
+ if (!h.isExpired()) return h;
899
+ // Prune released/expired handles so they don't accumulate; a handle past its deadline checking
900
+ // re-entrancy would otherwise be seen as the holder and incorrectly granted access.
901
+ storeMap.delete(keyId);
902
+ return undefined;
903
+ }
904
+
905
+ unregisterRecordLock(handle: RecordLockHandle): void {
906
+ const storeMap = this.recordLocks?.get(handle.store);
907
+ if (storeMap?.get(handle.keyId) === handle) storeMap.delete(handle.keyId);
908
+ }
909
+
910
+ registerPendingLock(store: any, keyId: unknown, pending: Promise<RecordLockHandle>): void {
911
+ if (!this.pendingLocks) this.pendingLocks = new Map();
912
+ let storeMap = this.pendingLocks.get(store);
913
+ if (!storeMap) this.pendingLocks.set(store, (storeMap = new Map()));
914
+ storeMap.set(keyId, pending);
915
+ }
916
+
917
+ pendingLockFor(store: any, keyId: unknown): Promise<RecordLockHandle> | undefined {
918
+ return this.pendingLocks?.get(store)?.get(keyId);
919
+ }
920
+
921
+ unregisterPendingLock(store: any, keyId: unknown): void {
922
+ this.pendingLocks?.get(store)?.delete(keyId);
923
+ }
924
+
925
+ /** Release every transaction-scoped lock() handle this link owns. */
926
+ releaseRecordLocks(): void {
927
+ const recordLocks = this.recordLocks;
928
+ if (!recordLocks) return;
929
+ for (const [store, storeMap] of recordLocks) {
930
+ for (const [keyId, handle] of storeMap) {
931
+ if (handle.hold) continue; // hold handles outlive the transaction; released by unlock()
932
+ handle.release();
933
+ storeMap.delete(keyId);
934
+ }
935
+ if (storeMap.size === 0) recordLocks.delete(store);
936
+ }
937
+ if (recordLocks.size === 0) this.recordLocks = undefined;
938
+ }
939
+
940
+ private noteCommittedLockVersions(): void {
941
+ const recordLocks = this.recordLocks;
942
+ if (!recordLocks) return;
943
+ for (const write of this.writes) {
944
+ if (write?.appliedRecordVersion == null) continue;
945
+ const handle = write.lockHandle ?? recordLocks.get(write.store)?.get(writeKeyId(write.key));
946
+ if (handle && !handle.released) handle.noteHolderVersion(write.appliedRecordVersion);
947
+ }
948
+ }
949
+
715
950
  /**
716
951
  * Discard the staged write set (committed or aborted); the per-key chain must go with it so a
717
952
  * reused transaction never bases a write on a previous batch's staged state.
@@ -721,9 +956,16 @@ export class DatabaseTransaction implements Transaction {
721
956
  // fire after a commit or abort, and routing it back here would revive a write this transaction
722
957
  // already rolled back — whose blobs abort() has reclaimed. Cleared here, save() resolves the
723
958
  // context's current transaction as it did before `stagedIn` existed.
724
- for (const write of this.writes) if (write?.stagedIn === this) write.stagedIn = undefined;
959
+ for (const write of this.writes) {
960
+ if (write?.stagedIn === this) write.stagedIn = undefined;
961
+ if (write) this.ownedWrites?.delete(write);
962
+ }
725
963
  this.writes = [];
726
964
  this.writesByKey = undefined;
965
+ // The commit-fence latch belongs to the discarded batch. A reused transaction that once staged
966
+ // a locked write would otherwise re-scan every write of every later unlocked batch. Assigned
967
+ // only when it was set, so an ordinary transaction never gains the property at all.
968
+ if (this.hasLeaseProtectedWrite) this.hasLeaseProtectedWrite = false;
727
969
  }
728
970
 
729
971
  /**
@@ -771,26 +1013,28 @@ export class DatabaseTransaction implements Transaction {
771
1013
  // original per-request log was fixed to avoid, just shifted from per-request to per-commit.
772
1014
  // A commit skipped by the cooldown is NOT marked `logged`, so it still gets a log later if
773
1015
  // it's still the oldest once the cooldown clears, rather than going silent forever.
774
- if (!oldestOutstandingCommit.logged && now - lastOverloadLogAt > OVERLOAD_LOG_MIN_INTERVAL_MS) {
1016
+ if (!oldestOutstandingCommit.logged && allowStuckCommitLog('shed', now)) {
775
1017
  // Log once per stuck commit (not once per rejected request, harper#2001): a wedged
776
1018
  // thread otherwise logs nothing at all server-side while rejecting every write with a
777
1019
  // 503, which was the single biggest obstacle to root-causing a recurrence. The flag lives
778
1020
  // on the node itself, so if THIS commit settles while still over the limit and a
779
1021
  // different one is now oldest, that one logs too instead of staying silent forever.
780
1022
  oldestOutstandingCommit.logged = true;
781
- lastOverloadLogAt = now;
782
- const nativeTransactionId = oldestOutstandingCommit.nativeTransaction?.id;
783
- const store = oldestOutstandingCommit.store;
784
- const startedFrom = oldestOutstandingCommit.startedFrom;
785
1023
  harperLogger.error(
786
1024
  `Rejecting writes on this thread: a commit has been outstanding for ` +
787
1025
  `${Math.round(now - oldestOutstandingCommit.start)}ms (exceeds the ` +
788
- `${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${store?.rootStore?.databaseName ?? '?'}.${store?.name ?? '?'}` +
789
- (nativeTransactionId !== undefined ? ` (transaction ${nativeTransactionId})` : '') +
790
- (startedFrom?.resourceName
791
- ? `, started from ${startedFrom.resourceName}${startedFrom.method ? '.' + startedFrom.method : ''}`
792
- : '') +
793
- `. Further record updates and publishes from new application requests on this thread ` +
1026
+ `${MAX_OUTSTANDING_TXN_DURATION}ms limit), ` +
1027
+ describeCommitIdentity(
1028
+ oldestOutstandingCommit.store,
1029
+ oldestOutstandingCommit.startedFrom,
1030
+ oldestOutstandingCommit.nativeTransaction
1031
+ ) +
1032
+ `.` +
1033
+ describeHolderCandidates(
1034
+ oldestOutstandingCommit.store?.rootStore?.path,
1035
+ oldestOutstandingCommit.nativeTransaction?.id
1036
+ ) +
1037
+ ` Further record updates and publishes from new application requests on this thread ` +
794
1038
  `will be rejected with 503 until the commit settles or the process is restarted (deletes, ` +
795
1039
  `and writes applied from a canonical source, e.g. replication or a caching source, bypass this check).`
796
1040
  );
@@ -854,8 +1098,10 @@ export class DatabaseTransaction implements Transaction {
854
1098
  // Independent write-recency signal for chainStillActive (see the field comment) — reads never
855
1099
  // touch this, only writes do.
856
1100
  this.writeTimeout = this.timeout;
1101
+ this.writeTick = monitorTick;
857
1102
  this.linkWrite(operation);
858
1103
  this.writes.push(operation);
1104
+ (this.ownedWrites ??= new WeakSet()).add(operation);
859
1105
  operation.stagedIn = this;
860
1106
  // Hold this write back while any earlier same-key write has not run — out of staging order both
861
1107
  // diff against the pre-transaction record (harper#2211, DESIGN.md). The whole chain, not just the
@@ -884,7 +1130,43 @@ export class DatabaseTransaction implements Transaction {
884
1130
  }
885
1131
 
886
1132
  save(operation: TransactionWrite, transaction?: RocksTransaction, reloadEntry = false, options?: CommitOptions) {
887
- let txnTime = this.timestamp;
1133
+ const lockHandle = operation.lockHandle;
1134
+ // Guard: a write staged through an expired or released lock handle must not land.
1135
+ // The handle's lease timer already unlocked the native key; another holder may have taken it.
1136
+ // isExpired() re-evaluates the deadline here rather than trusting the timer to have run: a
1137
+ // holder whose event loop stalled past its lease would otherwise commit in the window between
1138
+ // the deadline and its own timer callback, after peers had already granted the key onward.
1139
+ //
1140
+ // A RE-save is judged by the lease alone. `released` belongs to the first stage — a caller that
1141
+ // unlocked and then saved is writing through a handle it gave back — but an operation already
1142
+ // staged was staged while the lock was held, and an `unlock()` inside the lease leaves it valid
1143
+ // (the same rule the pre-submit fence applies below). Without the distinction,
1144
+ // `lock(); save(); unlock(); commit()` succeeded normally and threw 409 only when a conflict
1145
+ // retry or an open read iterator forced the replay to re-save it.
1146
+ if (operation.saved ? lockHandle?.isLeaseExpired() : lockHandle?.isExpired()) {
1147
+ // Through `detachWrite`, which also repairs the per-key chain: dropping it from `writes` alone
1148
+ // leaves `writesByKey` pointing at a write that was refused, so a caller that catches this 409
1149
+ // and stages the same key again would take the rejected operation as its merge basis.
1150
+ this.detachWrite(operation);
1151
+ throw lockNotHeldError(lockHandle);
1152
+ }
1153
+ // Lock-write timestamp rules.
1154
+ if (lockHandle) {
1155
+ this.hasLeaseProtectedWrite = true;
1156
+ if (this.open === TRANSACTION_STATE.CLOSED || this.saveCommits) {
1157
+ // CLOSED path (second+ write per ImmediateTransaction cycle) OR the first write in
1158
+ // an ImmediateTransaction (open=OPEN until commit sets it CLOSED, but saveCommits
1159
+ // signals the per-write-commit semantics): stamp with nextHolderVersion() so that
1160
+ // each sequential save() gets its own monotonically increasing stamp — scoped or hold.
1161
+ // The stamp stays on the operation and is consumed below rather than assigned to
1162
+ // this.timestamp: pinning the link's clock would stamp every OTHER write staged on
1163
+ // the same context before the commit resets it — a concurrent write in the caller's
1164
+ // own Promise.all, or the next operation in a retry/replay save loop — with the
1165
+ // lock's version, which LWW then silently drops against a newer record version.
1166
+ if (!operation.lockStamp) operation.lockStamp = lockHandle.holderVersionCandidate();
1167
+ }
1168
+ }
1169
+ let txnTime = operation.lockStamp ?? this.timestamp;
888
1170
  // Only an OPEN transaction accepts new staged writes. After commit, this.transaction may still
889
1171
  // be retained for outstanding read iterators; staging into it would silently discard the write
890
1172
  // when doneReadTxn() aborts the handle, so such writes commit immediately on a fresh
@@ -929,22 +1211,59 @@ export class DatabaseTransaction implements Transaction {
929
1211
  (transaction as RocksTransactionWithRetry).isRetry = true;
930
1212
  }
931
1213
  if (!txnTime) txnTime = this.timestamp = transaction.getTimestamp();
932
- if (reloadEntry || operation.entry === undefined) {
933
- operation.entry = operation.store.getEntry(operation.key, { transaction });
1214
+ if (!operation.saved && operation.pendingPriorWrite) {
1215
+ const pendingWrites = [];
1216
+ for (let pending = operation.pendingPriorWrite; pending;) {
1217
+ if (!pending.saved && this.ownedWrites?.has(pending)) pendingWrites.push(pending);
1218
+ pending = pending.pendingPriorWrite !== undefined ? pending.pendingPriorWrite : pending.priorWrite;
1219
+ }
1220
+ for (let index = pendingWrites.length - 1; index >= 0; index--)
1221
+ this.save(pendingWrites[index], transaction, false, options);
1222
+ operation.pendingPriorWrite = null;
1223
+ }
1224
+ // `txnTime` is this transaction's timestamp — the key its entries take in the per-origin log.
1225
+ // A write applied from elsewhere carries the origin's record version too, and that is what the
1226
+ // record is stored at; the two coincide for every locally-originated write. Gated on the apply
1227
+ // flags so an ordinary write never reads the property (harper#2412).
1228
+ const writeVersion =
1229
+ this.sourceApply || this.isReplay ? getAppliedWriteVersion(operation.recordVersion, txnTime) : txnTime;
1230
+ // A base that feeds stored state must come from this transaction's snapshot, never the
1231
+ // cross-worker cache vouch (stale when a resequenced write reused a version). That closes the
1232
+ // lost-update window only when this transaction holds a snapshot to validate the later Put
1233
+ // against; a snapshot-free transaction (this.snapshotFree, after a mid-scope-commit rotation)
1234
+ // has no snapshot for rocksdb-js to validate the Put against, so it only narrows the window to
1235
+ // the read-to-put span rather than closing it (open follow-up, tracked in the PR description).
1236
+ // Replays keep their pre-read base — their convergence contract is the replay pass itself.
1237
+ const reloadsCommitBase = operation.reloadCommitBase && !operation.saved && !this.isReplay;
1238
+ if (reloadEntry || operation.entry === undefined || reloadsCommitBase) {
1239
+ const uncachedRead = (!!operation.reloadCommitBase && !this.isReplay) || reloadEntry;
1240
+ operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
934
1241
  }
935
1242
  if (!operation.saved) {
936
- operation.saved = true;
937
1243
  // immediately execute in this transaction
938
- if ((operation.validate?.(txnTime, this) as any) === false) {
1244
+ const validated = validateWrite(operation, writeVersion, this);
1245
+ if ((validated as any) === false) {
1246
+ operation.saved = true;
939
1247
  operation.commit = () => {}; // noop if we try again
1248
+ closeWriteInstance(operation);
940
1249
  return;
941
1250
  }
1251
+ operation.saved = true;
942
1252
  let result: Promise<void> = operation.before?.() as Promise<void>;
943
1253
  if (result?.then) this.stageCompletion(result);
944
1254
  result = operation.beforeIntermediate?.() as Promise<void>;
945
1255
  if (result?.then) this.stageCompletion(result);
946
1256
  }
947
- const completion = operation.commit(txnTime, operation.entry, this.retries > 0, transaction) as Promise<void>;
1257
+ if (lockHandle || this.recordLocks) operation.trackRecordVersion = true;
1258
+ if (operation.trackRecordVersion) operation.recordVersionApplied = false;
1259
+ let completion: Promise<void>;
1260
+ try {
1261
+ completion = operation.commit(writeVersion, operation.entry, this.retries > 0, transaction) as Promise<void>;
1262
+ } finally {
1263
+ closeWriteInstance(operation);
1264
+ }
1265
+ if (operation.trackRecordVersion)
1266
+ operation.appliedRecordVersion = operation.recordVersionApplied ? writeVersion : undefined;
948
1267
  if (typeof completion?.then === 'function') this.stageCompletion(completion);
949
1268
  // Sticky record that THIS write staged with its audit entry appended (log entries batch on the
950
1269
  // native transaction and are durably written by its commit attempt — even a failed one — so
@@ -956,7 +1275,13 @@ export class DatabaseTransaction implements Transaction {
956
1275
  operation.appendedAuditEntry = true;
957
1276
  }
958
1277
  if (immediateCommit) {
959
- return this.commit({ ...options, transaction }); // immediately commit if the harper transaction is closed
1278
+ // immediately commit if the harper transaction is closed
1279
+ const innerCommit = this.commit({ ...options, transaction });
1280
+ // Expose on the operation so the lock-writable Table.save() path can await the real
1281
+ // native commit — without this the ImmediateTransaction outer commit resolves before
1282
+ // the native transaction.commit() settles (fire-and-forget from the if-branch).
1283
+ operation.innerCommit = innerCommit;
1284
+ return innerCommit;
960
1285
  }
961
1286
  }
962
1287
 
@@ -968,17 +1293,35 @@ export class DatabaseTransaction implements Transaction {
968
1293
  // reused across retries — the native layer resets it in place (fresh snapshot) on IsBusy/TryAgain —
969
1294
  // but reassigned to a fresh replay transaction when outstanding read iterators retain this.transaction
970
1295
  let transaction = options.transaction ?? this.transaction;
971
- for (let i = 0; i < this.writes.length; i++) {
972
- let operation = this.writes[i];
973
- if (!operation || (this.retries === 0 && operation.saved)) continue;
974
- this.save(operation, transaction, i < this.validated, options);
1296
+ try {
1297
+ for (let i = 0; i < this.writes.length; i++) {
1298
+ let operation = this.writes[i];
1299
+ if (!operation || (this.retries === 0 && operation.saved)) continue;
1300
+ this.save(operation, transaction, i < this.validated, options);
1301
+ }
1302
+ } catch (error) {
1303
+ this.abort();
1304
+ throw error;
975
1305
  }
976
1306
  this.validated = this.writes.length;
977
1307
  const completions = this.completions;
978
1308
  if (completions.length > 0) this.completions = []; // reset
1309
+ const stagedWrites = this.writes.length;
1310
+ if (completions.length > 0) {
1311
+ this.setCommitPhase(true);
1312
+ }
979
1313
  return when(
980
1314
  completions.length > 0 ? Promise.all(completions) : null,
981
1315
  () => {
1316
+ if (completions.length > 0) this.setCommitPhase(false);
1317
+ // The transaction can be aborted underneath us while we are parked in the await above — by the
1318
+ // monitor once the commit phase outlives its grace, or through the multi-store poison chain.
1319
+ // abort() cleared the write set and released the handle, so resuming would commit nothing and
1320
+ // resolve as SUCCESS: the caller is told its write landed when it was dropped, and a write
1321
+ // carrying a blob is left holding an instance whose file was unlinked (issue #2062).
1322
+ if (this.timedOut) throw transactionOpenTooLongError();
1323
+ if (stagedWrites > 0 && this.writes.length === 0 && this.open === TRANSACTION_STATE.CLOSED)
1324
+ throw new ServerError('Transaction was aborted while its commit was waiting on pre-commit work', 500);
982
1325
  if (this.writes.length > this.validated) {
983
1326
  // check just in case we got any more transactions while we were waiting, if so just recursively continue to finish the additional writes now
984
1327
  return this.commit(options);
@@ -990,6 +1333,7 @@ export class DatabaseTransaction implements Transaction {
990
1333
  // the local is empty: a truthy one is what the loop staged into, and the retained-handle
991
1334
  // and replay branches below deliberately commit a handle other than this.transaction.
992
1335
  if (!transaction) transaction = this.transaction;
1336
+ if (!options.transaction && this.writes.some((write) => write)) this.renewChainForNativeCommit();
993
1337
  this.open = TRANSACTION_STATE.CLOSED;
994
1338
  // RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
995
1339
  // coordinatedRetry, or void on a normal commit/abort.
@@ -1077,6 +1421,28 @@ export class DatabaseTransaction implements Transaction {
1077
1421
  if (this.writes.length > 0) {
1078
1422
  // Commit retries can construct fresh ranges on this live handle after read ownership ends.
1079
1423
  readTransactionOwners.delete(transaction);
1424
+ // Re-fence before submitting: the loop above skips operations already marked saved, so
1425
+ // save()'s own check cannot see a holder that stalled between staging and commit. The
1426
+ // retry/replay path re-saves every operation and is fenced there instead. Lease expiry
1427
+ // only: an unlock() inside the lease leaves the staged write valid, an elapsed lease
1428
+ // does not, whether or not the caller also unlocked.
1429
+ for (let i = 0; this.hasLeaseProtectedWrite && i < this.writes.length; i++) {
1430
+ const lapsed = this.writes[i].lockHandle;
1431
+ if (!lapsed?.isLeaseExpired()) continue;
1432
+ try {
1433
+ transaction.abort();
1434
+ } catch {}
1435
+ // Every other terminal exit from commit() runs the logical cleanup too. Throwing
1436
+ // straight out would strand the OTHER locks this transaction holds, its staged
1437
+ // blobs, and the context's back-reference to a CLOSED transaction — and
1438
+ // transaction()'s onComplete has no rejection path to run it later.
1439
+ try {
1440
+ this.abort();
1441
+ } catch (error) {
1442
+ harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
1443
+ }
1444
+ throw lockNotHeldError(lapsed);
1445
+ }
1080
1446
  // The transaction was created with coordinatedRetry:true (see
1081
1447
  // getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
1082
1448
  // sentinel (a number) is why commitResolution is typed
@@ -1114,8 +1480,12 @@ export class DatabaseTransaction implements Transaction {
1114
1480
  // claimed this per-database transaction in txnForContext and so can name the wrong
1115
1481
  // table when a transaction spans more than one table in the same database.
1116
1482
  trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
1483
+ // Every retry round and every chained store re-enters here and must inherit the chain
1484
+ // root's clock rather than restart it, so only the first submission stamps.
1485
+ const chainRoot = this.root ?? this;
1486
+ if (chainRoot.commitStartedAt == null) chainRoot.commitStartedAt = performance.now();
1117
1487
  const completions = [];
1118
- return commitResolution.then(
1488
+ const commitOutcome = commitResolution.then(
1119
1489
  (commitResult) => {
1120
1490
  if (commitResult === RETRY_NOW_VALUE) {
1121
1491
  this.retries++;
@@ -1123,6 +1493,8 @@ export class DatabaseTransaction implements Transaction {
1123
1493
  // Mark this specific native transaction as a retry so RocksTransactionLogStore
1124
1494
  // skips re-writing its already-staged txn-log entries (#2).
1125
1495
  (transaction as RocksTransactionWithRetry).isRetry = true;
1496
+ const pastBudget = this.elapsedPastCommitBudget();
1497
+ if (pastBudget) this.abandonCommitAfterDeadline(transaction, pastBudget);
1126
1498
  // Mirror the ERR_BUSY cap/warn policy: non-sourceApply transactions abort
1127
1499
  // at MAX_RETRIES; sourceApply transactions keep retrying with periodic warn.
1128
1500
  if (this.retries > MAX_RETRIES) {
@@ -1182,9 +1554,11 @@ export class DatabaseTransaction implements Transaction {
1182
1554
  if (write?.savedBlobs && (write.skipped || (write.superseded && !write.blobsAuditReferenced)))
1183
1555
  cleanupUnusedBlobs(write.savedBlobs, collectRetainedFileIds(write.store.getEntry(write.key)?.value));
1184
1556
  }
1557
+ if (this.recordLocks) this.noteCommittedLockVersions();
1185
1558
  // now reset transactions tracking; this transaction be reused and committed again
1186
1559
  this.retries = 0; // reset per-native-transaction retry counter so a reused DatabaseTransaction's next batch starts fresh
1187
1560
  this.clearWrites();
1561
+ this.releaseRecordLocks();
1188
1562
  if (options.doneWriting) this.endScopeOwnership();
1189
1563
  this.releaseContext(!!options.doneWriting);
1190
1564
  let txnTime = this.timestamp;
@@ -1239,6 +1613,10 @@ export class DatabaseTransaction implements Transaction {
1239
1613
  // premature publish, and no fresh-transaction replay that would drop the entry.
1240
1614
  // Mark the native transaction as a retry so RocksTransactionLogStore skips re-staging entries.
1241
1615
  (transaction as RocksTransactionWithRetry).isRetry = true;
1616
+ // Before the backoff gate below: the budget can already be spent on the first
1617
+ // retry when an earlier store in this chain consumed it.
1618
+ const pastBudget = this.elapsedPastCommitBudget();
1619
+ if (pastBudget) this.abandonCommitAfterDeadline(transaction, pastBudget);
1242
1620
  if (this.retries > 2) {
1243
1621
  // Transactions applying data from a canonical source of truth (replication peer or
1244
1622
  // external caching source) must never drop a write on a transient conflict: there is no
@@ -1287,24 +1665,45 @@ export class DatabaseTransaction implements Transaction {
1287
1665
  // finished and its durability is unknown, so ownership goes with it.
1288
1666
  this.endScopeOwnership();
1289
1667
  this.releaseContext(!!options.doneWriting);
1668
+ this.releaseRecordLocks();
1669
+ this.timestamp = 0;
1290
1670
  throw error;
1291
1671
  }
1292
1672
  }
1293
1673
  );
1674
+ // `commitOutcome` settles when the LOGICAL commit ends — its success branch awaits the
1675
+ // chained stores' own commits — so releasing here covers every terminal exit (success,
1676
+ // retry exhaustion, abandonment, terminal failure) in one place rather than five.
1677
+ // Released through the RETURNED promise rather than a second subscriber on
1678
+ // `commitOutcome`: a subscriber would mark a dropped commit rejection as handled and
1679
+ // silence the unhandled-rejection that surfaces it.
1680
+ return commitOutcome.then(
1681
+ (resolution) => {
1682
+ chainRoot.commitStartedAt = undefined;
1683
+ return resolution;
1684
+ },
1685
+ (error) => {
1686
+ chainRoot.commitStartedAt = undefined;
1687
+ throw error;
1688
+ }
1689
+ );
1294
1690
  }
1295
1691
  for (const write of this.writes) {
1296
1692
  if (write?.savedBlobs && (write.skipped || (write.superseded && !write.blobsAuditReferenced)))
1297
1693
  cleanupUnusedBlobs(write.savedBlobs, collectRetainedFileIds(write.store.getEntry(write.key)?.value));
1298
1694
  }
1695
+ if (this.recordLocks) this.noteCommittedLockVersions();
1299
1696
  this.clearWrites();
1697
+ this.releaseRecordLocks();
1300
1698
  if (options.doneWriting) this.endScopeOwnership();
1301
1699
  this.releaseContext(!!options.doneWriting);
1302
1700
  const txnResolution: CommitResolution = {
1303
1701
  txnTime: this.timestamp,
1304
1702
  };
1703
+ this.timestamp = 0; // reset like the async path (~1279) so stale lock stamps don't persist
1305
1704
  if (this.next) {
1306
1705
  // now run any other transactions
1307
- options.timestamp = this.timestamp;
1706
+ options.timestamp = txnResolution.txnTime;
1308
1707
  // as above: the next store must not inherit this store's explicit native transaction
1309
1708
  let nextResolution;
1310
1709
  try {
@@ -1323,7 +1722,7 @@ export class DatabaseTransaction implements Transaction {
1323
1722
  // scope resumable after a partially failed mid-scope commit.
1324
1723
  this.completeMidScopeCommit(options);
1325
1724
  return {
1326
- txnTime: this.timestamp,
1725
+ txnTime: txnResolution.txnTime,
1327
1726
  next: nextResolution,
1328
1727
  };
1329
1728
  },
@@ -1342,6 +1741,7 @@ export class DatabaseTransaction implements Transaction {
1342
1741
  return txnResolution;
1343
1742
  },
1344
1743
  (error) => {
1744
+ this.setCommitPhase(false);
1345
1745
  this.abort();
1346
1746
  throw error;
1347
1747
  }
@@ -1390,6 +1790,7 @@ export class DatabaseTransaction implements Transaction {
1390
1790
  // Defensively release any native handle whose reference bookkeeping was already consumed.
1391
1791
  if (this.transaction) this.releaseReadTxn();
1392
1792
  this.open = TRANSACTION_STATE.CLOSED;
1793
+ this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
1393
1794
  this.drainCompletions();
1394
1795
  try {
1395
1796
  for (const write of this.writes) {
@@ -1399,6 +1800,7 @@ export class DatabaseTransaction implements Transaction {
1399
1800
  } finally {
1400
1801
  this.endScopeOwnership(); // the scope is over; nothing may rotate this instance again
1401
1802
  this.clearWrites();
1803
+ this.releaseRecordLocks();
1402
1804
  // A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
1403
1805
  // Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
1404
1806
  // starting a fresh one) so the rest of the logical operation fails atomically via the
@@ -1417,6 +1819,62 @@ export class DatabaseTransaction implements Transaction {
1417
1819
  }
1418
1820
  }
1419
1821
  }
1822
+ /** How long this logical commit may keep retrying: the thread-wide queue limit, or its own larger explicit budget. */
1823
+ private commitConflictBudget(): number {
1824
+ return Math.max(MAX_OUTSTANDING_TXN_DURATION, (this.root ?? this).timeoutBudget || 0);
1825
+ }
1826
+
1827
+ /**
1828
+ * How long this logical commit has been retrying once it is past its budget, else 0. rocksdb-js
1829
+ * returns control from a parked commit every `ROCKSDB_JS_PARK_TIMEOUT_MS` even when the intent
1830
+ * holder never releases, so without this bound a request-path commit retries the attempt cap out
1831
+ * — minutes past the queue limit an operator configured (issue #2450).
1832
+ *
1833
+ * Source-applied writes are exempt for the same reason they are exempt from the attempt cap:
1834
+ * there is no resubscribe/sequence-resume path, so dropping one permanently diverges this node.
1835
+ */
1836
+ private elapsedPastCommitBudget(): number {
1837
+ if (this.sourceApply) return 0;
1838
+ const startedAt = (this.root ?? this).commitStartedAt;
1839
+ if (startedAt == null) return 0;
1840
+ const elapsed = performance.now() - startedAt;
1841
+ return elapsed > this.commitConflictBudget() ? elapsed : 0;
1842
+ }
1843
+
1844
+ /**
1845
+ * Abandon a logical commit that stayed in write-intent conflict past its budget. Cleanup is
1846
+ * retry exhaustion's, so no link leaks a native handle or read snapshot; the error is distinct
1847
+ * because the condition is distinct — every attempt reported transient contention, so a later
1848
+ * request can succeed once the holder releases, which the generic exhaustion 500 does not say.
1849
+ *
1850
+ * Only a chain root may report `retryable`: a link commits solely from its predecessor's success
1851
+ * handler, so anywhere else in the chain an earlier store has already landed durable audit
1852
+ * entries and hooks that a replayed request would run twice. A head whose scope already rotated
1853
+ * through a mid-scope commit is in the same position.
1854
+ */
1855
+ private abandonCommitAfterDeadline(headTransaction: RocksTransaction, elapsedMs: number): never {
1856
+ const elapsed = Math.round(elapsedMs);
1857
+ const budget = this.commitConflictBudget();
1858
+ const retryable = !this.root && !this.snapshotFree;
1859
+ if (allowStuckCommitLog('abandon', performance.now())) {
1860
+ harperLogger.error(
1861
+ `Abandoning a write transaction: its commit has been in write-intent conflict for ${elapsed}ms ` +
1862
+ `(exceeds the ${budget}ms limit) across ${this.retries} retries, ` +
1863
+ describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
1864
+ `.` +
1865
+ describeHolderCandidates(this.writes[0]?.store?.rootStore?.path, headTransaction?.id) +
1866
+ ` Another transaction holds a conflicting write intent and has not completed; the request is ` +
1867
+ `failed with a 503${retryable ? '' : ' (not retryable — an earlier store in this transaction already committed)'} ` +
1868
+ `rather than waiting further.`
1869
+ );
1870
+ }
1871
+ this.abortChainAfterRetries(headTransaction);
1872
+ throw new TransactionCommitConflictTimeoutError(
1873
+ `Commit was in conflict with ongoing writes for ${elapsed}ms, exceeding the ${budget}ms limit; transaction abandoned after ${this.retries} retries`,
1874
+ retryable
1875
+ );
1876
+ }
1877
+
1420
1878
  /**
1421
1879
  * Give up on a chain of linked transactions after exhausting conflict retries: poison every link
1422
1880
  * first, then abort each link's native transaction and release its DatabaseTransaction-level
@@ -1506,11 +1964,7 @@ export class DatabaseTransaction implements Transaction {
1506
1964
  // throws. abort() rather than the native abort alone: it reclaims blobs a replayed write
1507
1965
  // staged.
1508
1966
  this.detachOwnedTransaction();
1509
- try {
1510
- transaction?.abort();
1511
- } catch (abortError) {
1512
- harperLogger.debug?.('aborting a transaction whose synchronous commit failed', abortError);
1513
- }
1967
+ abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed');
1514
1968
  try {
1515
1969
  this.abort();
1516
1970
  } catch (abortError) {
@@ -1537,6 +1991,20 @@ export interface Transaction {
1537
1991
  abort?(): any;
1538
1992
  }
1539
1993
 
1994
+ export function shouldSpareCommitPhase(
1995
+ txn: DatabaseTransaction,
1996
+ checkedCommitPhaseChains: Set<DatabaseTransaction>
1997
+ ): boolean {
1998
+ if (!txn.committing) return false;
1999
+ if (txn.sourceApply || txn.isReplay) return true;
2000
+ const commitChainHead = txn.commitChainHead ?? txn;
2001
+ if (!checkedCommitPhaseChains.has(commitChainHead)) {
2002
+ checkedCommitPhaseChains.add(commitChainHead);
2003
+ commitChainHead.commitPhaseTicks++;
2004
+ }
2005
+ return commitChainHead.commitPhaseTicks <= COMMIT_PHASE_GRACE;
2006
+ }
2007
+
1540
2008
  export class ImmediateTransaction extends DatabaseTransaction {
1541
2009
  isCommitting = false;
1542
2010
  saveCommits = true;
@@ -1545,16 +2013,47 @@ export class ImmediateTransaction extends DatabaseTransaction {
1545
2013
  this.db = db;
1546
2014
  }
1547
2015
  save(...args: any[]): any {
1548
- const transaction = args[0];
2016
+ const operation = args[0]; // the staged write, not a transaction — commit() re-enters here with it
1549
2017
  if (this.isCommitting) {
1550
2018
  // if we are in the commit, do the save and force a reload so we get a read within the transaction
1551
- super.save(transaction, null as any, true);
2019
+ super.save(operation, null as any, true);
1552
2020
  } else {
1553
2021
  this.isCommitting = true;
1554
- return when(this.commit(), () => {
2022
+ // A synchronous throw from commit() (e.g. a 409 from an expired lock handle) would
2023
+ // otherwise leave isCommitting latched at true, causing every subsequent save() in this
2024
+ // context to take the fire-and-forget if-branch and silently drop writes.
2025
+ let commitResult: any;
2026
+ try {
2027
+ commitResult = this.commit();
2028
+ } catch (err) {
1555
2029
  this.isCommitting = false;
1556
- });
2030
+ throw err;
2031
+ }
2032
+ return when(
2033
+ commitResult,
2034
+ () => {
2035
+ this.isCommitting = false;
2036
+ },
2037
+ (err: any) => {
2038
+ // Async rejection (e.g. a rejected rocksdb commit promise) must also clear the
2039
+ // latch; without this, every subsequent save() silently fire-and-forgets.
2040
+ this.isCommitting = false;
2041
+ throw err;
2042
+ }
2043
+ );
2044
+ }
2045
+ }
2046
+
2047
+ // Without an explicit transaction() a live lock is released only by unlock() or its lease, never
2048
+ // by the per-write commit. Expired handles are pruned so a reused context does not retain every key.
2049
+ releaseRecordLocks(): void {
2050
+ const recordLocks = this.recordLocks;
2051
+ if (!recordLocks) return;
2052
+ for (const [store, storeMap] of recordLocks) {
2053
+ for (const [keyId, handle] of storeMap) if (handle.released) storeMap.delete(keyId);
2054
+ if (storeMap.size === 0) recordLocks.delete(store);
1557
2055
  }
2056
+ if (recordLocks.size === 0) this.recordLocks = undefined;
1558
2057
  }
1559
2058
 
1560
2059
  declare _timestamp: number;
@@ -1635,6 +2134,7 @@ export function isJoinableScope(transaction: DatabaseTransaction | null | undefi
1635
2134
  }
1636
2135
 
1637
2136
  let timer;
2137
+ let monitorTick = 0;
1638
2138
 
1639
2139
  /**
1640
2140
  * True when a link other than `txn` in the same multi-store chain was written recently enough to
@@ -1654,20 +2154,100 @@ function chainStillActive(txn: DatabaseTransaction): boolean {
1654
2154
  return false;
1655
2155
  }
1656
2156
 
2157
+ /**
2158
+ * Name a chain link still holding its native handle past the reporting threshold, and why the
2159
+ * branches below are not reaping it (harper#2471). Attribution only: it reads the recency clocks
2160
+ * those branches maintain rather than calling `chainStillActive`, which would decay `writeTimeout`
2161
+ * as a side effect, and it runs before them so it cannot reorder them.
2162
+ */
2163
+ function reportLongLivedLink(
2164
+ link: DatabaseTransaction,
2165
+ thresholdMs: number,
2166
+ now: number,
2167
+ reportBudget: LongLivedHolderReportBudget
2168
+ ): void {
2169
+ if (!link.transaction || link.handleOpenedAt === 0) return;
2170
+ const ageMs = now - link.handleOpenedAt;
2171
+ if (ageMs < thresholdMs) return;
2172
+ const states: string[] = [];
2173
+ if (link.sourceApply) states.push('source-apply');
2174
+ if (link.isReplay) states.push('replay');
2175
+ if (link.committing) states.push('commit-phase');
2176
+ if (link.open === TRANSACTION_STATE.CLOSED) states.push('closed-with-open-iterators');
2177
+ if (monitorTick - link.writeTick <= 1) states.push('active');
2178
+ if (states.length === 0) states.push('over-limit');
2179
+ reportLongLivedHolder(
2180
+ {
2181
+ databasePath: (link.db as any)?.rootStore?.path,
2182
+ nativeId: link.transaction.id,
2183
+ openedAt: link.handleOpenedAt,
2184
+ ageMs,
2185
+ databaseName: (link.db as any)?.rootStore?.databaseName,
2186
+ tableName: (link.db as any)?.name,
2187
+ countPendingWrites: () => {
2188
+ let pendingWrites = 0;
2189
+ for (const write of link.writes) if (write) pendingWrites++;
2190
+ return pendingWrites;
2191
+ },
2192
+ states,
2193
+ timeoutBudget: link.timeoutBudget,
2194
+ startedFrom: link.startedFrom,
2195
+ },
2196
+ reportBudget
2197
+ );
2198
+ }
2199
+
2200
+ /**
2201
+ * Report every link of this logical transaction that holds its own native handle. A blind write to a
2202
+ * second database attaches a handle to a `.next` link while only the root is supervised, so reporting
2203
+ * the entry alone names an id the sweep's line can never be joined to when the child is the holder.
2204
+ * Links that read are in `trackedTxns` and get their own visit from the tick.
2205
+ */
2206
+ function reportIfLongLived(
2207
+ txn: DatabaseTransaction,
2208
+ thresholdMs: number,
2209
+ now: number,
2210
+ reportBudget: LongLivedHolderReportBudget
2211
+ ): void {
2212
+ if (thresholdMs === 0) return;
2213
+ for (let link: DatabaseTransaction = txn; link; link = link.next) {
2214
+ if (link !== txn && trackedTxns.has(link)) break;
2215
+ reportLongLivedLink(link, thresholdMs, now, reportBudget);
2216
+ }
2217
+ }
2218
+
1657
2219
  function startMonitoringTxns() {
1658
2220
  timer = setInterval(function () {
2221
+ monitorTick++;
2222
+ const checkedCommitPhaseChains = new Set<DatabaseTransaction>();
2223
+ const reportThresholdMs = getReportThresholdMs();
2224
+ rebaseLongLivedTransactionReports(reportThresholdMs);
2225
+ const reportBudget = createLongLivedHolderReportBudget(reportThresholdMs);
2226
+ const reportNow = performance.now();
1659
2227
  // Both registries, in sequence rather than as a union: a root can be in each (it read, and a
1660
2228
  // later link blind-wrote), and the membership check is cheaper than allocating per tick.
1661
- for (const txn of trackedTxns) monitorTransaction(txn);
1662
- for (const txn of supervisedWriteRoots) if (!trackedTxns.has(txn)) monitorTransaction(txn);
2229
+ for (const txn of trackedTxns)
2230
+ monitorTransaction(txn, checkedCommitPhaseChains, reportThresholdMs, reportNow, reportBudget);
2231
+ for (const txn of supervisedWriteRoots)
2232
+ if (!trackedTxns.has(txn))
2233
+ monitorTransaction(txn, checkedCommitPhaseChains, reportThresholdMs, reportNow, reportBudget);
2234
+ finishLongLivedHolderReports(reportBudget);
1663
2235
  }, txnExpiration).unref();
1664
2236
 
1665
- function monitorTransaction(txn: DatabaseTransaction) {
2237
+ function monitorTransaction(
2238
+ txn: DatabaseTransaction,
2239
+ checkedCommitPhaseChains: Set<DatabaseTransaction>,
2240
+ reportThresholdMs: number,
2241
+ reportNow: number,
2242
+ reportBudget: LongLivedHolderReportBudget
2243
+ ) {
1666
2244
  if (txn.rangeReadActive) {
1667
2245
  txn.rangeReadActive = false;
1668
2246
  txn.renewReadTimeout();
1669
2247
  }
2248
+ reportIfLongLived(txn, reportThresholdMs, reportNow, reportBudget);
1670
2249
  {
2250
+ const commitChainHead = txn.commitChainHead ?? txn;
1671
2251
  // Decay write recency once per tick for every tracked link, independent of the `timeout`
1672
2252
  // branches below — a tracked link that keeps its own idle limit alive by reading must not
1673
2253
  // thereby keep chainStillActive believing it was written recently too.
@@ -1695,6 +2275,25 @@ function startMonitoringTxns() {
1695
2275
  }`
1696
2276
  );
1697
2277
  txn.releaseReadTxn();
2278
+ } else if (shouldSpareCommitPhase(txn, checkedCommitPhaseChains)) {
2279
+ // Parked in commit()'s pre-commit await — a `before`/`beforeIntermediate` hook, in practice a
2280
+ // blob's durable file write, which for a multi-tens-of-MB payload legitimately outruns the
2281
+ // limit. The write set is sealed and the caller is awaiting this commit, so the limit's
2282
+ // premise (the application is holding a transaction open) does not hold, and poisoning here
2283
+ // would unlink the blobs the write still references (issue #2062). The grace is bounded
2284
+ // because the transaction still pins a read snapshot: a source that stalls rather than
2285
+ // finishing falls through to the abort below once it runs out. A canonical-source apply or
2286
+ // replay is spared for as long as it takes: neither aborting it (harper-pro#348) nor the
2287
+ // force-commit below — which would durably commit a record whose blob file is still being
2288
+ // written — is acceptable, and their blob sources are bounded by the receive-side idle
2289
+ // watchdog instead.
2290
+ harperLogger.warn?.(
2291
+ `Transaction has been in its commit phase past the open-transaction limit, waiting on pre-commit work (e.g. a large blob write); letting it complete, from table: ${
2292
+ (txn.db as any)?.name + (url ? ' path: ' + url : '')
2293
+ }`,
2294
+ ...(txn.startedFrom ? [`was started from ${txn.startedFrom.resourceName}.${txn.startedFrom.method}`] : [])
2295
+ );
2296
+ txn.timeout = Math.max(txnExpiration, txn.timeoutBudget ?? 0);
1698
2297
  } else if (txn.hasPendingWrites() && chainStillActive(txn)) {
1699
2298
  // A later link in the chain was written recently (writes re-arm only the link that
1700
2299
  // receives them, and a multi-store transaction can be writing database B while this
@@ -1718,7 +2317,7 @@ function startMonitoringTxns() {
1718
2317
  ...(DEBUG_LONG_TXNS ? ['starting stack trace', txn.stackTraces] : [])
1719
2318
  );
1720
2319
  try {
1721
- txn.abortDueToTimeout();
2320
+ commitChainHead.abortDueToTimeout();
1722
2321
  } catch (error) {
1723
2322
  harperLogger.debug?.(`Error aborting timed out transaction: ${error.message}`);
1724
2323
  }
@@ -1756,6 +2355,12 @@ export function resetReplayedWritesWarning() {
1756
2355
  replayedWritesWarned = false;
1757
2356
  }
1758
2357
 
2358
+ export function setMaxOutstandingTxnDuration(ms: number): number {
2359
+ const previous = MAX_OUTSTANDING_TXN_DURATION;
2360
+ MAX_OUTSTANDING_TXN_DURATION = ms;
2361
+ return previous;
2362
+ }
2363
+
1759
2364
  /** Test seam: whether the monitor supervises this logical transaction for its writes. */
1760
2365
  export function isWriteSupervised(txn: DatabaseTransaction): boolean {
1761
2366
  return supervisedWriteRoots.has(txn);