harper 5.3.2 → 5.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/bin/cliOperations.ts +3 -3
  2. package/bin/copyDb.ts +4 -3
  3. package/components/DESIGN.md +12 -0
  4. package/components/packageComponent.ts +42 -17
  5. package/config/DESIGN.md +4 -0
  6. package/config/configUtils.ts +8 -8
  7. package/config/parseConfigFile.ts +11 -7
  8. package/dist/bin/cliOperations.js +3 -3
  9. package/dist/bin/cliOperations.js.map +1 -1
  10. package/dist/bin/copyDb.js +3 -3
  11. package/dist/bin/copyDb.js.map +1 -1
  12. package/dist/components/packageComponent.d.ts +9 -7
  13. package/dist/components/packageComponent.js +41 -16
  14. package/dist/components/packageComponent.js.map +1 -1
  15. package/dist/config/configUtils.js +4 -3
  16. package/dist/config/configUtils.js.map +1 -1
  17. package/dist/config/parseConfigFile.d.ts +1 -0
  18. package/dist/config/parseConfigFile.js +7 -3
  19. package/dist/config/parseConfigFile.js.map +1 -1
  20. package/dist/resources/DatabaseTransaction.d.ts +80 -11
  21. package/dist/resources/DatabaseTransaction.js +1100 -281
  22. package/dist/resources/DatabaseTransaction.js.map +1 -1
  23. package/dist/resources/LMDBTransaction.d.ts +1 -1
  24. package/dist/resources/LMDBTransaction.js +4 -3
  25. package/dist/resources/LMDBTransaction.js.map +1 -1
  26. package/dist/resources/Resource.js +40 -14
  27. package/dist/resources/Resource.js.map +1 -1
  28. package/dist/resources/RocksTransactionLogStore.js +1 -0
  29. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  30. package/dist/resources/Table.js +97 -34
  31. package/dist/resources/Table.js.map +1 -1
  32. package/dist/resources/analytics/write.js +34 -16
  33. package/dist/resources/analytics/write.js.map +1 -1
  34. package/dist/resources/databases.d.ts +5 -0
  35. package/dist/resources/databases.js +38 -4
  36. package/dist/resources/databases.js.map +1 -1
  37. package/dist/resources/recordLock.d.ts +9 -4
  38. package/dist/resources/recordLock.js +79 -19
  39. package/dist/resources/recordLock.js.map +1 -1
  40. package/dist/server/DurableSubscriptionsSession.d.ts +2 -0
  41. package/dist/server/DurableSubscriptionsSession.js +24 -24
  42. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  43. package/dist/server/mqtt.js +59 -29
  44. package/dist/server/mqtt.js.map +1 -1
  45. package/dist/server/serverHelpers/NodeAdapterResponse.js +18 -0
  46. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -1
  47. package/dist/server/serverHelpers/contentTypes.js +8 -2
  48. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  49. package/dist/server/serverHelpers/sharedMessageEncoding.js +3 -2
  50. package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -1
  51. package/dist/utility/npmUtilities.js +18 -7
  52. package/dist/utility/npmUtilities.js.map +1 -1
  53. package/package.json +1 -1
  54. package/resources/DESIGN.md +46 -10
  55. package/resources/DatabaseTransaction.ts +1268 -279
  56. package/resources/LMDBTransaction.ts +4 -3
  57. package/resources/Resource.ts +39 -14
  58. package/resources/RocksTransactionLogStore.ts +1 -0
  59. package/resources/Table.ts +120 -41
  60. package/resources/analytics/DESIGN.md +17 -0
  61. package/resources/analytics/write.ts +28 -15
  62. package/resources/databases.ts +40 -4
  63. package/resources/record-locks.md +34 -17
  64. package/resources/recordLock.ts +89 -22
  65. package/server/DESIGN.md +9 -1
  66. package/server/DurableSubscriptionsSession.ts +22 -23
  67. package/server/mqtt.ts +54 -29
  68. package/server/serverHelpers/NodeAdapterResponse.ts +19 -1
  69. package/server/serverHelpers/contentTypes.ts +8 -1
  70. package/server/serverHelpers/sharedMessageEncoding.ts +3 -2
  71. package/studio/web/assets/Chat-DjZNWwwA.js +2100 -0
  72. package/studio/web/assets/FloatingChat-BWfKK_RX.js +23 -0
  73. package/studio/web/assets/SearchDialog-CdlNxWef.js +1 -0
  74. package/studio/web/assets/{abnfDiagram-O67JEVCF-raXE027J.js → abnfDiagram-YUKMZFIV-DPR2lFRk.js} +1 -1
  75. package/studio/web/assets/{apiToken-DNrfij0E.js → apiToken-DyI8IwU3.js} +1 -1
  76. package/studio/web/assets/applications-DgGopKJq.js +296 -0
  77. package/studio/web/assets/architecture-WOLXFQ4H-DtJHIK0_.js +1 -0
  78. package/studio/web/assets/{architectureDiagram-NJMV4G6O-C8_Eet7w.js → architectureDiagram-47P4ROYG-MDymp2dh.js} +3 -3
  79. package/studio/web/assets/authStore-DE1HFvVP.js +3 -0
  80. package/studio/web/assets/{blockDiagram-BEXU5L5S-DR6S5-1D.js → blockDiagram-E7TT5TSB-O5qRgZmd.js} +3 -3
  81. package/studio/web/assets/button-C37w80Uk.js +21 -0
  82. package/studio/web/assets/{c4Diagram-YGBWAQC7-BAfZ1G01.js → c4Diagram-CRA5TL53-BdPORGHo.js} +2 -2
  83. package/studio/web/assets/channel-yCStoq0_.js +1 -0
  84. package/studio/web/assets/{chevron-up-CFxbMGv2.js → chevron-up-DozwrIz-.js} +1 -1
  85. package/studio/web/assets/chunk-2BW5OAIV-DJ-iXJAw.js +10 -0
  86. package/studio/web/assets/{chunk-ZIGJFQKS-Cs8fUn7y.js → chunk-3YJQHVM4-BOgacqS1.js} +2 -2
  87. package/studio/web/assets/{chunk-7PRAP22T-KyqX6x3f.js → chunk-4EA7E6EY-C-7xqUBl.js} +1 -1
  88. package/studio/web/assets/{chunk-GWA4HPMP-DsejB6gA.js → chunk-7TKQ45FW-C3XA1bIN.js} +1 -1
  89. package/studio/web/assets/{chunk-GTNCS2PH-DGHLXyow.js → chunk-AW2ZBBNX-BdvgD1UW.js} +3 -3
  90. package/studio/web/assets/{chunk-5DYCD2WN-BceBfIHT.js → chunk-DBDB3WZW-CKKxIc6F.js} +1 -1
  91. package/studio/web/assets/{chunk-MBY4JIJT-BwSGweQP.js → chunk-E2ZNV5FY-bVefNHvz.js} +3 -3
  92. package/studio/web/assets/chunk-EU5HNXII-BHv2kWp4.js +213 -0
  93. package/studio/web/assets/chunk-FVRAUYC3-BoL_NO6b.js +2 -0
  94. package/studio/web/assets/{chunk-742MDFTN-CIkt8ijt.js → chunk-HJ2JQQFS-N_98oMRS.js} +1 -1
  95. package/studio/web/assets/{chunk-NETBCI7D-C8UnOw_f.js → chunk-HTAEGDNF-BQ1fSjPs.js} +1 -1
  96. package/studio/web/assets/{chunk-WEXAMYUT-BAIhTwyh.js → chunk-KQW6MTUR-qP0QiQAg.js} +1 -1
  97. package/studio/web/assets/chunk-NGNAAXSQ-DqoPQk1a.js +136 -0
  98. package/studio/web/assets/{chunk-UA2S7LBM-BHpEdH8D.js → chunk-NTY3LDVX-CSBFjlJg.js} +1 -1
  99. package/studio/web/assets/chunk-VPRB5NB3-BVHOe9Q-.js +129 -0
  100. package/studio/web/assets/{chunk-7INBJB4K-BpeAXyOi.js → chunk-XC4XBNZT-46uQwtF4.js} +10 -10
  101. package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DmDPkt2u.js → classDiagram-v2-K4WV4PDN-Da0ps1Ui.js} +3 -3
  102. package/studio/web/assets/cn-DpF1PxBT.js +1 -0
  103. package/studio/web/assets/{createLucideIcon-fO5dL7D5.js → createLucideIcon-DLWRoWN-.js} +1 -1
  104. package/studio/web/assets/{cssMode-DBucrQdT.js → cssMode-4ddO86ey.js} +1 -1
  105. package/studio/web/assets/cynefin-EF2NZ3EQ-jQ2eCUaU.js +1 -0
  106. package/studio/web/assets/cynefinDiagram-3GCD6N5R-TNi5iS0E.js +62 -0
  107. package/studio/web/assets/dagre-W4DXFKR2-BnLHmA44.js +4 -0
  108. package/studio/web/assets/{diagram-22UHCM2B-DJCvcaWP.js → diagram-2UJZ2QOL-r_jehhZR.js} +7 -7
  109. package/studio/web/assets/{diagram-3UASUU5V-Du6VKWln.js → diagram-OVF4WLC6-BSUXGVh8.js} +11 -11
  110. package/studio/web/assets/{diagram-CDSNMT55-l5Ga0u-W.js → diagram-PFPMY2P6-BrVedycw.js} +3 -3
  111. package/studio/web/assets/{diagram-MLGK6HIB-CNQY47LA.js → diagram-UMYRVEAY-CBIaakBB.js} +1 -1
  112. package/studio/web/assets/diagram-YEKJPTXX-DFmmDXCl.js +41 -0
  113. package/studio/web/assets/diagram-ZIFT7M5P-CQfOWxOi.js +3 -0
  114. package/studio/web/assets/dist-9lpzt2Hn.js +11 -0
  115. package/studio/web/assets/{ebnfDiagram-ZINNZB2B-5OnkTpfi.js → ebnfDiagram-VR2GEFS7-FXA3Xe6e.js} +1 -1
  116. package/studio/web/assets/{editor-CHpQCuBq.js → editor-DATuu7_v.js} +1 -1
  117. package/studio/web/assets/elk-IJKZMXRS-CC4gfVRU.js +27 -0
  118. package/studio/web/assets/{erDiagram-OPXOYQCR-CQlCX84D.js → erDiagram-O2IAPWRE-C-BkDTPl.js} +2 -2
  119. package/studio/web/assets/errorText-8xV8WZBt.js +1 -0
  120. package/studio/web/assets/eventmodeling-K75KTNOO-D0dJ_PbR.js +1 -0
  121. package/studio/web/assets/flowDiagram-OXPTDLAJ-C1LEc_Hf.js +1 -0
  122. package/studio/web/assets/{ganttDiagram-FUAMR5RP-bloX3Voy.js → ganttDiagram-R7TSEDQI-85F44BIo.js} +4 -4
  123. package/studio/web/assets/{getAnalytics-C0FHjtdQ.js → getAnalytics-B1xpd4kG.js} +1 -1
  124. package/studio/web/assets/gitGraph-VSP46ZUC-BBmqrmjK.js +1 -0
  125. package/studio/web/assets/gitGraphDiagram-XJZIOB7I-CFY6iMwg.js +106 -0
  126. package/studio/web/assets/graphlib-DS17s2tU.js +1 -0
  127. package/studio/web/assets/{html-B1iA8q38.js → html-c4OKnEF1.js} +1 -1
  128. package/studio/web/assets/{htmlMode-CrL3YO5Y.js → htmlMode-yXjqUsW9.js} +1 -1
  129. package/studio/web/assets/index-CDnn0-PN.css +1 -0
  130. package/studio/web/assets/index-CIK9Nhgo.js +833 -0
  131. package/studio/web/assets/index.lazy-DQpKL_QQ.js +1 -0
  132. package/studio/web/assets/info-OHQRW6UA-Bs2aEco7.js +1 -0
  133. package/studio/web/assets/{infoDiagram-VRGFBTTK-B0J1132p.js → infoDiagram-5W2HQ5XZ-d6yG1yfZ.js} +1 -1
  134. package/studio/web/assets/invalidateEntityQueries-BkWXiqjz.js +1 -0
  135. package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-BNk0tuQx.js → ishikawaDiagram-K3B6WC7H-CSYz-WRA.js} +2 -2
  136. package/studio/web/assets/{javascript-CXgQMkoa.js → javascript-CP1WPkLu.js} +1 -1
  137. package/studio/web/assets/{journeyDiagram-ZHPQQLJL-Be79RMQM.js → journeyDiagram-COXZFDF6-BDj6Wqcj.js} +3 -3
  138. package/studio/web/assets/{jsonMode-DycqRKOv.js → jsonMode-BWsIHqbb.js} +1 -1
  139. package/studio/web/assets/{kanban-definition-PNTS6WVX-4uuoQP9W.js → kanban-definition-P3RFRI5V-C2Bjfbp-.js} +6 -6
  140. package/studio/web/assets/{languageServices-HYHV1wDJ.js → languageServices-Ca72n4Ql.js} +1 -1
  141. package/studio/web/assets/{lspLanguageFeatures-CH_6SYiA.js → lspLanguageFeatures-BQdsSDeh.js} +1 -1
  142. package/studio/web/assets/{mermaid-parser.core-V1UYMeXV.js → mermaid-parser.core-Ch_T61iL.js} +6 -6
  143. package/studio/web/assets/mermaid.core-BOAgKzo5.js +44 -0
  144. package/studio/web/assets/{mindmap-definition-NLK3R4M7-C70Ctm0J.js → mindmap-definition-LZFPQGKD-DzOapzMM.js} +3 -3
  145. package/studio/web/assets/notifications-BYgO5-yk.js +1 -0
  146. package/studio/web/assets/notifications-DsGk-TnW.js +1 -0
  147. package/studio/web/assets/operations-Dy69AlTr.js +4 -0
  148. package/studio/web/assets/packet-JDAUHWVQ-CGHB_EIE.js +1 -0
  149. package/studio/web/assets/{pegDiagram-GJSIUBJH-d6z1cc7W.js → pegDiagram-BGZESJAR-ADzOIKrz.js} +1 -1
  150. package/studio/web/assets/pie-XZMESJXO-Bijz5Y2o.js +1 -0
  151. package/studio/web/assets/pieDiagram-CAPJLFHJ-VM7fvbwO.js +39 -0
  152. package/studio/web/assets/profile-DzQvoYBF.js +1 -0
  153. package/studio/web/assets/{quadrantDiagram-O4NWA36T-CIF5Qkou.js → quadrantDiagram-GDMTTRAM-Bmza84_j.js} +3 -3
  154. package/studio/web/assets/radar-ABXABTNO-IaC8pB63.js +1 -0
  155. package/studio/web/assets/railroad-I3PHUGI6-CCrSUqeq.js +1 -0
  156. package/studio/web/assets/railroad-abnf-LNEFI6M7-BhobVp-t.js +1 -0
  157. package/studio/web/assets/railroad-ebnf-DWYD2UWJ-OHhGUTEh.js +1 -0
  158. package/studio/web/assets/railroad-peg-JR7BVNR7-FupC3Jcn.js +1 -0
  159. package/studio/web/assets/{railroadDiagram-XR7U4H2S-y3Jv2U6N.js → railroadDiagram-SM67HX2B-CgFGOVI5.js} +1 -1
  160. package/studio/web/assets/regions-Sv_IN7XA.js +1 -0
  161. package/studio/web/assets/{register-DCN4QUii.js → register-AM2_79FO.js} +3 -3
  162. package/studio/web/assets/request-BJj8dbdU.js +2 -0
  163. package/studio/web/assets/{requirementDiagram-PLB6GJNP-ChINZEmu.js → requirementDiagram-X7JNWC4A-BHoQgkoT.js} +4 -4
  164. package/studio/web/assets/restartGate-D_e93lFE.js +1 -0
  165. package/studio/web/assets/restartUnconfirmed-BqcDZ__Y.js +1 -0
  166. package/studio/web/assets/sankeyDiagram-UM26HJRW-8tFu_oUe.js +40 -0
  167. package/studio/web/assets/sequenceDiagram-ZO4K6R2Y-jryb1rwT.js +169 -0
  168. package/studio/web/assets/setComponentFile-BCnAnxjR.js +1 -0
  169. package/studio/web/assets/setup-Ca4RpPzm.js +2 -0
  170. package/studio/web/assets/{stateDiagram-v2-GCMORJYK-Bs8RIb9K.js → stateDiagram-v2-TBUQTH76-CnWE5yFn.js} +2 -2
  171. package/studio/web/assets/status-D05eZ5pQ.js +61 -0
  172. package/studio/web/assets/{swimlanes-2SLR337P-B52xrf6t.js → swimlanes-N4OXWK64-CBlFzDJn.js} +1 -1
  173. package/studio/web/assets/swimlanesDiagram-K3J5GTZL-DvFLtUPi.js +8 -0
  174. package/studio/web/assets/{timeline-definition-EJHVYXUP-DJSITpQX.js → timeline-definition-YOQAKGHF-CFWqHrqI.js} +4 -4
  175. package/studio/web/assets/{toggleHighContrast-CfAp6Sxb.js → toggleHighContrast-JRkLfyfb.js} +1 -1
  176. package/studio/web/assets/treeView-D4JQ5SDB-CpV9zuVb.js +1 -0
  177. package/studio/web/assets/treemap-SAJKECNS-Bc94Tqiq.js +1 -0
  178. package/studio/web/assets/{tsMode-HZR0bfq4.js → tsMode-C_-eSRVR.js} +1 -1
  179. package/studio/web/assets/{typescript-UMJSfZyF.js → typescript-BYXigM_n.js} +1 -1
  180. package/studio/web/assets/{useEntityRestURL-B379CS-h.js → useEntityRestURL-HHmjsRm9.js} +1 -1
  181. package/studio/web/assets/usecaseDiagram-VIAY4XPW-Brf2xP3S.js +328 -0
  182. package/studio/web/assets/{vendor-core-6LhfJlRC.js → vendor-core-PejxhUWQ.js} +1 -1
  183. package/studio/web/assets/vendor-datadog-GjET_Cbn.js +6 -0
  184. package/studio/web/assets/{vendor-react-BOliS0BL.js → vendor-react-DSIx92Ld.js} +1 -1
  185. package/studio/web/assets/vendor-tanstack-C5Y-nrd2.js +2 -0
  186. package/studio/web/assets/vendor-ui-BBWHOEoQ.js +41 -0
  187. package/studio/web/assets/{vennDiagram-UO4OBE2U-DZTsDA4k.js → vennDiagram-BLWOH2XV-XxOyJPq_.js} +3 -3
  188. package/studio/web/assets/wardley-7MLQ67FV-DRW0zuE4.js +1 -0
  189. package/studio/web/assets/{wardleyDiagram-VNRHLVJA-qu_f7Tvl.js → wardleyDiagram-YMQ3BMBF-B3WskJ9W.js} +3 -3
  190. package/studio/web/assets/wasAReleasedBeforeB-D5grAaGM.js +1 -0
  191. package/studio/web/assets/{workers-Bn_0_L1R.js → workers-DY0ka4Y7.js} +1 -1
  192. package/studio/web/assets/{xml-CG2N-1r1.js → xml-CdGINpmN.js} +1 -1
  193. package/studio/web/assets/xychartDiagram-TAQBALBS-D5NV4Env.js +7 -0
  194. package/studio/web/assets/{yaml-68pda6Av.js → yaml-DYeF07_w.js} +1 -1
  195. package/studio/web/index.html +17 -15
  196. package/utility/npmUtilities.ts +17 -7
  197. package/studio/web/assets/Chat-CrxPD4ph.js +0 -2294
  198. package/studio/web/assets/FloatingChat-EXzoUQ-9.js +0 -23
  199. package/studio/web/assets/SearchDialog-7qRL8Yqx.js +0 -1
  200. package/studio/web/assets/_basePickBy-QJXUw_pr.js +0 -1
  201. package/studio/web/assets/_baseUniq-DR4vVjyC.js +0 -1
  202. package/studio/web/assets/alertDialog-BbWD9Xxa.js +0 -1
  203. package/studio/web/assets/applications-CdqjiSQe.js +0 -296
  204. package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +0 -1
  205. package/studio/web/assets/arrow-up-D2g_57vT.js +0 -1
  206. package/studio/web/assets/authStore-CJCaADw9.js +0 -3
  207. package/studio/web/assets/button-DWwfgGWI.js +0 -21
  208. package/studio/web/assets/channel-D0AkPjJ8.js +0 -1
  209. package/studio/web/assets/chunk-3FUC2YCW-DuRp-j19.js +0 -2
  210. package/studio/web/assets/chunk-7M6MHVWA-h6WlaTzO.js +0 -213
  211. package/studio/web/assets/chunk-FOHPRMQF-DzxwRta7.js +0 -161
  212. package/studio/web/assets/chunk-O7XYJQB3-CBS3nZYV.js +0 -128
  213. package/studio/web/assets/chunk-Z7XXMR3K-DSHbImdf.js +0 -10
  214. package/studio/web/assets/cn-6T8VranJ.js +0 -1
  215. package/studio/web/assets/cynefin-OW5HDTMX-DIvbXLSj.js +0 -1
  216. package/studio/web/assets/cynefinDiagram-VND7K2PF-BdtaXiCE.js +0 -62
  217. package/studio/web/assets/dagre-6A5THRUB-C7RUWHyz.js +0 -4
  218. package/studio/web/assets/diagram-ATOU4E4O-dxRfeG4B.js +0 -3
  219. package/studio/web/assets/diagram-MPIPVDR6-7prveD1B.js +0 -41
  220. package/studio/web/assets/dist-BrRuMUzt.js +0 -11
  221. package/studio/web/assets/download-Cn6tPG4L.js +0 -1
  222. package/studio/web/assets/elk-276RUBZZ-DxDQPjQT.js +0 -27
  223. package/studio/web/assets/errorText-NpBmvk9I.js +0 -1
  224. package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +0 -1
  225. package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +0 -1
  226. package/studio/web/assets/gitGraph-4MIJSDKK-BrXWmJt2.js +0 -1
  227. package/studio/web/assets/gitGraphDiagram-X574FWY7-C2KnnZeY.js +0 -106
  228. package/studio/web/assets/graphlib-C9Tl7QrK.js +0 -1
  229. package/studio/web/assets/index-QLZw6xrU.css +0 -1
  230. package/studio/web/assets/index-qCLyhyah.js +0 -828
  231. package/studio/web/assets/index.lazy-DCx-DxIV.js +0 -2
  232. package/studio/web/assets/info-A6RAGUB7-BhzBHe4U.js +0 -1
  233. package/studio/web/assets/mermaid.core-1PX6R49U.js +0 -44
  234. package/studio/web/assets/notifications-BSObVIY1.js +0 -1
  235. package/studio/web/assets/notifications-DFLXk68b.js +0 -1
  236. package/studio/web/assets/packet-AYTQ26CC-DWbXHx5g.js +0 -1
  237. package/studio/web/assets/pie-WAS4IAKB-DjoTm3yr.js +0 -1
  238. package/studio/web/assets/pieDiagram-5QR66LMP-Dz3RHpDA.js +0 -39
  239. package/studio/web/assets/profile-TBjzOMIy.js +0 -1
  240. package/studio/web/assets/radar-RG4KPBEZ-CVBgjCOj.js +0 -1
  241. package/studio/web/assets/railroad-74A4TZTK-BdhXw10Q.js +0 -1
  242. package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +0 -1
  243. package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +0 -1
  244. package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +0 -1
  245. package/studio/web/assets/regions-C4an4H7e.js +0 -1
  246. package/studio/web/assets/restartGate-nHAFLDh-.js +0 -1
  247. package/studio/web/assets/sankeyDiagram-IPEJSGJF-D9AyLce2.js +0 -40
  248. package/studio/web/assets/sequenceDiagram-PO4LG4MO-hfk4aIEm.js +0 -169
  249. package/studio/web/assets/setComponentFile-BrqOM1Zm.js +0 -1
  250. package/studio/web/assets/setup-0v2MCm-E.js +0 -2
  251. package/studio/web/assets/status-DdezofUu.js +0 -61
  252. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +0 -8
  253. package/studio/web/assets/tabs-BCTQzoQK.js +0 -1
  254. package/studio/web/assets/treeView-Q6P3EWNA-C-epdxRo.js +0 -1
  255. package/studio/web/assets/treemap-WGGIJYW6-DOVyp_zw.js +0 -1
  256. package/studio/web/assets/usecaseDiagram-POWQR4AR-B6252cbq.js +0 -328
  257. package/studio/web/assets/vendor-datadog-ozKf-pts.js +0 -6
  258. package/studio/web/assets/vendor-tanstack-Bntfka-M.js +0 -2
  259. package/studio/web/assets/vendor-ui-9EYYDgoN.js +0 -41
  260. package/studio/web/assets/wardley-WFR3VGLG-Cfqoe6z4.js +0 -1
  261. package/studio/web/assets/xychartDiagram-PMCCYNJV-UrOKRsmR.js +0 -7
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.RELEASED_TRANSACTION = exports.ImmediateTransaction = exports.DatabaseTransaction = exports.COMMIT_PHASE_GRACE = exports.TRANSACTION_STATE = void 0;
37
+ exports.resetStuckCommitLogCooldownForTests = resetStuckCommitLogCooldownForTests;
37
38
  exports.trackOutstandingCommit = trackOutstandingCommit;
38
39
  exports.databaseCommitsSuspended = databaseCommitsSuspended;
39
40
  exports.permanentlySuspendDatabaseCommits = permanentlySuspendDatabaseCommits;
@@ -61,7 +62,9 @@ exports.isJoinableScope = isJoinableScope;
61
62
  exports.deferForCommitInFlight = deferForCommitInFlight;
62
63
  exports.resetReplayedWritesWarning = resetReplayedWritesWarning;
63
64
  exports.setMaxOutstandingTxnDuration = setMaxOutstandingTxnDuration;
65
+ exports.setMaxRetryDelayMs = setMaxRetryDelayMs;
64
66
  exports.isWriteSupervised = isWriteSupervised;
67
+ exports.getTxnExpiration = getTxnExpiration;
65
68
  exports.setTxnExpiration = setTxnExpiration;
66
69
  const blob_ts_1 = require("./blob.js");
67
70
  const commonUtility_ts_1 = require("../utility/lmdb/commonUtility.js");
@@ -107,7 +110,7 @@ const MAX_RETRIES = 40;
107
110
  exports.COMMIT_PHASE_GRACE = 10;
108
111
  // Cap the per-retry backoff so replication-applied transactions, which retry conflicts without a
109
112
  // cap (see the commit rejection handler), don't grow the delay unbounded.
110
- const MAX_RETRY_DELAY_MS = 1000;
113
+ let MAX_RETRY_DELAY_MS = 1000;
111
114
  let oldestOutstandingCommit;
112
115
  let newestOutstandingCommit;
113
116
  let outstandingCommitCount = 0;
@@ -123,13 +126,21 @@ const OVERLOAD_LOG_MIN_INTERVAL_MS = 1000;
123
126
  // wedge and would starve `abandon`, which names a different transaction and is the only server-side
124
127
  // record of why a request was failed. Mutates only when it grants, so a caller with its own
125
128
  // suppression as well (checkOverloaded's per-node `logged`) must test that first.
126
- const lastStuckCommitLogAt = { shed: -Infinity, abandon: -Infinity };
129
+ const lastStuckCommitLogAt = { shed: -Infinity, abandon: -Infinity, overdue: -Infinity };
127
130
  function allowStuckCommitLog(site, now) {
128
131
  if (now - lastStuckCommitLogAt[site] <= OVERLOAD_LOG_MIN_INTERVAL_MS)
129
132
  return false;
130
133
  lastStuckCommitLogAt[site] = now;
131
134
  return true;
132
135
  }
136
+ /** Test seam: clears the stuck- and stalled-commit log cooldowns so consecutive tests can assert on log emission without sleeping. */
137
+ function resetStuckCommitLogCooldownForTests() {
138
+ lastStuckCommitLogAt.shed = -Infinity;
139
+ lastStuckCommitLogAt.abandon = -Infinity;
140
+ lastStuckCommitLogAt.overdue = -Infinity;
141
+ lastStalledCommitWarningAt = -Infinity;
142
+ lastStalledCommitErrorAt = -Infinity;
143
+ }
133
144
  // Which database/table, which native transaction, and which request a stuck commit belongs to —
134
145
  // without it a wedge gives no indication of what to investigate (harper#2001).
135
146
  function describeCommitIdentity(store, startedFrom, nativeTransaction, rootStore = store?.rootStore) {
@@ -330,14 +341,23 @@ let replayedWritesWarned = false;
330
341
  * committed or aborted, and every caller is a cleanup path whose own callers have no handler — a
331
342
  * throw there would abandon the rest of the cleanup.
332
343
  */
333
- function abortNativeTransaction(transaction, context) {
344
+ function abortNativeTransaction(transaction, context, store, startedFrom) {
334
345
  if (transaction == null)
335
346
  return;
347
+ const txn = transaction;
348
+ if (txn.abortedByWrapper || txn.committed || txn.commitInFlight)
349
+ return;
350
+ txn.abortedByWrapper = true;
336
351
  try {
337
352
  transaction.abort();
338
353
  }
339
354
  catch (error) {
340
- harperLogger.debug?.(context, error);
355
+ if (error?.code === 'ERR_TRANSACTION_ABANDONED') {
356
+ harperLogger.error?.(`Aborted a transaction with a durable log entry (${context}), ${describeCommitIdentity(store, startedFrom, transaction)}`, error);
357
+ }
358
+ else {
359
+ harperLogger.debug?.(context, error);
360
+ }
341
361
  }
342
362
  }
343
363
  // The analytics module registers a recorder here at load (dependency inversion, mirroring
@@ -564,6 +584,23 @@ function clearAttemptState(txn) {
564
584
  txn.postSubmitPoisoned = false;
565
585
  txn.stalledCommitResourcesReleased = false;
566
586
  }
587
+ // Returned by takeCompletions() when nothing is staged; only its length is read before Promise.all.
588
+ const NO_COMPLETIONS = Object.freeze([]);
589
+ // Crash-recovery replay sets `isRetry` before `save()`, so `hasStagedLog` is never set there.
590
+ function hasDurableLog(transaction, error) {
591
+ const txn = transaction;
592
+ return error?.hasLog === true || Boolean(txn?.isRetry && txn?.hasStagedLog);
593
+ }
594
+ function releaseTransactionLockHolds(transaction) {
595
+ const txn = transaction;
596
+ const holds = txn?.lockHolds;
597
+ if (!holds)
598
+ return;
599
+ txn.lockHolds = undefined;
600
+ for (const handle of holds) {
601
+ handle.releaseCommitHold?.();
602
+ }
603
+ }
567
604
  class DatabaseTransaction {
568
605
  #context;
569
606
  // Whether a resources/transaction.ts scope owns this instance — i.e. a final commit or abort is
@@ -583,6 +620,10 @@ class DatabaseTransaction {
583
620
  readTxn;
584
621
  readTxnRefCount;
585
622
  readTxnsUsed;
623
+ // Bumped each time this instance acquires a read handle, so a reference can be returned to the
624
+ // acquisition it came from (doneReadTxn()). Not the handle's identity: lmdb-js hands back the same
625
+ // read Txn object for a new snapshot.
626
+ readTxnGeneration = 0;
586
627
  timeout;
587
628
  writeTick = -1;
588
629
  timeoutBudget = 0;
@@ -705,6 +746,7 @@ class DatabaseTransaction {
705
746
  readTransactionOwners.set(transaction, this);
706
747
  this.rangeReadActive = false;
707
748
  this.readTxnsUsed = 1;
749
+ this.readTxnGeneration++;
708
750
  this.baseReadRefConsumed = false;
709
751
  this.handleOpenedAt = performance.now();
710
752
  }
@@ -806,11 +848,12 @@ class DatabaseTransaction {
806
848
  // is not worth doing either, so those reads stay untraced.
807
849
  if (DEBUG_LONG_TXNS && this.stackTraces)
808
850
  this.stackTraces.push(new StartedTransaction());
809
- this.readTxnsUsed++;
851
+ if (readTxn)
852
+ this.readTxnsUsed++;
810
853
  return readTxn;
811
854
  }
812
- doneReadTxn() {
813
- if (!this.transaction)
855
+ doneReadTxn(generation = this.readTxnGeneration) {
856
+ if (!this.transaction || generation !== this.readTxnGeneration)
814
857
  return;
815
858
  if (--this.readTxnsUsed === 0) {
816
859
  // The native handle was only being held open for the read iterators: any writes staged
@@ -851,7 +894,7 @@ class DatabaseTransaction {
851
894
  */
852
895
  releaseReadTxn() {
853
896
  const transaction = this.detachOwnedTransaction();
854
- abortNativeTransaction(transaction, 'releasing timed-out read transaction');
897
+ abortNativeTransaction(transaction, 'releasing timed-out read transaction', this.writes[0]?.store ?? this.db, this.startedFrom);
855
898
  this.completeDeferredContextRelease();
856
899
  }
857
900
  /**
@@ -889,7 +932,7 @@ class DatabaseTransaction {
889
932
  let writesForStore = (this.writesByKey ??= new Map()).get(operation.store);
890
933
  if (!writesForStore)
891
934
  this.writesByKey.set(operation.store, (writesForStore = new Map()));
892
- const keyId = writeKeyId(operation.key);
935
+ const keyId = (operation.keyId = writeKeyId(operation.key));
893
936
  const priorWrite = writesForStore.get(keyId);
894
937
  if (priorWrite)
895
938
  operation.priorWrite = priorWrite;
@@ -904,6 +947,8 @@ class DatabaseTransaction {
904
947
  if (index > -1)
905
948
  this.writes[index] = null;
906
949
  this.ownedWrites?.delete(operation);
950
+ if (operation.stagedIn === this)
951
+ operation.stagedIn = undefined;
907
952
  if (operation.key === undefined)
908
953
  return;
909
954
  const writesForStore = this.writesByKey?.get(operation.store);
@@ -991,12 +1036,299 @@ class DatabaseTransaction {
991
1036
  if (recordLocks.size === 0)
992
1037
  this.recordLocks = undefined;
993
1038
  }
994
- noteCommittedLockVersions() {
1039
+ budgetCarrier(transaction) {
1040
+ const txn = transaction;
1041
+ if (txn?.commitStartedAt != null)
1042
+ return txn;
1043
+ if (this.saveCommits && txn && this.hasSiblingCommitInFlight(transaction ?? undefined)) {
1044
+ return txn;
1045
+ }
1046
+ if (txn?.immediateCommit)
1047
+ return txn;
1048
+ return this.saveCommits ? this : (this.root ?? this);
1049
+ }
1050
+ backfillActiveHandleWrites(handle) {
1051
+ if (handle.immediateCommit)
1052
+ return;
1053
+ for (let i = 0; i < this.writes.length; i++) {
1054
+ const w = this.writes[i];
1055
+ if (w?.saved && !w.stagedOn)
1056
+ w.stagedOn = handle;
1057
+ }
1058
+ }
1059
+ isSingleHandleTarget(txn) {
1060
+ return (!txn.immediateCommit &&
1061
+ (!this.activeCommitHandles || this.activeCommitHandles.size === 0) &&
1062
+ (!this.activeCommitHandle || this.activeCommitHandle === txn));
1063
+ }
1064
+ getHandleStagedTxnTime(txn) {
1065
+ if (!txn)
1066
+ return undefined;
1067
+ if (this.isSingleHandleTarget(txn) &&
1068
+ (this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
1069
+ return this.activeHandleStagedTxnTime || txn.stagedTxnTime;
1070
+ }
1071
+ return txn.stagedTxnTime;
1072
+ }
1073
+ setHandleStagedTxnTime(txn, txnTime) {
1074
+ if (this.isSingleHandleTarget(txn) &&
1075
+ (this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
1076
+ if (!this.activeHandleStagedTxnTime)
1077
+ this.activeHandleStagedTxnTime = txnTime;
1078
+ return;
1079
+ }
1080
+ txn.stagedTxnTime ??= txnTime;
1081
+ }
1082
+ isHandleCommitInFlight(txn) {
1083
+ if (!txn)
1084
+ return false;
1085
+ if (this.activeCommitHandle === txn) {
1086
+ return Boolean(this.activeHandleInFlight || txn.commitInFlight);
1087
+ }
1088
+ return Boolean(txn.commitInFlight);
1089
+ }
1090
+ setHandleCommitInFlight(txn, inFlight) {
1091
+ if (this.isSingleHandleTarget(txn) && inFlight && !this.activeCommitHandle) {
1092
+ this.activeCommitHandle = txn;
1093
+ }
1094
+ if (this.activeCommitHandle === txn) {
1095
+ this.activeHandleInFlight = inFlight;
1096
+ if (!inFlight) {
1097
+ txn.commitInFlight = false;
1098
+ }
1099
+ return;
1100
+ }
1101
+ txn.commitInFlight = inFlight;
1102
+ }
1103
+ isHandleCommitted(txn) {
1104
+ if (!txn)
1105
+ return false;
1106
+ if (this.activeCommitHandle === txn) {
1107
+ return Boolean(this.activeHandleCommitted || txn.committed);
1108
+ }
1109
+ return Boolean(txn.committed);
1110
+ }
1111
+ setHandleCommitted(txn) {
1112
+ if (this.activeCommitHandle === txn) {
1113
+ this.activeHandleInFlight = false;
1114
+ this.activeHandleCommitted = true;
1115
+ txn.commitInFlight = false;
1116
+ return;
1117
+ }
1118
+ txn.commitInFlight = false;
1119
+ txn.committed = true;
1120
+ }
1121
+ trackActiveCommitHandle(transaction) {
1122
+ if (!transaction.immediateCommit &&
1123
+ !this.activeCommitHandle &&
1124
+ (!this.activeCommitHandles || this.activeCommitHandles.size === 0)) {
1125
+ this.activeCommitHandle = transaction;
1126
+ return;
1127
+ }
1128
+ if (this.activeCommitHandle === transaction)
1129
+ return;
1130
+ let handles = this.activeCommitHandles;
1131
+ if (!handles) {
1132
+ handles = this.activeCommitHandles = new Set();
1133
+ }
1134
+ if (this.activeCommitHandle) {
1135
+ const first = this.activeCommitHandle;
1136
+ if (this.activeHandleStagedTxnTime && !first.stagedTxnTime) {
1137
+ first.stagedTxnTime = this.activeHandleStagedTxnTime;
1138
+ }
1139
+ if (this.activeHandleInFlight)
1140
+ first.commitInFlight = true;
1141
+ if (this.activeHandleCommitted)
1142
+ first.committed = true;
1143
+ this.activeHandleStagedTxnTime = 0;
1144
+ this.activeHandleInFlight = false;
1145
+ this.activeHandleCommitted = false;
1146
+ this.backfillActiveHandleWrites(first);
1147
+ if (this.saveCommits) {
1148
+ if (this.commitStartedAt != null && first.commitStartedAt == null) {
1149
+ first.commitStartedAt = this.commitStartedAt;
1150
+ this.commitStartedAt = undefined;
1151
+ }
1152
+ if (this.commitOverdueLogged) {
1153
+ first.commitOverdueLogged = true;
1154
+ this.commitOverdueLogged = undefined;
1155
+ }
1156
+ }
1157
+ handles.add(this.activeCommitHandle);
1158
+ this.activeCommitHandle = undefined;
1159
+ }
1160
+ handles.add(transaction);
1161
+ }
1162
+ releaseActiveCommitHandle(transaction) {
1163
+ if (this.activeCommitHandle === transaction) {
1164
+ this.activeCommitHandle = undefined;
1165
+ this.activeHandleStagedTxnTime = 0;
1166
+ this.activeHandleInFlight = false;
1167
+ this.activeHandleCommitted = false;
1168
+ return;
1169
+ }
1170
+ const handles = this.activeCommitHandles;
1171
+ if (!handles)
1172
+ return;
1173
+ handles.delete(transaction);
1174
+ if (handles.size === 1) {
1175
+ for (const remaining of handles) {
1176
+ const rem = remaining;
1177
+ if (!rem.immediateCommit) {
1178
+ this.activeCommitHandle = remaining;
1179
+ this.activeHandleStagedTxnTime = rem.stagedTxnTime ?? 0;
1180
+ this.activeHandleInFlight = Boolean(rem.commitInFlight);
1181
+ this.activeHandleCommitted = Boolean(rem.committed);
1182
+ handles.clear();
1183
+ }
1184
+ }
1185
+ }
1186
+ }
1187
+ clearActiveCommitHandles() {
1188
+ if (this.activeCommitHandle !== undefined || this.activeHandleStagedTxnTime) {
1189
+ this.activeCommitHandle = undefined;
1190
+ this.activeHandleStagedTxnTime = 0;
1191
+ this.activeHandleInFlight = false;
1192
+ this.activeHandleCommitted = false;
1193
+ }
1194
+ this.activeCommitHandles?.clear();
1195
+ }
1196
+ hasActiveCommitHandle() {
1197
+ return (this.activeCommitHandle !== undefined ||
1198
+ (this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0));
1199
+ }
1200
+ /**
1201
+ * Retries so far of the handles with a durable transaction-log entry still retrying their commit on
1202
+ * this link, summed over overlapping commits so the total moves whenever any of them does, or -1
1203
+ * when there is none.
1204
+ */
1205
+ activeDurableLogRetries() {
1206
+ let retries = -1;
1207
+ const note = (handle) => {
1208
+ const txn = handle;
1209
+ if (!txn || !hasDurableLog(txn) || this.isHandleCommitted(txn) || txn.abortedByWrapper)
1210
+ return;
1211
+ retries = Math.max(retries, 0) + (txn.retries ?? 0);
1212
+ };
1213
+ note(this.activeCommitHandle);
1214
+ if (this.activeCommitHandles)
1215
+ for (const handle of this.activeCommitHandles)
1216
+ note(handle);
1217
+ note(this.transaction);
1218
+ return retries;
1219
+ }
1220
+ isActiveCommitHandle(transaction) {
1221
+ return this.activeCommitHandle === transaction || Boolean(this.activeCommitHandles?.has(transaction));
1222
+ }
1223
+ hasSiblingCommitInFlight(targetTransaction) {
1224
+ const excluded = targetTransaction ?? this.transaction;
1225
+ if (this.activeCommitHandle)
1226
+ return this.activeCommitHandle !== excluded;
1227
+ const active = this.activeCommitHandles;
1228
+ if (!active || active.size === 0)
1229
+ return false;
1230
+ if (!excluded)
1231
+ return true;
1232
+ return active.size > (active.has(excluded) ? 1 : 0);
1233
+ }
1234
+ isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
1235
+ if (!write)
1236
+ return false;
1237
+ if (targetTransaction?.immediateCommit) {
1238
+ return write.stagedOn !== targetTransaction;
1239
+ }
1240
+ if (write.deferSave && !write.saved && this.saveCommits)
1241
+ return ((!fromAbort || Boolean(targetTransaction) || Boolean(this.isCommitting)) &&
1242
+ !write.stagedOn &&
1243
+ !write.lockHandle?.released &&
1244
+ !write.lockHandle?.isExpired());
1245
+ if (!write.saved)
1246
+ return !fromAbort && Boolean(targetTransaction) && this.open === exports.TRANSACTION_STATE.OPEN && !write.stagedOn;
1247
+ const stagedOn = (write.stagedOn ?? this.activeCommitHandle ?? this.transaction);
1248
+ if (!stagedOn || stagedOn === targetTransaction || stagedOn.abortedByWrapper || this.isHandleCommitted(stagedOn))
1249
+ return false;
1250
+ if (excludeImmediate && stagedOn.immediateCommit)
1251
+ return false;
1252
+ if (!fromAbort &&
1253
+ Boolean(targetTransaction) &&
1254
+ this.open === exports.TRANSACTION_STATE.OPEN &&
1255
+ stagedOn === this.transaction)
1256
+ return true;
1257
+ if (hasSiblingInFlight &&
1258
+ (Boolean(targetTransaction) ||
1259
+ (!fromAbort && this.isActiveCommitHandle(stagedOn)) ||
1260
+ (fromAbort &&
1261
+ this.isActiveCommitHandle(stagedOn) &&
1262
+ ((!excludeImmediate && Boolean(stagedOn.immediateCommit)) || Boolean(stagedOn.isRetry)))))
1263
+ return true;
1264
+ return Boolean(this.isHandleCommitInFlight(stagedOn) || hasDurableLog(stagedOn));
1265
+ }
1266
+ hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
1267
+ if (!this.stagesWriteOnSave)
1268
+ return false;
1269
+ for (let i = 0; i < this.writes.length; i++) {
1270
+ if (this.isWriteOwnedByActiveCommit(this.writes[i], targetTransaction, hasSiblingInFlight, fromAbort, excludeImmediate))
1271
+ return true;
1272
+ }
1273
+ return false;
1274
+ }
1275
+ hasSavedWrite() {
1276
+ for (let i = 0; i < this.writes.length; i++) {
1277
+ if (this.writes[i]?.saved)
1278
+ return true;
1279
+ }
1280
+ return false;
1281
+ }
1282
+ hasSavedWriteForHandle(transaction, isImmediateCommit) {
1283
+ for (let i = 0; i < this.writes.length; i++) {
1284
+ const write = this.writes[i];
1285
+ if (!write?.saved)
1286
+ continue;
1287
+ if (isImmediateCommit ? write.stagedOn === transaction : !write.stagedOn || write.stagedOn === transaction) {
1288
+ return true;
1289
+ }
1290
+ }
1291
+ return false;
1292
+ }
1293
+ retainTransactionLockHolds(transaction) {
1294
+ if (!transaction || !this.hasLeaseProtectedWrite)
1295
+ return;
1296
+ const txn = transaction;
1297
+ if (txn.lockHolds || txn.holdBudgetSpent)
1298
+ return;
1299
+ const carrier = this.budgetCarrier(transaction);
1300
+ if (carrier.commitStartedAt == null)
1301
+ carrier.commitStartedAt = performance.now();
1302
+ const budget = this.commitConflictBudget();
1303
+ const holdDeadlineMono = Number.isFinite(budget) ? carrier.commitStartedAt + budget : undefined;
1304
+ let holds;
1305
+ for (let i = 0; i < this.writes.length; i++) {
1306
+ const write = this.writes[i];
1307
+ if (!write?.saved || (write.stagedOn && write.stagedOn !== transaction))
1308
+ continue;
1309
+ const handle = write.lockHandle;
1310
+ if (!handle?.retainForCommit || (holds && holds.has(handle)))
1311
+ continue;
1312
+ handle.retainForCommit(holdDeadlineMono);
1313
+ (holds ??= new Set()).add(handle);
1314
+ }
1315
+ if (holds)
1316
+ txn.lockHolds = holds;
1317
+ }
1318
+ noteCommittedLockVersions(committedTransaction) {
995
1319
  const recordLocks = this.recordLocks;
996
1320
  if (!recordLocks)
997
1321
  return;
1322
+ const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
1323
+ const filterByStagedOn = Boolean(committedTransaction || this.hasSiblingCommitInFlight(committedTransaction));
998
1324
  for (const write of this.writes) {
999
- if (write?.appliedRecordVersion == null)
1325
+ if (!write?.saved)
1326
+ continue;
1327
+ if (isImmediateCommit
1328
+ ? write.stagedOn !== committedTransaction
1329
+ : filterByStagedOn && write.stagedOn && write.stagedOn !== committedTransaction)
1330
+ continue;
1331
+ if (write.appliedRecordVersion == null)
1000
1332
  continue;
1001
1333
  const handle = write.lockHandle ?? recordLocks.get(write.store)?.get(writeKeyId(write.key));
1002
1334
  if (handle && !handle.released)
@@ -1005,9 +1337,32 @@ class DatabaseTransaction {
1005
1337
  }
1006
1338
  /**
1007
1339
  * Discard the staged write set (committed or aborted); the per-key chain must go with it so a
1008
- * reused transaction never bases a write on a previous batch's staged state.
1340
+ * reused transaction never bases a write on a previous batch's staged state. When concurrent
1341
+ * commits share one DatabaseTransaction, only the writes staged on `committedTransaction` are
1342
+ * removed while another native commit is still in flight.
1009
1343
  */
1010
- clearWrites() {
1344
+ clearWrites(committedTransaction, fromAbort = false) {
1345
+ if (committedTransaction)
1346
+ this.releaseActiveCommitHandle(committedTransaction);
1347
+ const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
1348
+ const hasSiblingInFlight = this.hasSiblingCommitInFlight(committedTransaction);
1349
+ if (this.stagesWriteOnSave &&
1350
+ (isImmediateCommit ||
1351
+ ((hasSiblingInFlight || !committedTransaction || this.commitsInFlight > 1 || this.saveCommits) &&
1352
+ this.hasWriteOwnedByActiveCommit(committedTransaction, hasSiblingInFlight, fromAbort)))) {
1353
+ for (const write of [...this.writes]) {
1354
+ if (!write || this.isWriteOwnedByActiveCommit(write, committedTransaction, hasSiblingInFlight, fromAbort))
1355
+ continue;
1356
+ this.detachWrite(write);
1357
+ }
1358
+ this.writes = this.writes.filter((write) => write);
1359
+ this.validated = this.writes.reduce((acc, w, idx) => (w?.saved ? idx + 1 : acc), 0);
1360
+ if (this.hasLeaseProtectedWrite && !this.writes.some((write) => write?.lockHandle)) {
1361
+ this.hasLeaseProtectedWrite = false;
1362
+ }
1363
+ return;
1364
+ }
1365
+ this.clearActiveCommitHandles();
1011
1366
  // A deferred write's `stagedIn` must not outlive this transaction's ability to commit it: save() can
1012
1367
  // fire after a commit or abort, and routing it back here would revive a write this transaction
1013
1368
  // already rolled back — whose blobs abort() has reclaimed. Cleared here, save() resolves the
@@ -1019,6 +1374,7 @@ class DatabaseTransaction {
1019
1374
  this.ownedWrites?.delete(write);
1020
1375
  }
1021
1376
  this.writes = [];
1377
+ this.validated = 0;
1022
1378
  this.writesByKey = undefined;
1023
1379
  // The commit-fence latch belongs to the discarded batch. A reused transaction that once staged
1024
1380
  // a locked write would otherwise re-scan every write of every later unlocked batch. Assigned
@@ -1098,12 +1454,13 @@ class DatabaseTransaction {
1098
1454
  * no record), and writes with no stored entry at all — a delete leaves a readable tombstone on any
1099
1455
  * audited or delete-tracking table, so only a delete on a table with neither is entry-less. Such a
1100
1456
  * `put(A); delete(B)` confirms on A's entry, whose version is this transaction's (every write in a
1101
- * transaction is stamped with one version).
1457
+ * transaction is stamped with one version). `savedOnly` also skips a deferred write not yet saved,
1458
+ * which a commit leaves in place for a later one.
1102
1459
  */
1103
- lastConfirmableEntry() {
1460
+ lastConfirmableEntry(savedOnly = false) {
1104
1461
  for (let i = this.writes.length - 1; i >= 0; i--) {
1105
1462
  const write = this.writes[i];
1106
- if (!write || write.skipReplicationConfirmation)
1463
+ if (!write || write.skipReplicationConfirmation || (savedOnly && !write.saved))
1107
1464
  continue;
1108
1465
  const entry = write.store.getEntry(write.key);
1109
1466
  if (entry)
@@ -1116,9 +1473,61 @@ class DatabaseTransaction {
1116
1473
  * rejection in that window is an unhandled rejection (fatal under --unhandled-rejections=strict).
1117
1474
  * The rejection still surfaces through commit()'s Promise.all.
1118
1475
  */
1119
- stageCompletion(completion) {
1476
+ stageCompletion(completion, transaction) {
1120
1477
  completion.then(undefined, () => { });
1478
+ const index = this.completions.length;
1121
1479
  this.completions.push(completion);
1480
+ if (transaction &&
1481
+ (this.saveCommits ||
1482
+ Boolean(transaction.immediateCommit) ||
1483
+ this.hasActiveCommitHandle() ||
1484
+ Boolean(this.completionHandles))) {
1485
+ (this.completionHandles ??= [])[index] = transaction;
1486
+ }
1487
+ }
1488
+ takeCompletions(transaction) {
1489
+ if (this.completions.length === 0)
1490
+ return NO_COMPLETIONS;
1491
+ const handles = this.completionHandles;
1492
+ const isImmediate = Boolean(transaction?.immediateCommit);
1493
+ if (!handles) {
1494
+ if (isImmediate)
1495
+ return NO_COMPLETIONS;
1496
+ const completions = this.completions;
1497
+ this.completions = [];
1498
+ return completions;
1499
+ }
1500
+ let allMatch = true;
1501
+ for (let i = 0; i < this.completions.length; i++) {
1502
+ const handle = handles[i];
1503
+ if (!(handle ? handle === transaction : !isImmediate)) {
1504
+ allMatch = false;
1505
+ break;
1506
+ }
1507
+ }
1508
+ if (allMatch) {
1509
+ const completions = this.completions;
1510
+ this.completions = [];
1511
+ this.completionHandles = undefined;
1512
+ return completions;
1513
+ }
1514
+ const taken = [];
1515
+ const remaining = [];
1516
+ const remainingHandles = [];
1517
+ for (let i = 0; i < this.completions.length; i++) {
1518
+ const completion = this.completions[i];
1519
+ const handle = handles[i];
1520
+ if (handle ? handle === transaction : !isImmediate) {
1521
+ taken.push(completion);
1522
+ }
1523
+ else {
1524
+ remaining.push(completion);
1525
+ remainingHandles.push(handle);
1526
+ }
1527
+ }
1528
+ this.completions = remaining;
1529
+ this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
1530
+ return taken;
1122
1531
  }
1123
1532
  /**
1124
1533
  * Discard staged completions that no commit() will ever aggregate (abort path). Their rejections
@@ -1126,12 +1535,41 @@ class DatabaseTransaction {
1126
1535
  * Clearing them also keeps a reused transaction's next commit() from rejecting with the previous
1127
1536
  * batch's error.
1128
1537
  */
1129
- drainCompletions() {
1538
+ drainCompletions(targetTransaction, preserveActiveWrites = false) {
1130
1539
  if (this.completions.length === 0)
1131
1540
  return;
1132
- const completions = this.completions;
1133
- this.completions = [];
1134
- for (const completion of completions)
1541
+ const handles = this.completionHandles;
1542
+ const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
1543
+ let drained;
1544
+ if (!preserveActiveWrites) {
1545
+ drained = this.completions;
1546
+ this.completions = [];
1547
+ this.completionHandles = undefined;
1548
+ }
1549
+ else {
1550
+ drained = [];
1551
+ const remaining = [];
1552
+ const remainingHandles = [];
1553
+ for (let i = 0; i < this.completions.length; i++) {
1554
+ const completion = this.completions[i];
1555
+ const handle = handles?.[i];
1556
+ const keep = isImmediateTarget
1557
+ ? handle !== targetTransaction
1558
+ : Boolean(handle) &&
1559
+ handle !== targetTransaction &&
1560
+ (this.isActiveCommitHandle(handle) || hasDurableLog(handle));
1561
+ if (keep) {
1562
+ remaining.push(completion);
1563
+ remainingHandles.push(handle);
1564
+ }
1565
+ else {
1566
+ drained.push(completion);
1567
+ }
1568
+ }
1569
+ this.completions = remaining;
1570
+ this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
1571
+ }
1572
+ for (const completion of drained)
1135
1573
  completion.then(undefined, (error) => harperLogger.warn?.('A staged transaction completion failed after the transaction was aborted', error));
1136
1574
  }
1137
1575
  addWrite(operation) {
@@ -1205,7 +1643,7 @@ class DatabaseTransaction {
1205
1643
  // (the same rule the pre-submit fence applies below). Without the distinction,
1206
1644
  // `lock(); save(); unlock(); commit()` succeeded normally and threw 409 only when a conflict
1207
1645
  // retry or an open read iterator forced the replay to re-save it.
1208
- if (operation.saved ? lockHandle?.isLeaseExpired() : lockHandle?.isExpired()) {
1646
+ if (operation.saved ? lockHandle?.isLeaseExpired() && !hasDurableLog(transaction) : lockHandle?.isExpired()) {
1209
1647
  // Through `detachWrite`, which also repairs the per-key chain: dropping it from `writes` alone
1210
1648
  // leaves `writesByKey` pointing at a write that was refused, so a caller that catches this 409
1211
1649
  // and stages the same key again would take the rejected operation as its merge basis.
@@ -1229,7 +1667,10 @@ class DatabaseTransaction {
1229
1667
  operation.lockStamp = lockHandle.holderVersionCandidate();
1230
1668
  }
1231
1669
  }
1232
- let txnTime = operation.lockStamp ?? this.timestamp;
1670
+ const concurrentClosedWrite = Boolean(transaction?.immediateCommit) ||
1671
+ (!transaction && this.open !== exports.TRANSACTION_STATE.OPEN && !this.saveCommits);
1672
+ let txnTime = operation.lockStamp ?? this.getHandleStagedTxnTime(transaction) ?? (concurrentClosedWrite ? 0 : this.timestamp);
1673
+ const mintedTimestamp = !txnTime;
1233
1674
  // Only an OPEN transaction accepts new staged writes. After commit, this.transaction may still
1234
1675
  // be retained for outstanding read iterators; staging into it would silently discard the write
1235
1676
  // when doneReadTxn() aborts the handle, so such writes commit immediately on a fresh
@@ -1260,20 +1701,43 @@ class DatabaseTransaction {
1260
1701
  else {
1261
1702
  // if it is closed, we have to immediately commit, using our immediate transaction
1262
1703
  immediateCommit = true;
1704
+ if (!this.saveCommits)
1705
+ transaction.immediateCommit = true;
1263
1706
  }
1264
1707
  if (txnTime) {
1265
1708
  transaction.setTimestamp(txnTime);
1266
1709
  }
1267
1710
  }
1711
+ if (this.activeCommitHandle && this.activeCommitHandle !== transaction) {
1712
+ this.backfillActiveHandleWrites(this.activeCommitHandle);
1713
+ }
1714
+ else if (!this.activeCommitHandle && this.transaction && this.transaction !== transaction) {
1715
+ this.backfillActiveHandleWrites(this.transaction);
1716
+ }
1717
+ const shouldStampStagedOn = this.saveCommits ||
1718
+ immediateCommit ||
1719
+ Boolean(transaction?.immediateCommit) ||
1720
+ operation.stagedOn !== undefined ||
1721
+ (this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0) ||
1722
+ (this.activeCommitHandle !== undefined && this.activeCommitHandle !== transaction) ||
1723
+ (this.transaction != null && this.transaction !== transaction);
1268
1724
  if (this.isReplay) {
1269
1725
  // Replayed writes came FROM the transaction log; never re-append them —
1270
1726
  // replay iterates that same log, so re-appending prevents convergence
1271
1727
  // (boot hangs replaying its own output). Conflict retries stamp isRetry
1272
1728
  // at the retry sites in commit(); this is the replay-path equivalent.
1273
1729
  transaction.isRetry = true;
1730
+ transaction.retries = this.retries;
1274
1731
  }
1275
- if (!txnTime)
1276
- txnTime = this.timestamp = transaction.getTimestamp();
1732
+ if (!txnTime) {
1733
+ txnTime = transaction.getTimestamp();
1734
+ if (!concurrentClosedWrite)
1735
+ this.timestamp = txnTime;
1736
+ }
1737
+ if (!operation.lockStamp)
1738
+ this.setHandleStagedTxnTime(transaction, txnTime);
1739
+ if (this.saveCommits && mintedTimestamp)
1740
+ this.timestamp = 0;
1277
1741
  if (!operation.saved && operation.pendingPriorWrite) {
1278
1742
  const pendingWrites = [];
1279
1743
  for (let pending = operation.pendingPriorWrite; pending;) {
@@ -1301,50 +1765,78 @@ class DatabaseTransaction {
1301
1765
  // An entry read uncached through this same pinned-snapshot handle is already that base, until a retry resets
1302
1766
  // the snapshot or an earlier staged write to the key changes what a read returns; the resource withholds the
1303
1767
  // handle once its entry was replaced or evicted, and locked, snapshot-free and replayed writes always reload.
1768
+ const txnRetries = transaction?.retries ?? 0;
1304
1769
  const reusesBaseRead = operation.baseReadTxn === transaction &&
1305
- this.retries === 0 &&
1770
+ txnRetries === 0 &&
1306
1771
  !reloadEntry &&
1307
1772
  !operation.priorWrite &&
1308
1773
  !operation.lockHandle &&
1309
1774
  !this.snapshotFree &&
1310
1775
  !transaction.snapshotDisabled &&
1311
1776
  !this.isReplay;
1312
- if (!reusesBaseRead && (reloadEntry || operation.entry === undefined || reloadsCommitBase)) {
1313
- const uncachedRead = !!operation.reloadCommitBase || reloadEntry || this.isReplay;
1314
- operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
1315
- }
1316
- if (!operation.saved) {
1317
- // immediately execute in this transaction
1318
- const validated = validateWrite(operation, writeVersion, this);
1319
- if (validated === false) {
1777
+ const wasSaved = Boolean(operation.saved);
1778
+ let completion;
1779
+ try {
1780
+ if (!reusesBaseRead && (reloadEntry || operation.entry === undefined || reloadsCommitBase)) {
1781
+ const uncachedRead = !!operation.reloadCommitBase || reloadEntry || this.isReplay;
1782
+ operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
1783
+ }
1784
+ if (!operation.saved) {
1785
+ // immediately execute in this transaction
1786
+ const validated = validateWrite(operation, writeVersion, this);
1787
+ if (validated === false) {
1788
+ operation.saved = true;
1789
+ if (shouldStampStagedOn)
1790
+ operation.stagedOn = transaction;
1791
+ operation.commit = () => { }; // noop if we try again
1792
+ closeWriteInstance(operation);
1793
+ if (immediateCommit) {
1794
+ this.detachWrite(operation);
1795
+ if (!this.activeCommitHandle)
1796
+ this.activeHandleStagedTxnTime = 0;
1797
+ abortNativeTransaction(transaction, 'aborting an immediate transaction with nothing to write', operation.store, this.startedFrom);
1798
+ }
1799
+ return;
1800
+ }
1320
1801
  operation.saved = true;
1321
- operation.commit = () => { }; // noop if we try again
1802
+ let result = operation.before?.();
1803
+ if (result?.then)
1804
+ this.stageCompletion(result, transaction);
1805
+ result = operation.beforeIntermediate?.();
1806
+ if (result?.then)
1807
+ this.stageCompletion(result, transaction);
1808
+ }
1809
+ if (shouldStampStagedOn)
1810
+ operation.stagedOn = transaction;
1811
+ if (lockHandle || this.recordLocks)
1812
+ operation.trackRecordVersion = true;
1813
+ if (operation.trackRecordVersion)
1814
+ operation.recordVersionApplied = false;
1815
+ operation.committedOn = transaction;
1816
+ operation.committedRound = txnRetries;
1817
+ try {
1818
+ completion = operation.commit(writeVersion, operation.entry, (wasSaved || this.isReplay) && txnRetries > 0, transaction, this);
1819
+ }
1820
+ finally {
1322
1821
  closeWriteInstance(operation);
1323
- return;
1324
1822
  }
1325
- operation.saved = true;
1326
- let result = operation.before?.();
1327
- if (result?.then)
1328
- this.stageCompletion(result);
1329
- result = operation.beforeIntermediate?.();
1330
- if (result?.then)
1331
- this.stageCompletion(result);
1332
- }
1333
- if (lockHandle || this.recordLocks)
1334
- operation.trackRecordVersion = true;
1335
- if (operation.trackRecordVersion)
1336
- operation.recordVersionApplied = false;
1337
- let completion;
1338
- try {
1339
- completion = operation.commit(writeVersion, operation.entry, this.retries > 0, transaction);
1340
1823
  }
1341
- finally {
1342
- closeWriteInstance(operation);
1824
+ catch (error) {
1825
+ // A handle created for an immediate commit is owned by nobody else: a throw before its commit
1826
+ // is submitted must release it rather than leak it.
1827
+ if (immediateCommit) {
1828
+ this.drainCompletions(transaction, true);
1829
+ this.detachWrite(operation);
1830
+ if (!this.activeCommitHandle)
1831
+ this.activeHandleStagedTxnTime = 0;
1832
+ abortNativeTransaction(transaction, 'aborting an immediate transaction whose write threw', operation.store, this.startedFrom);
1833
+ }
1834
+ throw error;
1343
1835
  }
1344
1836
  if (operation.trackRecordVersion)
1345
1837
  operation.appliedRecordVersion = operation.recordVersionApplied ? writeVersion : undefined;
1346
1838
  if (typeof completion?.then === 'function')
1347
- this.stageCompletion(completion);
1839
+ this.stageCompletion(completion, transaction);
1348
1840
  // Sticky record that THIS write staged with its audit entry appended (log entries batch on the
1349
1841
  // native transaction and are durably written by its commit attempt — even a failed one — so
1350
1842
  // they survive the abort-after-failed-commit of the retry paths). isRetry stagings
@@ -1353,6 +1845,7 @@ class DatabaseTransaction {
1353
1845
  // retries where a recommit round self-skips before a fresh-transaction replay.
1354
1846
  if (!operation.skipped && !transaction.isRetry) {
1355
1847
  operation.appendedAuditEntry = true;
1848
+ operation.appendedTo = transaction;
1356
1849
  }
1357
1850
  if (immediateCommit) {
1358
1851
  // immediately commit if the harper transaction is closed
@@ -1549,6 +2042,10 @@ class DatabaseTransaction {
1549
2042
  endCommitAttempt() {
1550
2043
  this.commitsInFlight--;
1551
2044
  if (!this.commitsInFlight) {
2045
+ this.clearActiveCommitHandles();
2046
+ this.commitStartedAt = undefined;
2047
+ if (this.commitOverdueLogged)
2048
+ this.commitOverdueLogged = undefined;
1552
2049
  this.nativeCommitSubmitted = false;
1553
2050
  this.nativeCommitAttemptEnded();
1554
2051
  }
@@ -1568,7 +2065,14 @@ class DatabaseTransaction {
1568
2065
  root.submittedCommitSupervised = false;
1569
2066
  root.deferredPoisonDeadline = undefined;
1570
2067
  root.unsubmittedCommitDeadline = undefined;
2068
+ root.loggedRetriesSeen = undefined;
2069
+ root.loggedRetriesChangedAt = undefined;
2070
+ root.loggedRetriesTick = undefined;
2071
+ root.loggedRetryAdvancing = undefined;
1571
2072
  root.stalledCommitLogged = false;
2073
+ root.commitStartedAt = undefined;
2074
+ if (root.commitOverdueLogged)
2075
+ root.commitOverdueLogged = undefined;
1572
2076
  const submittedLink = root.submittedLink;
1573
2077
  root.submittedLink = undefined;
1574
2078
  const submittedLinks = root.submittedLinks;
@@ -1643,35 +2147,57 @@ class DatabaseTransaction {
1643
2147
  // reused across retries — the native layer resets it in place (fresh snapshot) on IsBusy/TryAgain —
1644
2148
  // but reassigned to a fresh replay transaction when outstanding read iterators retain this.transaction
1645
2149
  let transaction = options.transaction ?? this.transaction;
2150
+ const retryRound = Boolean(options.transaction?.isRetry);
2151
+ const isImmediateCommit = Boolean(transaction?.immediateCommit);
1646
2152
  try {
2153
+ if (retryRound && this.activeCommitHandle && this.activeCommitHandle !== transaction) {
2154
+ this.backfillActiveHandleWrites(this.activeCommitHandle);
2155
+ }
1647
2156
  for (let i = 0; i < this.writes.length; i++) {
1648
2157
  let operation = this.writes[i];
1649
- if (!operation || (this.retries === 0 && operation.saved))
2158
+ if (!operation ||
2159
+ (isImmediateCommit
2160
+ ? !retryRound || !operation.saved || operation.stagedOn !== transaction
2161
+ : retryRound
2162
+ ? !operation.saved || (Boolean(operation.stagedOn) && operation.stagedOn !== transaction)
2163
+ : Boolean(operation.saved) || (this.saveCommits && Boolean(operation.deferSave))))
1650
2164
  continue;
1651
- this.save(operation, transaction, i < this.validated, options);
2165
+ this.save(operation, transaction, Boolean(operation.saved) || i < this.validated, options);
2166
+ if (!transaction)
2167
+ transaction = this.transaction;
1652
2168
  }
2169
+ if (transaction && (this.writes.length > 0 || hasDurableLog(transaction)))
2170
+ this.trackActiveCommitHandle(transaction);
1653
2171
  }
1654
2172
  catch (error) {
2173
+ if (!transaction)
2174
+ transaction = this.transaction ?? undefined;
1655
2175
  // abort() releases only this.transaction; a retry round's handle was detached before its
1656
2176
  // first submission and would otherwise hold its write intents until GC.
1657
- if (transaction !== this.transaction)
1658
- abortNativeTransaction(transaction, 'aborting a retry transaction whose re-save threw');
1659
- this.abort();
2177
+ if (transaction && transaction !== this.transaction)
2178
+ abortNativeTransaction(transaction, 'aborting a retry transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
2179
+ this.abort(false, true, transaction);
1660
2180
  throw error;
1661
2181
  }
1662
- this.validated = this.writes.length;
1663
- const completions = this.completions;
1664
- if (completions.length > 0)
1665
- this.completions = []; // reset
2182
+ if (!isImmediateCommit) {
2183
+ this.validated = this.saveCommits
2184
+ ? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
2185
+ : this.writes.length;
2186
+ }
2187
+ const completions = this.takeCompletions(transaction);
1666
2188
  const stagedWrites = this.writes.length;
1667
- if (completions.length > 0) {
2189
+ let inCommitPhase = false;
2190
+ if (completions.length > 0 && !isImmediateCommit) {
2191
+ inCommitPhase = true;
1668
2192
  this.setCommitPhase(true);
1669
2193
  }
1670
2194
  let commitResult;
1671
2195
  try {
1672
2196
  commitResult = (0, when_ts_1.when)(completions.length > 0 ? Promise.all(completions) : null, () => {
1673
- if (completions.length > 0)
2197
+ if (inCommitPhase) {
2198
+ inCommitPhase = false;
1674
2199
  this.setCommitPhase(false);
2200
+ }
1675
2201
  // The transaction can be aborted underneath us while we are parked in the await above — by the
1676
2202
  // monitor once the commit phase outlives its grace, or through the multi-store poison chain.
1677
2203
  // abort() cleared the write set and released the handle, so resuming would commit nothing and
@@ -1685,7 +2211,11 @@ class DatabaseTransaction {
1685
2211
  }
1686
2212
  if (stagedWrites > 0 && this.writes.length === 0 && this.open === exports.TRANSACTION_STATE.CLOSED)
1687
2213
  throw new hdbError_ts_1.ServerError('Transaction was aborted while its commit was waiting on pre-commit work', 500);
1688
- if (this.writes.length > this.validated) {
2214
+ if (!retryRound &&
2215
+ !isImmediateCommit &&
2216
+ (this.saveCommits
2217
+ ? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
2218
+ : this.writes.length) > this.validated) {
1689
2219
  // check just in case we got any more transactions while we were waiting, if so just recursively continue to finish the additional writes now
1690
2220
  return this.commit({ ...options, continuation: true });
1691
2221
  }
@@ -1697,9 +2227,10 @@ class DatabaseTransaction {
1697
2227
  // and replay branches below deliberately commit a handle other than this.transaction.
1698
2228
  if (!transaction)
1699
2229
  transaction = this.transaction;
1700
- if (!options.transaction && this.writes.some((write) => write))
2230
+ if (!options.transaction && this.hasSavedWrite())
1701
2231
  this.renewChainForNativeCommit();
1702
- this.open = exports.TRANSACTION_STATE.CLOSED;
2232
+ if (!isImmediateCommit)
2233
+ this.open = exports.TRANSACTION_STATE.CLOSED;
1703
2234
  // RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
1704
2235
  // coordinatedRetry, or void on a normal commit/abort.
1705
2236
  let commitResolution;
@@ -1707,11 +2238,12 @@ class DatabaseTransaction {
1707
2238
  // recursions, immediate-commit re-entries, and a second top-level commit() (wrapper
1708
2239
  // commit after an explicit in-handler commit) must not steal a reference owned by an
1709
2240
  // outstanding iterator (doneReadTxn() would then never release the native handle).
1710
- if (!this.baseReadRefConsumed) {
2241
+ if (!isImmediateCommit && !this.baseReadRefConsumed) {
1711
2242
  this.baseReadRefConsumed = true;
1712
2243
  this.readTxnsUsed--;
1713
2244
  }
1714
- if (this.readTxnsUsed > 0) {
2245
+ let hadLeaseProtectedWrite = false;
2246
+ if (!isImmediateCommit && this.readTxnsUsed > 0) {
1715
2247
  // Outstanding iterators still stream through this.transaction — their native iterators
1716
2248
  // live inside it (GetIterator wraps its write batch + snapshot), so committing or
1717
2249
  // aborting the handle now would invalidate them mid-stream. Leave it open for the
@@ -1723,7 +2255,7 @@ class DatabaseTransaction {
1723
2255
  // kept the last iterator from finishing cleanly — a hung stream, the long-transaction
1724
2256
  // monitor's timeout abort — dropped writes the caller had already been told committed.
1725
2257
  this.writes = this.writes.filter((write) => write); // filter out removed entries
1726
- if (this.writes.length > 0) {
2258
+ if (this.hasSavedWrite() || hasDurableLog(transaction)) {
1727
2259
  if (!options.transaction) {
1728
2260
  if (!replayedWritesWarned) {
1729
2261
  replayedWritesWarned = true;
@@ -1741,24 +2273,48 @@ class DatabaseTransaction {
1741
2273
  const replayTransaction = new rocksdb_js_1.Transaction((this.writes[0].store.store ?? this.db.store), { coordinatedRetry: true });
1742
2274
  if (this.timestamp)
1743
2275
  replayTransaction.setTimestamp(this.timestamp);
1744
- this.retries++; // a replay round: commit handlers re-base on the reloaded entries
2276
+ const retries = (transaction?.retries ?? this.retries) + 1;
2277
+ replayTransaction.retries = retries;
2278
+ this.retries = retries; // a replay round: commit handlers re-base on the reloaded entries
2279
+ const prevStagedTxnTime = this.getHandleStagedTxnTime(transaction);
2280
+ if (transaction)
2281
+ this.releaseActiveCommitHandle(transaction);
2282
+ this.trackActiveCommitHandle(replayTransaction);
2283
+ if (prevStagedTxnTime)
2284
+ this.setHandleStagedTxnTime(replayTransaction, prevStagedTxnTime);
2285
+ const abandoned = transaction;
2286
+ const abandonedLogged = hasDurableLog(abandoned);
2287
+ transaction = replayTransaction;
1745
2288
  try {
1746
2289
  for (const operation of this.writes) {
2290
+ if (!operation?.saved)
2291
+ continue;
2292
+ // A non-durable abandoned handle left no readable audit entry; a durable one may
2293
+ // have published its log and must retain the retry shield.
2294
+ if (operation.appendedTo === abandoned && !abandonedLogged) {
2295
+ operation.appendedAuditEntry = false;
2296
+ operation.appendedTo = undefined;
2297
+ }
1747
2298
  this.save(operation, replayTransaction, true, options);
1748
2299
  }
1749
2300
  }
1750
2301
  catch (error) {
1751
- abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw');
1752
- this.abort();
2302
+ abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
2303
+ this.releaseActiveCommitHandle(replayTransaction);
2304
+ this.abort(false, true, this.hasSiblingCommitInFlight(replayTransaction) ? replayTransaction : undefined);
1753
2305
  throw error;
1754
2306
  }
1755
- transaction = replayTransaction;
1756
2307
  }
1757
2308
  // with options.transaction set this is a retry round — the save loop above already
1758
2309
  // re-staged the writes into it
1759
2310
  this.assertDatabaseCommitAllowed(transaction);
1760
2311
  this.markNativeCommitSubmitted();
1761
2312
  commitResolution = transaction.commit();
2313
+ this.setHandleCommitInFlight(transaction, true);
2314
+ if (this.hasLeaseProtectedWrite) {
2315
+ hadLeaseProtectedWrite = true;
2316
+ this.retainTransactionLockHolds(transaction);
2317
+ }
1762
2318
  recordCommitLatency(commitResolution, performance.now());
1763
2319
  // Write-queue-depth accounting for this replay commit happens uniformly below, via
1764
2320
  // trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
@@ -1779,10 +2335,11 @@ class DatabaseTransaction {
1779
2335
  }
1780
2336
  else {
1781
2337
  // no more reads need to be performed, just commit/abort based if there are any writes
1782
- this.detachOwnedTransaction(); // any further operations operate immediately
2338
+ if (!isImmediateCommit)
2339
+ this.detachOwnedTransaction(); // any further operations operate immediately
1783
2340
  if (transaction) {
1784
2341
  this.writes = this.writes.filter((write) => write); // filter out removed entries
1785
- if (this.writes.length > 0) {
2342
+ if (this.hasSavedWriteForHandle(transaction, isImmediateCommit) || hasDurableLog(transaction)) {
1786
2343
  // Commit retries can construct fresh ranges on this live handle after read ownership ends.
1787
2344
  readTransactionOwners.delete(transaction);
1788
2345
  // Re-fence before submitting: the loop above skips operations already marked saved, so
@@ -1790,25 +2347,30 @@ class DatabaseTransaction {
1790
2347
  // retry/replay path re-saves every operation and is fenced there instead. Lease expiry
1791
2348
  // only: an unlock() inside the lease leaves the staged write valid, an elapsed lease
1792
2349
  // does not, whether or not the caller also unlocked.
1793
- for (let i = 0; this.hasLeaseProtectedWrite && i < this.writes.length; i++) {
1794
- const lapsed = this.writes[i].lockHandle;
1795
- if (!lapsed?.isLeaseExpired())
1796
- continue;
1797
- try {
1798
- transaction.abort();
1799
- }
1800
- catch { }
1801
- // Every other terminal exit from commit() runs the logical cleanup too. Throwing
1802
- // straight out would strand the OTHER locks this transaction holds, its staged
1803
- // blobs, and the context's back-reference to a CLOSED transaction — and
1804
- // transaction()'s onComplete has no rejection path to run it later.
1805
- try {
1806
- this.abort();
1807
- }
1808
- catch (error) {
1809
- harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
2350
+ if (this.hasLeaseProtectedWrite && !hasDurableLog(transaction)) {
2351
+ for (let i = 0; i < this.writes.length; i++) {
2352
+ const write = this.writes[i];
2353
+ if (!write?.saved ||
2354
+ (isImmediateCommit
2355
+ ? write.stagedOn !== transaction
2356
+ : Boolean(write.stagedOn) && write.stagedOn !== transaction))
2357
+ continue;
2358
+ const lapsed = write.lockHandle;
2359
+ if (!lapsed?.isLeaseExpired())
2360
+ continue;
2361
+ abortNativeTransaction(transaction, 'aborting a transaction whose record lock lapsed before submission', this.writes[0]?.store ?? this.db, this.startedFrom);
2362
+ // Every other terminal exit from commit() runs the logical cleanup too. Throwing
2363
+ // straight out would strand the OTHER locks this transaction holds, its staged
2364
+ // blobs, and the context's back-reference to a CLOSED transaction — and
2365
+ // transaction()'s onComplete has no rejection path to run it later.
2366
+ try {
2367
+ this.abort(false, true, transaction);
2368
+ }
2369
+ catch (error) {
2370
+ harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
2371
+ }
2372
+ throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
1810
2373
  }
1811
- throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
1812
2374
  }
1813
2375
  // The transaction was created with coordinatedRetry:true (see
1814
2376
  // getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
@@ -1817,6 +2379,11 @@ class DatabaseTransaction {
1817
2379
  this.assertDatabaseCommitAllowed(transaction);
1818
2380
  this.markNativeCommitSubmitted();
1819
2381
  commitResolution = transaction.commit();
2382
+ this.setHandleCommitInFlight(transaction, true);
2383
+ if (this.hasLeaseProtectedWrite) {
2384
+ hadLeaseProtectedWrite = true;
2385
+ this.retainTransactionLockHolds(transaction);
2386
+ }
1820
2387
  // Record how long this commit stays outstanding (submit → settle) as a distribution
1821
2388
  // metric. This is the same clock the overload check uses (trackOutstandingCommit
1822
2389
  // stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
@@ -1836,7 +2403,10 @@ class DatabaseTransaction {
1836
2403
  try {
1837
2404
  commitResolution = transaction.abort();
1838
2405
  }
1839
- catch {
2406
+ catch (abortError) {
2407
+ if (abortError?.code === 'ERR_TRANSACTION_ABANDONED') {
2408
+ harperLogger.error?.(`Aborted a transaction with a durable log entry (empty write set), ${describeCommitIdentity(this.db, this.startedFrom, transaction)}`, abortError);
2409
+ }
1840
2410
  // The transaction has uncommitted writes that were already cleared from
1841
2411
  // this.writes by a concurrent immediate-commit path (e.g. writes made with
1842
2412
  // an explicitly-reused closed transaction). Those writes are handled by the
@@ -1849,38 +2419,53 @@ class DatabaseTransaction {
1849
2419
  // Read the table off the write itself, not this.db, which is whichever table first
1850
2420
  // claimed this per-database transaction in txnForContext and so can name the wrong
1851
2421
  // table when a transaction spans more than one table in the same database.
1852
- trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
2422
+ trackOutstandingCommit(commitResolution, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
1853
2423
  // Every retry round and every chained store re-enters here and must inherit the chain
1854
- // root's clock rather than restart it, so only the first submission stamps.
1855
- const chainRoot = this.root ?? this;
1856
- if (chainRoot.commitStartedAt == null)
1857
- chainRoot.commitStartedAt = performance.now();
2424
+ // root's clock rather than restart it (or the per-handle clock on an unchained
2425
+ // ImmediateTransaction), so only the first submission stamps.
2426
+ const carrier = this.budgetCarrier(transaction);
2427
+ if (carrier.commitStartedAt == null)
2428
+ carrier.commitStartedAt = performance.now();
1858
2429
  const completions = [];
1859
2430
  const commitOutcome = commitResolution.then((commitResult) => {
2431
+ const txnWithRetry = transaction;
1860
2432
  if (commitResult === RETRY_NOW_VALUE) {
1861
- this.retries++;
1862
- harperLogger.debug?.('coordinated retry', transaction.id, this.retries);
2433
+ this.setHandleCommitInFlight(transaction, false);
2434
+ const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
2435
+ if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
2436
+ this.retries = retries;
2437
+ harperLogger.debug?.('coordinated retry', transaction.id, retries);
1863
2438
  // Mark this specific native transaction as a retry so RocksTransactionLogStore
1864
2439
  // skips re-writing its already-staged txn-log entries (#2).
1865
- transaction.isRetry = true;
1866
- const pastBudget = this.elapsedPastCommitBudget();
1867
- if (pastBudget)
1868
- this.abandonCommitAfterDeadline(transaction, pastBudget);
1869
- // Mirror the ERR_BUSY cap/warn policy: non-sourceApply transactions abort
1870
- // at MAX_RETRIES; sourceApply transactions keep retrying with periodic warn.
1871
- if (this.retries > MAX_RETRIES) {
1872
- if (!this.sourceApply) {
1873
- // giving up: poison and abort the whole linked chain so no link leaks its native
1874
- // handle / read snapshot — or any unpublished transaction-log position — until GC.
1875
- this.abortChainAfterRetries(transaction);
1876
- throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} coordinated retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
1877
- }
1878
- if (this.retries % MAX_RETRIES === 0) {
1879
- harperLogger.warn?.(`Source-applied transaction ${transaction.id} still in conflict after ${this.retries} coordinated retries; continuing to retry`);
1880
- }
2440
+ txnWithRetry.isRetry = true;
2441
+ const logged = hasDurableLog(transaction);
2442
+ const pastBudget = this.elapsedPastCommitBudget(transaction);
2443
+ if (pastBudget) {
2444
+ if (logged)
2445
+ this.noteOverdueLoggedCommit(transaction, pastBudget);
2446
+ else
2447
+ this.abandonCommitAfterDeadline(transaction, pastBudget);
2448
+ }
2449
+ if (retries > MAX_RETRIES && !this.sourceApply && !logged) {
2450
+ // giving up: poison and abort the whole linked chain so no link leaks its native
2451
+ // handle / read snapshot — or any unpublished transaction-log position — until GC.
2452
+ this.abortChainAfterRetries(transaction);
2453
+ throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} coordinated retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
2454
+ }
2455
+ if ((this.sourceApply || logged) && retries > 0 && retries % MAX_RETRIES === 0) {
2456
+ harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} coordinated retries; continuing to retry`);
2457
+ }
2458
+ // Randomized so two uncapped logged commits crossing on the same keys stop waking in lockstep.
2459
+ if (logged && retries > 2) {
2460
+ const backoff = (0, promises_1.setTimeout)(Math.ceil(Math.min(retries * 2, MAX_RETRY_DELAY_MS) * Math.random()));
2461
+ trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
2462
+ return backoff.then(() => this.commit({ ...options, transaction }));
1881
2463
  }
1882
2464
  return this.commit({ ...options, transaction });
1883
2465
  }
2466
+ const committedTransaction = transaction;
2467
+ const committedImmediate = Boolean(committedTransaction?.immediateCommit);
2468
+ this.setHandleCommitted(transaction);
1884
2469
  // onCommit may be async (e.g. RocksTransactionLogStore emits 'aftercommit'). Surface a
1885
2470
  // rejection — or a synchronous throw — via logging rather than failing the commit, since
1886
2471
  // the write is already durable.
@@ -1892,7 +2477,14 @@ class DatabaseTransaction {
1892
2477
  catch (error) {
1893
2478
  harperLogger.warn?.('onCommit handler failed after commit', error);
1894
2479
  }
1895
- if (this.next) {
2480
+ if (this.recordLocks)
2481
+ this.noteCommittedLockVersions(committedTransaction);
2482
+ releaseTransactionLockHolds(transaction);
2483
+ if (committedImmediate
2484
+ ? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasSiblingCommitInFlight(committedTransaction)
2485
+ : !this.saveCommits || !this.hasSiblingCommitInFlight(committedTransaction))
2486
+ this.releaseRecordLocks();
2487
+ if (this.next && !committedImmediate) {
1896
2488
  // never forward options.transaction (a retry/replay round's HEAD-store handle) to
1897
2489
  // the next store — it must commit its own writes through its own transaction
1898
2490
  let nextCommit;
@@ -1915,7 +2507,7 @@ class DatabaseTransaction {
1915
2507
  // if we want to wait for replication confirmation, we need to track the transaction times
1916
2508
  // and when replication notifications come in, we count the number of confirms until we reach the desired number
1917
2509
  const databaseName = this.writes[0]?.store.rootStore.databaseName;
1918
- const lastEntry = this.lastConfirmableEntry();
2510
+ const lastEntry = this.lastConfirmableEntry(true);
1919
2511
  if (confirmReplication && databaseName && lastEntry) {
1920
2512
  completions.push(confirmReplication(databaseName, lastEntry.version, this.replicatedConfirmation));
1921
2513
  }
@@ -1926,24 +2518,36 @@ class DatabaseTransaction {
1926
2518
  // later write retained survives). Deferred until here so a retry that *would* have
1927
2519
  // referenced the blob can flip skipped/superseded back to false first.
1928
2520
  for (const write of this.writes) {
1929
- if (write?.savedBlobs && (write.skipped || (write.superseded && !write.blobsAuditReferenced)))
2521
+ if (!write?.saved || !write.savedBlobs)
2522
+ continue;
2523
+ if (committedImmediate
2524
+ ? write.stagedOn !== committedTransaction
2525
+ : Boolean(write.stagedOn) && write.stagedOn !== committedTransaction)
2526
+ continue;
2527
+ if ((write.skipped &&
2528
+ !(write.blobsAuditReferenced &&
2529
+ (write.appendedTo === committedTransaction || hasDurableLog(write.appendedTo)))) ||
2530
+ (write.superseded && !write.blobsAuditReferenced))
1930
2531
  (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
1931
2532
  }
1932
- if (this.recordLocks)
1933
- this.noteCommittedLockVersions();
2533
+ const committedStagedTxnTime = this.getHandleStagedTxnTime(committedTransaction);
1934
2534
  // now reset transactions tracking; this transaction be reused and committed again
1935
- this.retries = 0; // reset per-native-transaction retry counter so a reused DatabaseTransaction's next batch starts fresh
1936
- this.clearWrites();
1937
- this.releaseRecordLocks();
1938
- if (options.doneWriting)
1939
- this.endScopeOwnership();
1940
- this.releaseContext(!!options.doneWriting);
1941
- let txnTime = this.timestamp;
1942
- this.timestamp = 0; // reset the timestamp as well
2535
+ this.clearWrites(committedTransaction);
2536
+ if (!committedImmediate) {
2537
+ this.retries = 0;
2538
+ if (options.doneWriting)
2539
+ this.endScopeOwnership();
2540
+ this.releaseContext(!!options.doneWriting);
2541
+ }
2542
+ let txnTime = committedStagedTxnTime || this.timestamp || 0;
2543
+ if ((!committedImmediate || this.open !== exports.TRANSACTION_STATE.OPEN) &&
2544
+ this.writes.length === 0 &&
2545
+ !this.hasActiveCommitHandle())
2546
+ this.timestamp = 0; // reset the timestamp as well
1943
2547
  return Promise.all(completions).then(() => {
1944
2548
  // Only once the chained store's commit has settled, as on the synchronous path: a
1945
2549
  // partially failed mid-scope commit must not leave the scope resumable.
1946
- this.completeMidScopeCommit(options);
2550
+ this.completeMidScopeCommit(options, committedTransaction);
1947
2551
  return {
1948
2552
  txnTime,
1949
2553
  };
@@ -1951,10 +2555,14 @@ class DatabaseTransaction {
1951
2555
  // As on the synchronous path: a completion that failed (a chained store's commit,
1952
2556
  // a replication confirmation) leaves this commit partly landed, so ownership goes
1953
2557
  // with it rather than letting a later commit rotate on top.
1954
- this.endScopeOwnership();
2558
+ committedTransaction.committed = true;
2559
+ if (!committedImmediate)
2560
+ this.endScopeOwnership();
1955
2561
  throw error;
1956
2562
  });
1957
2563
  }, (error) => {
2564
+ const txnWithRetry = transaction;
2565
+ this.setHandleCommitInFlight(transaction, false);
1958
2566
  // Coordinated transactions surface conflicts as RETRY_NOW (handled in the
1959
2567
  // resolve branch above) and never reach here with ERR_BUSY. But not every
1960
2568
  // write transaction is coordinated — a write that reaches save() with no
@@ -1971,8 +2579,10 @@ class DatabaseTransaction {
1971
2579
  if (error.code === 'ERR_BUSY' || error.code === 'ERR_TRY_AGAIN') {
1972
2580
  // if the transaction failed due to concurrent changes, we need to retry. First record this as an increased risk of contention/retry
1973
2581
  // for future transactions
1974
- this.retries++;
1975
- harperLogger.debug?.('retrying', transaction.id, this.retries);
2582
+ const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
2583
+ if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
2584
+ this.retries = retries;
2585
+ harperLogger.debug?.('retrying', transaction.id, retries);
1976
2586
  // ERR_BUSY and ERR_TRY_AGAIN are both retried by recommitting the SAME native
1977
2587
  // transaction. ERR_BUSY recovers because the save loop re-writes each key, re-tracking
1978
2588
  // it at the current sequence. ERR_TRY_AGAIN — a snapshot stranded outside the memtable
@@ -1985,35 +2595,47 @@ class DatabaseTransaction {
1985
2595
  // already-staged change-feed entry publishes only when the retry really commits — no
1986
2596
  // premature publish, and no fresh-transaction replay that would drop the entry.
1987
2597
  // Mark the native transaction as a retry so RocksTransactionLogStore skips re-staging entries.
1988
- transaction.isRetry = true;
2598
+ txnWithRetry.isRetry = true;
2599
+ if (error?.hasLog === true) {
2600
+ txnWithRetry.hasStagedLog = true;
2601
+ }
2602
+ const logged = hasDurableLog(transaction, error);
1989
2603
  // Before the backoff gate below: the budget can already be spent on the first
1990
2604
  // retry when an earlier store in this chain consumed it.
1991
- const pastBudget = this.elapsedPastCommitBudget();
1992
- if (pastBudget)
1993
- this.abandonCommitAfterDeadline(transaction, pastBudget);
1994
- if (this.retries > 2) {
2605
+ const pastBudget = this.elapsedPastCommitBudget(transaction);
2606
+ if (pastBudget) {
2607
+ if (logged)
2608
+ this.noteOverdueLoggedCommit(transaction, pastBudget);
2609
+ else
2610
+ this.abandonCommitAfterDeadline(transaction, pastBudget);
2611
+ }
2612
+ if (retries > 2) {
1995
2613
  // Transactions applying data from a canonical source of truth (replication peer or
1996
2614
  // external caching source) must never drop a write on a transient conflict: there is no
1997
2615
  // re-subscribe / sequence-id-resume path, so a dropped write would leave this node
1998
2616
  // permanently diverged (harper-pro#348). Such transactions retry without a cap; the source
1999
2617
  // apply loop serializes commits (backpressure), so contention clears rather than
2000
- // compounding. Request-path transactions keep the MAX_RETRIES cap and surface a loud error.
2001
- const neverDropOnConflict = this.sourceApply;
2002
- if (this.retries > MAX_RETRIES) {
2003
- if (!neverDropOnConflict) {
2004
- // giving up: poison and abort the whole linked chain so no link leaks its native
2005
- // handle / read snapshot until GC.
2006
- this.abortChainAfterRetries(transaction);
2007
- throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
2008
- }
2009
- // Uncapped retry can otherwise stall silently (debug logging is off in production);
2010
- // surface periodic visibility into a stalled source-apply commit.
2011
- if (this.retries % MAX_RETRIES === 0) {
2012
- harperLogger.warn?.(`Source-applied transaction ${transaction.id} still in conflict after ${this.retries} retries; continuing to retry`);
2013
- }
2618
+ // compounding. Durably logged transactions likewise retry without a cap so a written
2619
+ // transaction-log entry is never abandoned.
2620
+ const neverDropOnConflict = this.sourceApply || logged;
2621
+ if (retries > MAX_RETRIES && !neverDropOnConflict) {
2622
+ // giving up: poison and abort the whole linked chain so no link leaks its native
2623
+ // handle / read snapshot until GC.
2624
+ this.abortChainAfterRetries(transaction);
2625
+ throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
2626
+ }
2627
+ // Uncapped retry can otherwise stall silently (debug logging is off in production);
2628
+ // surface periodic visibility into a stalled source-apply or logged commit.
2629
+ if (neverDropOnConflict && retries % MAX_RETRIES === 0) {
2630
+ harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} retries; continuing to retry`);
2014
2631
  }
2015
2632
  // start delaying, back off to try to space out transactions and avoid excessive conflicts
2016
- return (0, promises_1.setTimeout)(Math.min(this.retries * this.retries, MAX_RETRY_DELAY_MS)).then(() => this.commit({ ...options, transaction }));
2633
+ const backoff = (0, promises_1.setTimeout)(Math.min(retries * retries, MAX_RETRY_DELAY_MS));
2634
+ if (logged) {
2635
+ // waitForDrain() must outlast the backoff too
2636
+ trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
2637
+ }
2638
+ return backoff.then(() => this.commit({ ...options, transaction }));
2017
2639
  }
2018
2640
  return this.commit({ ...options, transaction }); // try again
2019
2641
  }
@@ -2021,54 +2643,87 @@ class DatabaseTransaction {
2021
2643
  // terminal (non-conflict) failure: release the native handle so it doesn't leak;
2022
2644
  // usually already released by the failed commit itself, abort for the unexpected
2023
2645
  // case (same defensive pattern as the retry-exhaustion give-up above)
2024
- try {
2025
- transaction.abort();
2026
- }
2027
- catch (abortError) {
2028
- harperLogger.debug?.('aborting transaction after failed commit', abortError);
2646
+ if (error?.hasLog) {
2647
+ txnWithRetry.isRetry = true;
2648
+ txnWithRetry.hasStagedLog = true;
2029
2649
  }
2650
+ abortNativeTransaction(transaction, 'aborting transaction after failed commit', this.writes[0]?.store ?? this.db, this.startedFrom);
2030
2651
  // A terminal failure is just as final as a success — release the context's
2031
2652
  // back-reference while handling the native transaction. performCommit's outer
2032
2653
  // rejection handler then aborts the wrapper and its linked chain.
2033
2654
  // A failed commit must never be followed by a resumed segment: this generation is
2034
2655
  // finished and its durability is unknown, so ownership goes with it.
2035
- this.endScopeOwnership();
2036
- this.releaseContext(!!options.doneWriting);
2037
- this.releaseRecordLocks();
2038
- this.timestamp = 0;
2656
+ if (!isImmediateCommit &&
2657
+ !this.hasWriteOwnedByActiveCommit(transaction, this.hasSiblingCommitInFlight(transaction), true, true)) {
2658
+ this.endScopeOwnership();
2659
+ this.releaseContext(!!options.doneWriting);
2660
+ }
2661
+ if (!hasDurableLog(transaction, error)) {
2662
+ for (const write of this.writes) {
2663
+ if (!write?.saved ||
2664
+ !write.savedBlobs ||
2665
+ (isImmediateCommit
2666
+ ? write.stagedOn !== transaction
2667
+ : Boolean(write.stagedOn) && write.stagedOn !== transaction))
2668
+ continue;
2669
+ (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
2670
+ }
2671
+ }
2672
+ this.clearWrites(transaction);
2673
+ releaseTransactionLockHolds(transaction);
2674
+ if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
2675
+ !this.hasActiveCommitHandle() &&
2676
+ this.writes.length === 0)
2677
+ this.releaseRecordLocks();
2678
+ if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle())
2679
+ this.timestamp = 0;
2039
2680
  throw error;
2040
2681
  }
2041
2682
  });
2042
- // `commitOutcome` settles when the LOGICAL commit ends — its success branch awaits the
2043
- // chained stores' own commits — so releasing here covers every terminal exit (success,
2044
- // retry exhaustion, abandonment, terminal failure) in one place rather than five.
2045
- // Released through the RETURNED promise rather than a second subscriber on
2046
- // `commitOutcome`: a subscriber would mark a dropped commit rejection as handled and
2047
- // silence the unhandled-rejection that surfaces it.
2048
- return commitOutcome.then((resolution) => {
2049
- chainRoot.commitStartedAt = undefined;
2050
- return resolution;
2051
- }, (error) => {
2052
- chainRoot.commitStartedAt = undefined;
2683
+ // Release any lock holds remaining if `commitOutcome` rejects (for example when a chained
2684
+ // store's commit or a completion fails) through the returned promise rather than a second
2685
+ // subscriber that would mark an unawaited commit rejection as handled.
2686
+ if (!hadLeaseProtectedWrite)
2687
+ return commitOutcome;
2688
+ return commitOutcome.then(undefined, (error) => {
2689
+ releaseTransactionLockHolds(transaction);
2053
2690
  throw error;
2054
2691
  });
2055
2692
  }
2056
2693
  for (const write of this.writes) {
2057
- if (write?.savedBlobs && (write.skipped || (write.superseded && !write.blobsAuditReferenced)))
2694
+ if (!write?.saved)
2695
+ continue;
2696
+ if (isImmediateCommit
2697
+ ? write.stagedOn !== transaction
2698
+ : this.hasSiblingCommitInFlight(transaction) && Boolean(write.stagedOn) && write.stagedOn !== transaction)
2699
+ continue;
2700
+ if (write.savedBlobs &&
2701
+ ((write.skipped &&
2702
+ !(write.blobsAuditReferenced && (write.appendedTo === transaction || hasDurableLog(write.appendedTo)))) ||
2703
+ (write.superseded && !write.blobsAuditReferenced)))
2058
2704
  (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
2059
2705
  }
2060
2706
  if (this.recordLocks)
2061
- this.noteCommittedLockVersions();
2062
- this.clearWrites();
2063
- this.releaseRecordLocks();
2064
- if (options.doneWriting)
2065
- this.endScopeOwnership();
2066
- this.releaseContext(!!options.doneWriting);
2707
+ this.noteCommittedLockVersions(transaction);
2708
+ const committedStagedTxnTime = this.getHandleStagedTxnTime(transaction);
2709
+ this.clearWrites(transaction);
2710
+ if (isImmediateCommit
2711
+ ? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle() && this.writes.length === 0
2712
+ : !this.saveCommits || (!this.hasActiveCommitHandle() && this.writes.length === 0))
2713
+ this.releaseRecordLocks();
2714
+ if (!isImmediateCommit) {
2715
+ if (options.doneWriting)
2716
+ this.endScopeOwnership();
2717
+ this.releaseContext(!!options.doneWriting);
2718
+ }
2067
2719
  const txnResolution = {
2068
- txnTime: this.timestamp,
2720
+ txnTime: committedStagedTxnTime || this.timestamp,
2069
2721
  };
2070
- this.timestamp = 0; // reset like the async path (~1279) so stale lock stamps don't persist
2071
- if (this.next) {
2722
+ if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
2723
+ this.writes.length === 0 &&
2724
+ !this.hasActiveCommitHandle())
2725
+ this.timestamp = 0;
2726
+ if (this.next && !isImmediateCommit) {
2072
2727
  // now run any other transactions
2073
2728
  options.timestamp = txnResolution.txnTime;
2074
2729
  // as above: the next store must not inherit this store's explicit native transaction
@@ -2087,7 +2742,7 @@ class DatabaseTransaction {
2087
2742
  return nextResolution?.then((nextResolution) => {
2088
2743
  // Only once the chained store's own commit has SETTLED: rotating first would leave the
2089
2744
  // scope resumable after a partially failed mid-scope commit.
2090
- this.completeMidScopeCommit(options);
2745
+ this.completeMidScopeCommit(options, transaction);
2091
2746
  return {
2092
2747
  txnTime: txnResolution.txnTime,
2093
2748
  next: nextResolution,
@@ -2102,18 +2757,20 @@ class DatabaseTransaction {
2102
2757
  });
2103
2758
  txnResolution.next = nextResolution;
2104
2759
  }
2105
- this.completeMidScopeCommit(options);
2760
+ this.completeMidScopeCommit(options, transaction);
2106
2761
  return txnResolution;
2107
2762
  });
2108
2763
  }
2109
2764
  catch (error) {
2110
- this.setCommitPhase(false);
2111
- this.abortAfterCommitError(error);
2765
+ if (inCommitPhase)
2766
+ this.setCommitPhase(false);
2767
+ this.abortAfterCommitError(error, transaction);
2112
2768
  }
2113
2769
  if (commitResult?.then)
2114
2770
  return commitResult.catch((error) => {
2115
- this.setCommitPhase(false);
2116
- this.abortAfterCommitError(error);
2771
+ if (inCommitPhase)
2772
+ this.setCommitPhase(false);
2773
+ this.abortAfterCommitError(error, transaction);
2117
2774
  });
2118
2775
  return commitResult;
2119
2776
  }
@@ -2141,8 +2798,12 @@ class DatabaseTransaction {
2141
2798
  this.#scopeOwned = false;
2142
2799
  this.snapshotFree = false;
2143
2800
  }
2144
- completeMidScopeCommit(options) {
2801
+ completeMidScopeCommit(options, committedTransaction) {
2802
+ if (committedTransaction?.immediateCommit)
2803
+ return;
2145
2804
  this.next = null;
2805
+ if (this.saveCommits && this.hasActiveCommitHandle())
2806
+ return;
2146
2807
  this.rotateAfterMidScopeCommit(options);
2147
2808
  }
2148
2809
  /** See completeMidScopeCommit, which is the only caller and carries the reasoning. */
@@ -2157,82 +2818,147 @@ class DatabaseTransaction {
2157
2818
  this.open = exports.TRANSACTION_STATE.OPEN;
2158
2819
  this.snapshotFree = true;
2159
2820
  this.writesAbandoned = false;
2821
+ if (this.#context && (this.#context.transaction === this || this.#context.transaction === exports.RELEASED_TRANSACTION)) {
2822
+ this.#context.transaction = this;
2823
+ }
2160
2824
  }
2161
2825
  assertDatabaseCommitAllowed(transaction) {
2826
+ if (hasDurableLog(transaction))
2827
+ return;
2162
2828
  const rootStore = this.writes[0]?.store?.rootStore;
2163
2829
  if (!databaseCommitsSuspended(rootStore))
2164
2830
  return;
2165
2831
  const retryable = !this.root && !this.snapshotFree;
2166
2832
  if (this.transaction === transaction)
2167
2833
  this.detachOwnedTransaction();
2168
- abortNativeTransaction(transaction, 'aborting a transaction while its database is closing');
2834
+ abortNativeTransaction(transaction, 'aborting a transaction while its database is closing', this.writes[0]?.store ?? this.db, this.startedFrom);
2169
2835
  try {
2170
- this.abort();
2836
+ this.abort(false, true, transaction);
2171
2837
  }
2172
2838
  catch (error) {
2173
2839
  harperLogger.debug?.('cleaning up a transaction while its database is closing', error);
2174
2840
  }
2175
2841
  throw new hdbError_ts_1.DatabaseClosingError(rootStore?.databaseName ?? rootStore?.path ?? 'unknown', retryable);
2176
2842
  }
2177
- abortAfterCommitError(error) {
2843
+ abortAfterCommitError(error, targetTransaction) {
2178
2844
  try {
2845
+ const retryTxn = targetTransaction;
2846
+ const retryTxnCommitted = this.isHandleCommitted(retryTxn);
2847
+ const retryTxnInFlight = this.isHandleCommitInFlight(retryTxn);
2848
+ if (retryTxn && retryTxnCommitted)
2849
+ retryTxn.committed = true;
2850
+ if (retryTxn)
2851
+ this.releaseActiveCommitHandle(retryTxn);
2852
+ if (retryTxn && retryTxn !== this.transaction && !retryTxnCommitted && !retryTxnInFlight) {
2853
+ releaseTransactionLockHolds(retryTxn);
2854
+ abortNativeTransaction(retryTxn, 'aborting a retry transaction after pre-commit failure', this.writes[0]?.store ?? this.db, this.startedFrom);
2855
+ }
2179
2856
  // Not "always retain": abort(true) keeps the native handle only while read iterators still
2180
2857
  // own it. resources/transaction.ts's own post-error abort passes the same argument, so the
2181
2858
  // two layers cannot contradict each other one frame apart.
2182
- this.abort(true);
2859
+ this.abort(true, true, targetTransaction);
2183
2860
  }
2184
2861
  catch (abortError) {
2185
2862
  harperLogger.debug?.('aborting transaction after a failed commit', abortError);
2186
2863
  }
2187
2864
  throw error;
2188
2865
  }
2189
- abort(retainReadTransaction = false, cascade = true) {
2190
- const next = cascade ? this.next : undefined;
2191
- if (cascade && !retainReadTransaction)
2866
+ abort(retainReadTransaction = false, cascade = true, targetTransaction) {
2867
+ const nativeTxn = targetTransaction ?? this.transaction;
2868
+ const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
2869
+ const hasSiblingInFlight = this.hasSiblingCommitInFlight(targetTransaction);
2870
+ const preserveActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true);
2871
+ const preserveNonImmediateActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true, true);
2872
+ const cascadeNext = cascade && !preserveNonImmediateActiveWrites;
2873
+ const next = cascadeNext ? this.next : undefined;
2874
+ if (cascadeNext && !retainReadTransaction)
2192
2875
  this.next = null;
2193
2876
  try {
2194
- const hasOpenReadIterator = retainReadTransaction &&
2195
- this.transaction &&
2196
- (this.readTxnsUsed > 1 || (this.baseReadRefConsumed && this.readTxnsUsed > 0));
2197
- if (hasOpenReadIterator) {
2198
- this.releaseRetainedWriteIntents();
2199
- if (!this.baseReadRefConsumed) {
2200
- this.doneReadTxn();
2201
- this.baseReadRefConsumed = true;
2877
+ const ownsCurrentTransaction = !isImmediateTarget && (!hasSiblingInFlight || !targetTransaction || this.transaction === targetTransaction);
2878
+ const currentTxn = this.transaction;
2879
+ const currentTxnActive = Boolean(currentTxn) &&
2880
+ !currentTxn.abortedByWrapper &&
2881
+ !this.isHandleCommitted(currentTxn) &&
2882
+ (this.isHandleCommitInFlight(currentTxn) ||
2883
+ hasDurableLog(currentTxn) ||
2884
+ (Boolean(targetTransaction) && currentTxn !== targetTransaction && this.isActiveCommitHandle(currentTxn)));
2885
+ if (ownsCurrentTransaction && !currentTxnActive) {
2886
+ if (this.transaction)
2887
+ this.releaseActiveCommitHandle(this.transaction);
2888
+ const hasOpenReadIterator = retainReadTransaction &&
2889
+ this.transaction &&
2890
+ (this.readTxnsUsed > 1 || (this.baseReadRefConsumed && this.readTxnsUsed > 0));
2891
+ if (hasOpenReadIterator) {
2892
+ this.releaseRetainedWriteIntents();
2893
+ if (!this.baseReadRefConsumed) {
2894
+ this.doneReadTxn();
2895
+ this.baseReadRefConsumed = true;
2896
+ }
2202
2897
  }
2898
+ else {
2899
+ while (this.transaction && this.readTxnsUsed > 0)
2900
+ this.doneReadTxn(); // release the read snapshot when we abort, we assume we don't need it
2901
+ }
2902
+ // A write-only transaction never took a read reference (getReadTxn was never called), so the
2903
+ // loop above releases nothing even though save() created a native handle. Keep an iterator's
2904
+ // handle alive, but release every other remaining handle through the shared cleanup path.
2905
+ if (this.transaction && !hasOpenReadIterator)
2906
+ this.releaseReadTxn();
2203
2907
  }
2204
- else {
2205
- while (this.readTxnsUsed > 0)
2206
- this.doneReadTxn(); // release the read snapshot when we abort, we assume we don't need it
2207
- }
2208
- // A write-only transaction never took a read reference (getReadTxn was never called), so the
2209
- // loop above releases nothing even though save() created a native handle. Keep an iterator's
2210
- // handle alive, but release every other remaining handle through the shared cleanup path.
2211
- if (this.transaction && !hasOpenReadIterator)
2212
- this.releaseReadTxn();
2213
- this.open = exports.TRANSACTION_STATE.CLOSED;
2214
- this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
2215
- this.drainCompletions();
2908
+ if (!isImmediateTarget)
2909
+ this.open = exports.TRANSACTION_STATE.CLOSED;
2910
+ this.drainCompletions(targetTransaction, preserveActiveWrites);
2911
+ const targetLogged = hasDurableLog(targetTransaction);
2216
2912
  for (const write of this.writes) {
2217
- if (write?.savedBlobs)
2913
+ if (!write)
2914
+ continue;
2915
+ if (preserveActiveWrites && this.isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, true))
2916
+ continue;
2917
+ if (targetLogged &&
2918
+ write.saved &&
2919
+ (isImmediateTarget
2920
+ ? write.stagedOn === targetTransaction
2921
+ : !write.stagedOn || write.stagedOn === targetTransaction))
2922
+ continue;
2923
+ if (write.savedBlobs)
2218
2924
  (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
2219
2925
  }
2220
2926
  }
2221
2927
  finally {
2222
2928
  try {
2223
- this.open = exports.TRANSACTION_STATE.CLOSED;
2224
- this.endScopeOwnership(); // the scope is over; nothing may rotate this instance again
2225
- this.clearWrites();
2226
- this.releaseRecordLocks();
2929
+ if (!isImmediateTarget)
2930
+ this.open = exports.TRANSACTION_STATE.CLOSED;
2931
+ const endScopeOnAbort = !isImmediateTarget && (!targetTransaction || !preserveNonImmediateActiveWrites);
2932
+ if (endScopeOnAbort)
2933
+ this.endScopeOwnership(); // the scope is over; nothing may rotate this instance again
2934
+ this.clearWrites(targetTransaction, true);
2935
+ if (targetTransaction) {
2936
+ releaseTransactionLockHolds(targetTransaction);
2937
+ }
2938
+ if ((!isImmediateTarget || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle()) {
2939
+ this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
2940
+ this.retries = 0;
2941
+ }
2942
+ if ((isImmediateTarget
2943
+ ? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle()
2944
+ : !preserveActiveWrites) &&
2945
+ this.writes.length === 0) {
2946
+ releaseTransactionLockHolds(nativeTxn);
2947
+ if (this.transaction && this.transaction !== nativeTxn)
2948
+ releaseTransactionLockHolds(this.transaction);
2949
+ this.releaseRecordLocks();
2950
+ }
2227
2951
  // A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
2228
2952
  // Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
2229
2953
  // starting a fresh one) so the rest of the logical operation fails atomically via the
2230
2954
  // poison check in addWrite()/commit(), rather than silently landing a later write on a
2231
2955
  // brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
2232
2956
  // make that check see `undefined?.timedOut` and take the "start fresh" branch instead.
2233
- this.releaseContext(!this.timedOut && !this.disconnected);
2234
- if (!this.root)
2235
- this.releaseRequestAbortListener();
2957
+ if (endScopeOnAbort) {
2958
+ this.releaseContext(!this.timedOut && !this.disconnected);
2959
+ if (!this.root)
2960
+ this.releaseRequestAbortListener();
2961
+ }
2236
2962
  }
2237
2963
  finally {
2238
2964
  if (next && !(next.nativeCommitSubmitted && next.commitsInFlight)) {
@@ -2259,10 +2985,10 @@ class DatabaseTransaction {
2259
2985
  * Source-applied writes are exempt for the same reason they are exempt from the attempt cap:
2260
2986
  * there is no resubscribe/sequence-resume path, so dropping one permanently diverges this node.
2261
2987
  */
2262
- elapsedPastCommitBudget() {
2988
+ elapsedPastCommitBudget(transaction) {
2263
2989
  if (this.sourceApply)
2264
2990
  return 0;
2265
- const startedAt = (this.root ?? this).commitStartedAt;
2991
+ const startedAt = this.budgetCarrier(transaction).commitStartedAt;
2266
2992
  if (startedAt == null)
2267
2993
  return 0;
2268
2994
  const elapsed = performance.now() - startedAt;
@@ -2283,18 +3009,54 @@ class DatabaseTransaction {
2283
3009
  const elapsed = Math.round(elapsedMs);
2284
3010
  const budget = this.commitConflictBudget();
2285
3011
  const retryable = !this.root && !this.snapshotFree;
3012
+ const retries = headTransaction?.retries ?? this.retries;
2286
3013
  if (allowStuckCommitLog('abandon', performance.now())) {
2287
- harperLogger.error(`Abandoning a write transaction: its commit has been in write-intent conflict for ${elapsed}ms ` +
2288
- `(exceeds the ${budget}ms limit) across ${this.retries} retries, ` +
3014
+ let headTransactionId;
3015
+ try {
3016
+ headTransactionId = headTransaction?.id;
3017
+ }
3018
+ catch { }
3019
+ harperLogger.error?.(`Abandoning a write transaction: its commit has been in write-intent conflict for ${elapsed}ms ` +
3020
+ `(exceeds the ${budget}ms limit) across ${retries} retries, ` +
2289
3021
  describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
2290
3022
  `.` +
2291
- (0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransaction?.id) +
3023
+ (0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
2292
3024
  ` Another transaction holds a conflicting write intent and has not completed; the request is ` +
2293
3025
  `failed with a 503${retryable ? '' : ' (not retryable — an earlier store in this transaction already committed)'} ` +
2294
3026
  `rather than waiting further.`);
2295
3027
  }
2296
3028
  this.abortChainAfterRetries(headTransaction);
2297
- throw new hdbError_ts_1.TransactionCommitConflictTimeoutError(`Commit was in conflict with ongoing writes for ${elapsed}ms, exceeding the ${budget}ms limit; transaction abandoned after ${this.retries} retries`, retryable);
3029
+ throw new hdbError_ts_1.TransactionCommitConflictTimeoutError(`Commit was in conflict with ongoing writes for ${elapsed}ms, exceeding the ${budget}ms limit; transaction abandoned after ${retries} retries`, retryable);
3030
+ }
3031
+ /**
3032
+ * A durably logged commit past its conflict budget cannot be aborted; release its deferred
3033
+ * record-lock holds (`holdBudgetSpent` prevents re-retaining them) and log once per chain/cooldown.
3034
+ */
3035
+ noteOverdueLoggedCommit(headTransaction, elapsedMs) {
3036
+ headTransaction.holdBudgetSpent = true;
3037
+ releaseTransactionLockHolds(headTransaction);
3038
+ const carrier = this.budgetCarrier(headTransaction);
3039
+ if (carrier.commitOverdueLogged)
3040
+ return;
3041
+ if (!allowStuckCommitLog('overdue', performance.now()))
3042
+ return;
3043
+ carrier.commitOverdueLogged = true;
3044
+ const elapsed = Math.round(elapsedMs);
3045
+ const budget = this.commitConflictBudget();
3046
+ const retries = headTransaction?.retries ?? this.retries;
3047
+ let headTransactionId;
3048
+ try {
3049
+ headTransactionId = headTransaction?.id;
3050
+ }
3051
+ catch { }
3052
+ harperLogger.error?.(`A durably logged write transaction is still in write-intent conflict after ${elapsed}ms ` +
3053
+ `(exceeds the ${budget}ms limit) across ${retries} retries, ` +
3054
+ describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
3055
+ `.` +
3056
+ (0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
3057
+ ` Because its transaction-log entry is already durable on disk, it cannot be aborted and ` +
3058
+ `will continue retrying until the conflicting write intent releases (pinning the local ` +
3059
+ `transaction-log read watermark for subscriptions and replication while uncommitted).`);
2298
3060
  }
2299
3061
  /**
2300
3062
  * Give up on a chain of linked transactions after exhausting conflict retries: poison every link
@@ -2307,36 +3069,51 @@ class DatabaseTransaction {
2307
3069
  * from the replay transaction; retry exhaustion aborts both.
2308
3070
  */
2309
3071
  abortChainAfterRetries(headTransaction) {
3072
+ const isImmediateCommit = Boolean(headTransaction?.immediateCommit);
3073
+ const hasSiblingInFlight = this.hasSiblingCommitInFlight(headTransaction);
3074
+ if (isImmediateCommit ||
3075
+ (hasSiblingInFlight && this.hasWriteOwnedByActiveCommit(headTransaction, hasSiblingInFlight, true, true))) {
3076
+ if (!isImmediateCommit && this.transaction === headTransaction)
3077
+ this.detachOwnedTransaction();
3078
+ releaseTransactionLockHolds(headTransaction);
3079
+ abortNativeTransaction(headTransaction, 'aborting conflicted transaction in chain after exhausting retries', this.writes[0]?.store ?? this.db, this.startedFrom);
3080
+ try {
3081
+ this.abort(false, false, headTransaction);
3082
+ }
3083
+ catch (abortError) {
3084
+ harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
3085
+ }
3086
+ return;
3087
+ }
2310
3088
  for (let txn = this; txn; txn = txn.next) {
2311
3089
  txn.open = exports.TRANSACTION_STATE.CLOSED;
2312
3090
  }
2313
- for (let txn = this; txn; txn = txn.next) {
3091
+ for (let txn = this, next; txn; txn = next) {
3092
+ next = txn.next;
3093
+ txn.next = null;
2314
3094
  // Detach first so the abort() below performs only non-native cleanup, and so its doneReadTxn
2315
3095
  // loop cannot spin on a nulled handle. Not to avoid a double abort: rocksdb-js tolerates
2316
3096
  // abort-after-abort, and it is abort-after-COMMIT that throws.
2317
- const detached = txn.detachOwnedTransaction();
3097
+ const detached = txn === this &&
3098
+ hasSiblingInFlight &&
3099
+ txn.transaction &&
3100
+ txn.transaction !== headTransaction &&
3101
+ (this.isActiveCommitHandle(txn.transaction) || hasDurableLog(txn.transaction))
3102
+ ? null
3103
+ : txn.detachOwnedTransaction();
2318
3104
  const committingTransaction = txn === this ? headTransaction : detached;
2319
- try {
2320
- committingTransaction?.abort();
2321
- }
2322
- catch (abortError) {
2323
- harperLogger.debug?.('aborting conflicted transaction in chain after exhausting retries', abortError);
2324
- }
3105
+ releaseTransactionLockHolds(committingTransaction);
3106
+ abortNativeTransaction(committingTransaction, 'aborting conflicted transaction in chain after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
2325
3107
  // With outstanding iterators, the head owns a retained read handle while the retry commits
2326
3108
  // through a separate replay handle. Both must be aborted after retry exhaustion.
2327
3109
  if (txn === this && detached && detached !== committingTransaction) {
2328
- try {
2329
- detached.abort();
2330
- }
2331
- catch (abortError) {
2332
- harperLogger.debug?.('aborting retained read transaction after exhausting retries', abortError);
2333
- }
3110
+ abortNativeTransaction(detached, 'aborting retained read transaction after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
2334
3111
  }
2335
3112
  try {
2336
3113
  // abort() synchronously walks savedBlobs and can call write.store.getEntry(), which can throw
2337
3114
  // (closed store, decode error). Catch and continue so one link's wrapper-cleanup failure can't
2338
3115
  // strand later links' native handles — they were already detached/aborted above regardless.
2339
- txn.abort();
3116
+ txn.abort(false, false, committingTransaction);
2340
3117
  }
2341
3118
  catch (abortError) {
2342
3119
  harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
@@ -2430,18 +3207,7 @@ class DatabaseTransaction {
2430
3207
  */
2431
3208
  poisonAfterStalledSubmittedCommit() {
2432
3209
  const root = this.root ?? this;
2433
- const links = new Set();
2434
- const collect = (start) => {
2435
- for (let txn = start; txn; txn = txn.next)
2436
- links.add(txn);
2437
- };
2438
- collect(root);
2439
- collect(this);
2440
- collect(root.submittedLink);
2441
- if (root.submittedLinks)
2442
- for (const txn of root.submittedLinks)
2443
- collect(txn);
2444
- for (const txn of links) {
3210
+ for (const txn of collectCommitLinks(root, this)) {
2445
3211
  txn.postSubmitPoisoned = true;
2446
3212
  // Not on a link an earlier poison already aborted: abort() cleared its write set, so excusing
2447
3213
  // its continuation from the poison check would let it resume onto nothing and report success.
@@ -2465,7 +3231,7 @@ class DatabaseTransaction {
2465
3231
  // throws. abort() rather than the native abort alone: it reclaims blobs a replayed write
2466
3232
  // staged.
2467
3233
  this.detachOwnedTransaction();
2468
- abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed');
3234
+ abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed', this.writes[0]?.store ?? this.db, this.startedFrom);
2469
3235
  try {
2470
3236
  this.abort();
2471
3237
  }
@@ -2505,10 +3271,12 @@ class ImmediateTransaction extends DatabaseTransaction {
2505
3271
  }
2506
3272
  save(...args) {
2507
3273
  const operation = args[0]; // the staged write, not a transaction — commit() re-enters here with it
2508
- if (this.isCommitting) {
3274
+ if (operation?.deferSave)
3275
+ operation.deferSave = false;
3276
+ if (this.isCommitting || args[1] || args[3]) {
2509
3277
  // Stage into the transaction commit() is committing (on a retry round, the one being retried)
2510
3278
  // and reload within it; with no handle super.save() would open its own and nest a commit().
2511
- super.save(operation, args[1], true);
3279
+ super.save(operation, args[1], true, args[3]);
2512
3280
  }
2513
3281
  else {
2514
3282
  this.admitRequestWrite(); // a deferred save reaches here without passing through addWrite()
@@ -2542,7 +3310,7 @@ class ImmediateTransaction extends DatabaseTransaction {
2542
3310
  return;
2543
3311
  for (const [store, storeMap] of recordLocks) {
2544
3312
  for (const [keyId, handle] of storeMap)
2545
- if (handle.released)
3313
+ if (handle.released || handle.isExpired())
2546
3314
  storeMap.delete(keyId);
2547
3315
  if (storeMap.size === 0)
2548
3316
  recordLocks.delete(store);
@@ -2704,6 +3472,12 @@ function reportIfLongLived(txn, thresholdMs, now, reportBudget) {
2704
3472
  /** Diagnostic grace after a native submission outlives the ordinary transaction timeout. */
2705
3473
  const MAX_DEFERRED_POISON_TICKS = 2;
2706
3474
  const STALLED_COMMIT_LOG_MIN_INTERVAL_MS = 1000;
3475
+ // A logged retry parked on a write intent moves its retry counter at least every native park timeout,
3476
+ // so a counter that has not moved for two of them is a native attempt that never settled — whatever
3477
+ // the monitor's tick interval (STORAGE_MAXTRANSACTIONOPENTIME can be set far below the park timeout).
3478
+ const configuredParkTimeoutMs = Number(process.env.ROCKSDB_JS_PARK_TIMEOUT_MS);
3479
+ // Same acceptance rule as rocksdb-js: anything but a finite positive number falls back to its 5000 ms default.
3480
+ const LOGGED_RETRY_STALL_MS = 2 * (Number.isFinite(configuredParkTimeoutMs) && configuredParkTimeoutMs > 0 ? configuredParkTimeoutMs : 5000);
2707
3481
  let lastStalledCommitWarningAt = -Infinity;
2708
3482
  let lastStalledCommitErrorAt = -Infinity;
2709
3483
  function submittedCommitStillInFlight(root) {
@@ -2717,12 +3491,36 @@ function submittedCommitStillInFlight(root) {
2717
3491
  }
2718
3492
  return false;
2719
3493
  }
3494
+ // Every link the monitor would poison for a stalled commit, so the two walks agree on which links count.
3495
+ function collectCommitLinks(root, txn) {
3496
+ const links = new Set();
3497
+ const collect = (start) => {
3498
+ for (let link = start; link; link = link.next)
3499
+ links.add(link);
3500
+ };
3501
+ collect(root);
3502
+ collect(txn);
3503
+ collect(root.submittedLink);
3504
+ if (root.submittedLinks)
3505
+ for (const link of root.submittedLinks)
3506
+ collect(link);
3507
+ return links;
3508
+ }
3509
+ function chainDurableLogRetries(root, txn) {
3510
+ let retries = -1;
3511
+ for (const link of collectCommitLinks(root, txn)) {
3512
+ const linkRetries = link.activeDurableLogRetries();
3513
+ if (linkRetries >= 0)
3514
+ retries = Math.max(retries, 0) + linkRetries;
3515
+ }
3516
+ return retries;
3517
+ }
2720
3518
  /**
2721
3519
  * True when this tick must leave `txn` alone because some link has submitted writes whose native
2722
3520
  * outcome is not known yet. Merely entering commit() is not enough: a wholly pre-submit transaction
2723
3521
  * remains subject to shouldSpareCommitPhase() and its ordinary bounded grace.
2724
3522
  */
2725
- function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
3523
+ function deferForCommitInFlight(txn, url, expiration = txnExpiration, tick = monitorTick) {
2726
3524
  const root = txn.root ?? txn;
2727
3525
  if (!txn.isChainCommitting() || !root.commitSubmitted)
2728
3526
  return false;
@@ -2735,27 +3533,36 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
2735
3533
  // diagnostic grace, nor report a legitimately slow one early.
2736
3534
  const now = performance.now();
2737
3535
  const deadline = (root.deferredPoisonDeadline ??= now + MAX_DEFERRED_POISON_TICKS * expiration);
3536
+ // A handle with a durable log entry retrying a conflict has a known outcome — it commits once the
3537
+ // conflicting intent clears — so while its retry counter keeps advancing it is neither stalled nor
3538
+ // poisoned, and noteOverdueLoggedCommit is the operator's signal. A counter that has not moved for
3539
+ // LOGGED_RETRY_STALL_MS is a native attempt that never settled, which is the stall handled below.
3540
+ const loggedRetries = chainDurableLogRetries(root, txn);
3541
+ let loggedRetry = false;
3542
+ if (loggedRetries >= 0) {
3543
+ if (root.loggedRetriesTick === tick) {
3544
+ loggedRetry = Boolean(root.loggedRetryAdvancing);
3545
+ }
3546
+ else {
3547
+ if (root.loggedRetriesSeen !== loggedRetries) {
3548
+ root.loggedRetriesSeen = loggedRetries;
3549
+ root.loggedRetriesChangedAt = now;
3550
+ }
3551
+ loggedRetry = now - root.loggedRetriesChangedAt < LOGGED_RETRY_STALL_MS;
3552
+ root.loggedRetriesTick = tick;
3553
+ root.loggedRetryAdvancing = loggedRetry;
3554
+ }
3555
+ }
2738
3556
  let hasUnsubmittedCommit = false;
2739
3557
  let unsubmittedCommitGraceExpired = false;
2740
3558
  if (now >= deadline) {
2741
3559
  // Only reclaim after the commit has genuinely stalled. Submitted writes no longer depend on these
2742
3560
  // separate snapshots; pre-submit handles can still carry writes needed by the attempt.
2743
3561
  txn.releaseStalledCommitReadResources();
2744
- if (!txn.isProtectedCommit()) {
3562
+ if (!loggedRetry && !txn.isProtectedCommit()) {
2745
3563
  if (!root.postSubmitPoisoned)
2746
3564
  txn.poisonAfterStalledSubmittedCommit();
2747
- const links = new Set();
2748
- const collect = (start) => {
2749
- for (let link = start; link; link = link.next)
2750
- links.add(link);
2751
- };
2752
- collect(root);
2753
- collect(txn);
2754
- collect(root.submittedLink);
2755
- if (root.submittedLinks)
2756
- for (const link of root.submittedLinks)
2757
- collect(link);
2758
- for (const link of links) {
3565
+ for (const link of collectCommitLinks(root, txn)) {
2759
3566
  if (link.committing && !link.nativeCommitSubmitted) {
2760
3567
  hasUnsubmittedCommit = true;
2761
3568
  break;
@@ -2775,14 +3582,16 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
2775
3582
  txn.open === exports.TRANSACTION_STATE.CLOSED &&
2776
3583
  !(txn === root && submittedCommitStillInFlight(root)))
2777
3584
  return hasUnsubmittedCommit && !unsubmittedCommitGraceExpired;
2778
- if (!root.stalledCommitLogged && now - lastStalledCommitErrorAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
3585
+ if (!loggedRetry &&
3586
+ !root.stalledCommitLogged &&
3587
+ now - lastStalledCommitErrorAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
2779
3588
  lastStalledCommitErrorAt = now;
2780
3589
  root.stalledCommitLogged = true;
2781
3590
  harperLogger.error(`A native commit has not settled after exceeding the open-transaction limit by a further ${MAX_DEFERRED_POISON_TICKS * expiration}ms. Its outcome is unknown, so Harper will not abort or delete its writes; fresh ordinary work ` +
2782
3591
  `on this transaction is rejected until it settles, and the worker may require a restart, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
2783
3592
  }
2784
3593
  }
2785
- else if (now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
3594
+ else if (!loggedRetry && now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
2786
3595
  lastStalledCommitWarningAt = now;
2787
3596
  harperLogger.warn?.(`Transaction exceeded the open-transaction limit after submitting a native commit; preserving its unknown outcome, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
2788
3597
  }
@@ -2933,10 +3742,20 @@ function setMaxOutstandingTxnDuration(ms) {
2933
3742
  MAX_OUTSTANDING_TXN_DURATION = ms;
2934
3743
  return previous;
2935
3744
  }
3745
+ /** Test seam: overrides the per-retry backoff cap and returns the previous value. */
3746
+ function setMaxRetryDelayMs(ms) {
3747
+ const previous = MAX_RETRY_DELAY_MS;
3748
+ MAX_RETRY_DELAY_MS = ms;
3749
+ return previous;
3750
+ }
2936
3751
  /** Test seam: whether the monitor supervises this logical transaction for its writes. */
2937
3752
  function isWriteSupervised(txn) {
2938
3753
  return supervisedWriteRoots.has(txn);
2939
3754
  }
3755
+ /** Test seam: returns the current open-transaction monitor interval. */
3756
+ function getTxnExpiration() {
3757
+ return txnExpiration;
3758
+ }
2940
3759
  function setTxnExpiration(ms) {
2941
3760
  clearInterval(timer);
2942
3761
  txnExpiration = ms;