@harperfast/harper 5.3.2 → 5.3.3

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 (251) 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 +75 -10
  21. package/dist/resources/DatabaseTransaction.js +1080 -277
  22. package/dist/resources/DatabaseTransaction.js.map +1 -1
  23. package/dist/resources/Resource.js +40 -14
  24. package/dist/resources/Resource.js.map +1 -1
  25. package/dist/resources/RocksTransactionLogStore.js +1 -0
  26. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  27. package/dist/resources/Table.js +16 -2
  28. package/dist/resources/Table.js.map +1 -1
  29. package/dist/resources/analytics/write.js +34 -16
  30. package/dist/resources/analytics/write.js.map +1 -1
  31. package/dist/resources/databases.d.ts +5 -0
  32. package/dist/resources/databases.js +38 -4
  33. package/dist/resources/databases.js.map +1 -1
  34. package/dist/resources/recordLock.d.ts +9 -4
  35. package/dist/resources/recordLock.js +79 -19
  36. package/dist/resources/recordLock.js.map +1 -1
  37. package/dist/server/DurableSubscriptionsSession.d.ts +2 -0
  38. package/dist/server/DurableSubscriptionsSession.js +20 -17
  39. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  40. package/dist/server/mqtt.js +41 -23
  41. package/dist/server/mqtt.js.map +1 -1
  42. package/dist/server/serverHelpers/NodeAdapterResponse.js +18 -0
  43. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -1
  44. package/dist/utility/npmUtilities.js +18 -7
  45. package/dist/utility/npmUtilities.js.map +1 -1
  46. package/package.json +1 -1
  47. package/resources/DESIGN.md +42 -10
  48. package/resources/DatabaseTransaction.ts +1244 -275
  49. package/resources/Resource.ts +39 -14
  50. package/resources/RocksTransactionLogStore.ts +1 -0
  51. package/resources/Table.ts +24 -2
  52. package/resources/analytics/DESIGN.md +17 -0
  53. package/resources/analytics/write.ts +28 -15
  54. package/resources/databases.ts +40 -4
  55. package/resources/record-locks.md +34 -17
  56. package/resources/recordLock.ts +89 -22
  57. package/server/DESIGN.md +2 -0
  58. package/server/DurableSubscriptionsSession.ts +18 -16
  59. package/server/mqtt.ts +39 -22
  60. package/server/serverHelpers/NodeAdapterResponse.ts +19 -1
  61. package/studio/web/assets/Chat-B-Rd-ft9.js +2100 -0
  62. package/studio/web/assets/FloatingChat-CFypO7kh.js +23 -0
  63. package/studio/web/assets/SearchDialog-BRIyPiBl.js +1 -0
  64. package/studio/web/assets/{abnfDiagram-O67JEVCF-raXE027J.js → abnfDiagram-YUKMZFIV-DPR2lFRk.js} +1 -1
  65. package/studio/web/assets/{apiToken-DNrfij0E.js → apiToken-BraYnzvC.js} +1 -1
  66. package/studio/web/assets/applications-XCds8-ZP.js +296 -0
  67. package/studio/web/assets/architecture-WOLXFQ4H-DtJHIK0_.js +1 -0
  68. package/studio/web/assets/{architectureDiagram-NJMV4G6O-C8_Eet7w.js → architectureDiagram-47P4ROYG-MDymp2dh.js} +3 -3
  69. package/studio/web/assets/authStore-DE1HFvVP.js +3 -0
  70. package/studio/web/assets/{blockDiagram-BEXU5L5S-DR6S5-1D.js → blockDiagram-E7TT5TSB-O5qRgZmd.js} +3 -3
  71. package/studio/web/assets/button-C37w80Uk.js +21 -0
  72. package/studio/web/assets/{c4Diagram-YGBWAQC7-BAfZ1G01.js → c4Diagram-CRA5TL53-BdPORGHo.js} +2 -2
  73. package/studio/web/assets/channel-yCStoq0_.js +1 -0
  74. package/studio/web/assets/{chevron-up-CFxbMGv2.js → chevron-up-DozwrIz-.js} +1 -1
  75. package/studio/web/assets/chunk-2BW5OAIV-DJ-iXJAw.js +10 -0
  76. package/studio/web/assets/{chunk-ZIGJFQKS-Cs8fUn7y.js → chunk-3YJQHVM4-BOgacqS1.js} +2 -2
  77. package/studio/web/assets/{chunk-7PRAP22T-KyqX6x3f.js → chunk-4EA7E6EY-C-7xqUBl.js} +1 -1
  78. package/studio/web/assets/{chunk-GWA4HPMP-DsejB6gA.js → chunk-7TKQ45FW-C3XA1bIN.js} +1 -1
  79. package/studio/web/assets/{chunk-GTNCS2PH-DGHLXyow.js → chunk-AW2ZBBNX-BdvgD1UW.js} +3 -3
  80. package/studio/web/assets/{chunk-5DYCD2WN-BceBfIHT.js → chunk-DBDB3WZW-CKKxIc6F.js} +1 -1
  81. package/studio/web/assets/{chunk-MBY4JIJT-BwSGweQP.js → chunk-E2ZNV5FY-bVefNHvz.js} +3 -3
  82. package/studio/web/assets/chunk-EU5HNXII-BHv2kWp4.js +213 -0
  83. package/studio/web/assets/chunk-FVRAUYC3-BoL_NO6b.js +2 -0
  84. package/studio/web/assets/{chunk-742MDFTN-CIkt8ijt.js → chunk-HJ2JQQFS-N_98oMRS.js} +1 -1
  85. package/studio/web/assets/{chunk-NETBCI7D-C8UnOw_f.js → chunk-HTAEGDNF-BQ1fSjPs.js} +1 -1
  86. package/studio/web/assets/{chunk-WEXAMYUT-BAIhTwyh.js → chunk-KQW6MTUR-qP0QiQAg.js} +1 -1
  87. package/studio/web/assets/chunk-NGNAAXSQ-DqoPQk1a.js +136 -0
  88. package/studio/web/assets/{chunk-UA2S7LBM-BHpEdH8D.js → chunk-NTY3LDVX-CSBFjlJg.js} +1 -1
  89. package/studio/web/assets/chunk-VPRB5NB3-BVHOe9Q-.js +129 -0
  90. package/studio/web/assets/{chunk-7INBJB4K-BpeAXyOi.js → chunk-XC4XBNZT-46uQwtF4.js} +10 -10
  91. package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DmDPkt2u.js → classDiagram-v2-K4WV4PDN-Da0ps1Ui.js} +3 -3
  92. package/studio/web/assets/cn-DpF1PxBT.js +1 -0
  93. package/studio/web/assets/{createLucideIcon-fO5dL7D5.js → createLucideIcon-DLWRoWN-.js} +1 -1
  94. package/studio/web/assets/{cssMode-DBucrQdT.js → cssMode-BY1w91l8.js} +1 -1
  95. package/studio/web/assets/cynefin-EF2NZ3EQ-jQ2eCUaU.js +1 -0
  96. package/studio/web/assets/cynefinDiagram-3GCD6N5R-TNi5iS0E.js +62 -0
  97. package/studio/web/assets/dagre-W4DXFKR2-BnLHmA44.js +4 -0
  98. package/studio/web/assets/{diagram-22UHCM2B-DJCvcaWP.js → diagram-2UJZ2QOL-r_jehhZR.js} +7 -7
  99. package/studio/web/assets/{diagram-3UASUU5V-Du6VKWln.js → diagram-OVF4WLC6-BSUXGVh8.js} +11 -11
  100. package/studio/web/assets/{diagram-CDSNMT55-l5Ga0u-W.js → diagram-PFPMY2P6-BrVedycw.js} +3 -3
  101. package/studio/web/assets/{diagram-MLGK6HIB-CNQY47LA.js → diagram-UMYRVEAY-CBIaakBB.js} +1 -1
  102. package/studio/web/assets/diagram-YEKJPTXX-DFmmDXCl.js +41 -0
  103. package/studio/web/assets/diagram-ZIFT7M5P-CQfOWxOi.js +3 -0
  104. package/studio/web/assets/dist-9lpzt2Hn.js +11 -0
  105. package/studio/web/assets/{ebnfDiagram-ZINNZB2B-5OnkTpfi.js → ebnfDiagram-VR2GEFS7-FXA3Xe6e.js} +1 -1
  106. package/studio/web/assets/{editor-CHpQCuBq.js → editor-D5mpVs1b.js} +1 -1
  107. package/studio/web/assets/elk-IJKZMXRS-CC4gfVRU.js +27 -0
  108. package/studio/web/assets/{erDiagram-OPXOYQCR-CQlCX84D.js → erDiagram-O2IAPWRE-C-BkDTPl.js} +2 -2
  109. package/studio/web/assets/errorText-8xV8WZBt.js +1 -0
  110. package/studio/web/assets/eventmodeling-K75KTNOO-D0dJ_PbR.js +1 -0
  111. package/studio/web/assets/flowDiagram-OXPTDLAJ-C1LEc_Hf.js +1 -0
  112. package/studio/web/assets/{ganttDiagram-FUAMR5RP-bloX3Voy.js → ganttDiagram-R7TSEDQI-85F44BIo.js} +4 -4
  113. package/studio/web/assets/{getAnalytics-C0FHjtdQ.js → getAnalytics-B1xpd4kG.js} +1 -1
  114. package/studio/web/assets/gitGraph-VSP46ZUC-BBmqrmjK.js +1 -0
  115. package/studio/web/assets/gitGraphDiagram-XJZIOB7I-CFY6iMwg.js +106 -0
  116. package/studio/web/assets/graphlib-DS17s2tU.js +1 -0
  117. package/studio/web/assets/{html-B1iA8q38.js → html-DiXKiahz.js} +1 -1
  118. package/studio/web/assets/{htmlMode-CrL3YO5Y.js → htmlMode-DitVJ707.js} +1 -1
  119. package/studio/web/assets/index-CDnn0-PN.css +1 -0
  120. package/studio/web/assets/index-D8kyF6dt.js +833 -0
  121. package/studio/web/assets/index.lazy-DgbjUxLo.js +1 -0
  122. package/studio/web/assets/info-OHQRW6UA-Bs2aEco7.js +1 -0
  123. package/studio/web/assets/{infoDiagram-VRGFBTTK-B0J1132p.js → infoDiagram-5W2HQ5XZ-d6yG1yfZ.js} +1 -1
  124. package/studio/web/assets/invalidateEntityQueries-BkWXiqjz.js +1 -0
  125. package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-BNk0tuQx.js → ishikawaDiagram-K3B6WC7H-CSYz-WRA.js} +2 -2
  126. package/studio/web/assets/{javascript-CXgQMkoa.js → javascript-BBVXeMDm.js} +1 -1
  127. package/studio/web/assets/{journeyDiagram-ZHPQQLJL-Be79RMQM.js → journeyDiagram-COXZFDF6-BDj6Wqcj.js} +3 -3
  128. package/studio/web/assets/{jsonMode-DycqRKOv.js → jsonMode-D85_j2oD.js} +1 -1
  129. package/studio/web/assets/{kanban-definition-PNTS6WVX-4uuoQP9W.js → kanban-definition-P3RFRI5V-C2Bjfbp-.js} +6 -6
  130. package/studio/web/assets/{languageServices-HYHV1wDJ.js → languageServices-U3mWhpRL.js} +1 -1
  131. package/studio/web/assets/{lspLanguageFeatures-CH_6SYiA.js → lspLanguageFeatures-CoymyM3c.js} +1 -1
  132. package/studio/web/assets/{mermaid-parser.core-V1UYMeXV.js → mermaid-parser.core-Ch_T61iL.js} +6 -6
  133. package/studio/web/assets/mermaid.core-BOAgKzo5.js +44 -0
  134. package/studio/web/assets/{mindmap-definition-NLK3R4M7-C70Ctm0J.js → mindmap-definition-LZFPQGKD-DzOapzMM.js} +3 -3
  135. package/studio/web/assets/notifications-CPgEC6ZP.js +1 -0
  136. package/studio/web/assets/notifications-D_VqhHnx.js +1 -0
  137. package/studio/web/assets/operations-D8aaKYty.js +4 -0
  138. package/studio/web/assets/packet-JDAUHWVQ-CGHB_EIE.js +1 -0
  139. package/studio/web/assets/{pegDiagram-GJSIUBJH-d6z1cc7W.js → pegDiagram-BGZESJAR-ADzOIKrz.js} +1 -1
  140. package/studio/web/assets/pie-XZMESJXO-Bijz5Y2o.js +1 -0
  141. package/studio/web/assets/pieDiagram-CAPJLFHJ-VM7fvbwO.js +39 -0
  142. package/studio/web/assets/profile-DM5E6Xls.js +1 -0
  143. package/studio/web/assets/{quadrantDiagram-O4NWA36T-CIF5Qkou.js → quadrantDiagram-GDMTTRAM-Bmza84_j.js} +3 -3
  144. package/studio/web/assets/radar-ABXABTNO-IaC8pB63.js +1 -0
  145. package/studio/web/assets/railroad-I3PHUGI6-CCrSUqeq.js +1 -0
  146. package/studio/web/assets/railroad-abnf-LNEFI6M7-BhobVp-t.js +1 -0
  147. package/studio/web/assets/railroad-ebnf-DWYD2UWJ-OHhGUTEh.js +1 -0
  148. package/studio/web/assets/railroad-peg-JR7BVNR7-FupC3Jcn.js +1 -0
  149. package/studio/web/assets/{railroadDiagram-XR7U4H2S-y3Jv2U6N.js → railroadDiagram-SM67HX2B-CgFGOVI5.js} +1 -1
  150. package/studio/web/assets/regions-WFvXqryO.js +1 -0
  151. package/studio/web/assets/{register-DCN4QUii.js → register-B7DvfAGj.js} +3 -3
  152. package/studio/web/assets/request-ChoMERRk.js +2 -0
  153. package/studio/web/assets/{requirementDiagram-PLB6GJNP-ChINZEmu.js → requirementDiagram-X7JNWC4A-BHoQgkoT.js} +4 -4
  154. package/studio/web/assets/restartGate-D_e93lFE.js +1 -0
  155. package/studio/web/assets/restartUnconfirmed-BqcDZ__Y.js +1 -0
  156. package/studio/web/assets/sankeyDiagram-UM26HJRW-8tFu_oUe.js +40 -0
  157. package/studio/web/assets/sequenceDiagram-ZO4K6R2Y-jryb1rwT.js +169 -0
  158. package/studio/web/assets/setComponentFile-BHk22Rh6.js +1 -0
  159. package/studio/web/assets/setup-wNdpTrVH.js +2 -0
  160. package/studio/web/assets/{stateDiagram-v2-GCMORJYK-Bs8RIb9K.js → stateDiagram-v2-TBUQTH76-CnWE5yFn.js} +2 -2
  161. package/studio/web/assets/status-CYoonm9V.js +61 -0
  162. package/studio/web/assets/{swimlanes-2SLR337P-B52xrf6t.js → swimlanes-N4OXWK64-CBlFzDJn.js} +1 -1
  163. package/studio/web/assets/swimlanesDiagram-K3J5GTZL-DvFLtUPi.js +8 -0
  164. package/studio/web/assets/{timeline-definition-EJHVYXUP-DJSITpQX.js → timeline-definition-YOQAKGHF-CFWqHrqI.js} +4 -4
  165. package/studio/web/assets/{toggleHighContrast-CfAp6Sxb.js → toggleHighContrast-CKEm-P4K.js} +1 -1
  166. package/studio/web/assets/treeView-D4JQ5SDB-CpV9zuVb.js +1 -0
  167. package/studio/web/assets/treemap-SAJKECNS-Bc94Tqiq.js +1 -0
  168. package/studio/web/assets/{tsMode-HZR0bfq4.js → tsMode-Dv35qHmn.js} +1 -1
  169. package/studio/web/assets/{typescript-UMJSfZyF.js → typescript-Bly1EW8o.js} +1 -1
  170. package/studio/web/assets/{useEntityRestURL-B379CS-h.js → useEntityRestURL-Cg3P0MOb.js} +1 -1
  171. package/studio/web/assets/usecaseDiagram-VIAY4XPW-Brf2xP3S.js +328 -0
  172. package/studio/web/assets/{vendor-core-6LhfJlRC.js → vendor-core-PejxhUWQ.js} +1 -1
  173. package/studio/web/assets/vendor-datadog-GjET_Cbn.js +6 -0
  174. package/studio/web/assets/{vendor-react-BOliS0BL.js → vendor-react-DSIx92Ld.js} +1 -1
  175. package/studio/web/assets/vendor-tanstack-C5Y-nrd2.js +2 -0
  176. package/studio/web/assets/vendor-ui-BBWHOEoQ.js +41 -0
  177. package/studio/web/assets/{vennDiagram-UO4OBE2U-DZTsDA4k.js → vennDiagram-BLWOH2XV-XxOyJPq_.js} +3 -3
  178. package/studio/web/assets/wardley-7MLQ67FV-DRW0zuE4.js +1 -0
  179. package/studio/web/assets/{wardleyDiagram-VNRHLVJA-qu_f7Tvl.js → wardleyDiagram-YMQ3BMBF-B3WskJ9W.js} +3 -3
  180. package/studio/web/assets/wasAReleasedBeforeB-D5grAaGM.js +1 -0
  181. package/studio/web/assets/{workers-Bn_0_L1R.js → workers-DVtjrvUa.js} +1 -1
  182. package/studio/web/assets/{xml-CG2N-1r1.js → xml-DjAPbgLN.js} +1 -1
  183. package/studio/web/assets/xychartDiagram-TAQBALBS-D5NV4Env.js +7 -0
  184. package/studio/web/assets/{yaml-68pda6Av.js → yaml-oTUyoJy1.js} +1 -1
  185. package/studio/web/index.html +17 -15
  186. package/utility/npmUtilities.ts +17 -7
  187. package/studio/web/assets/Chat-CrxPD4ph.js +0 -2294
  188. package/studio/web/assets/FloatingChat-EXzoUQ-9.js +0 -23
  189. package/studio/web/assets/SearchDialog-7qRL8Yqx.js +0 -1
  190. package/studio/web/assets/_basePickBy-QJXUw_pr.js +0 -1
  191. package/studio/web/assets/_baseUniq-DR4vVjyC.js +0 -1
  192. package/studio/web/assets/alertDialog-BbWD9Xxa.js +0 -1
  193. package/studio/web/assets/applications-CdqjiSQe.js +0 -296
  194. package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +0 -1
  195. package/studio/web/assets/arrow-up-D2g_57vT.js +0 -1
  196. package/studio/web/assets/authStore-CJCaADw9.js +0 -3
  197. package/studio/web/assets/button-DWwfgGWI.js +0 -21
  198. package/studio/web/assets/channel-D0AkPjJ8.js +0 -1
  199. package/studio/web/assets/chunk-3FUC2YCW-DuRp-j19.js +0 -2
  200. package/studio/web/assets/chunk-7M6MHVWA-h6WlaTzO.js +0 -213
  201. package/studio/web/assets/chunk-FOHPRMQF-DzxwRta7.js +0 -161
  202. package/studio/web/assets/chunk-O7XYJQB3-CBS3nZYV.js +0 -128
  203. package/studio/web/assets/chunk-Z7XXMR3K-DSHbImdf.js +0 -10
  204. package/studio/web/assets/cn-6T8VranJ.js +0 -1
  205. package/studio/web/assets/cynefin-OW5HDTMX-DIvbXLSj.js +0 -1
  206. package/studio/web/assets/cynefinDiagram-VND7K2PF-BdtaXiCE.js +0 -62
  207. package/studio/web/assets/dagre-6A5THRUB-C7RUWHyz.js +0 -4
  208. package/studio/web/assets/diagram-ATOU4E4O-dxRfeG4B.js +0 -3
  209. package/studio/web/assets/diagram-MPIPVDR6-7prveD1B.js +0 -41
  210. package/studio/web/assets/dist-BrRuMUzt.js +0 -11
  211. package/studio/web/assets/download-Cn6tPG4L.js +0 -1
  212. package/studio/web/assets/elk-276RUBZZ-DxDQPjQT.js +0 -27
  213. package/studio/web/assets/errorText-NpBmvk9I.js +0 -1
  214. package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +0 -1
  215. package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +0 -1
  216. package/studio/web/assets/gitGraph-4MIJSDKK-BrXWmJt2.js +0 -1
  217. package/studio/web/assets/gitGraphDiagram-X574FWY7-C2KnnZeY.js +0 -106
  218. package/studio/web/assets/graphlib-C9Tl7QrK.js +0 -1
  219. package/studio/web/assets/index-QLZw6xrU.css +0 -1
  220. package/studio/web/assets/index-qCLyhyah.js +0 -828
  221. package/studio/web/assets/index.lazy-DCx-DxIV.js +0 -2
  222. package/studio/web/assets/info-A6RAGUB7-BhzBHe4U.js +0 -1
  223. package/studio/web/assets/mermaid.core-1PX6R49U.js +0 -44
  224. package/studio/web/assets/notifications-BSObVIY1.js +0 -1
  225. package/studio/web/assets/notifications-DFLXk68b.js +0 -1
  226. package/studio/web/assets/packet-AYTQ26CC-DWbXHx5g.js +0 -1
  227. package/studio/web/assets/pie-WAS4IAKB-DjoTm3yr.js +0 -1
  228. package/studio/web/assets/pieDiagram-5QR66LMP-Dz3RHpDA.js +0 -39
  229. package/studio/web/assets/profile-TBjzOMIy.js +0 -1
  230. package/studio/web/assets/radar-RG4KPBEZ-CVBgjCOj.js +0 -1
  231. package/studio/web/assets/railroad-74A4TZTK-BdhXw10Q.js +0 -1
  232. package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +0 -1
  233. package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +0 -1
  234. package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +0 -1
  235. package/studio/web/assets/regions-C4an4H7e.js +0 -1
  236. package/studio/web/assets/restartGate-nHAFLDh-.js +0 -1
  237. package/studio/web/assets/sankeyDiagram-IPEJSGJF-D9AyLce2.js +0 -40
  238. package/studio/web/assets/sequenceDiagram-PO4LG4MO-hfk4aIEm.js +0 -169
  239. package/studio/web/assets/setComponentFile-BrqOM1Zm.js +0 -1
  240. package/studio/web/assets/setup-0v2MCm-E.js +0 -2
  241. package/studio/web/assets/status-DdezofUu.js +0 -61
  242. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +0 -8
  243. package/studio/web/assets/tabs-BCTQzoQK.js +0 -1
  244. package/studio/web/assets/treeView-Q6P3EWNA-C-epdxRo.js +0 -1
  245. package/studio/web/assets/treemap-WGGIJYW6-DOVyp_zw.js +0 -1
  246. package/studio/web/assets/usecaseDiagram-POWQR4AR-B6252cbq.js +0 -328
  247. package/studio/web/assets/vendor-datadog-ozKf-pts.js +0 -6
  248. package/studio/web/assets/vendor-tanstack-Bntfka-M.js +0 -2
  249. package/studio/web/assets/vendor-ui-9EYYDgoN.js +0 -41
  250. package/studio/web/assets/wardley-WFR3VGLG-Cfqoe6z4.js +0 -1
  251. 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
@@ -851,7 +888,7 @@ class DatabaseTransaction {
851
888
  */
852
889
  releaseReadTxn() {
853
890
  const transaction = this.detachOwnedTransaction();
854
- abortNativeTransaction(transaction, 'releasing timed-out read transaction');
891
+ abortNativeTransaction(transaction, 'releasing timed-out read transaction', this.writes[0]?.store ?? this.db, this.startedFrom);
855
892
  this.completeDeferredContextRelease();
856
893
  }
857
894
  /**
@@ -904,6 +941,8 @@ class DatabaseTransaction {
904
941
  if (index > -1)
905
942
  this.writes[index] = null;
906
943
  this.ownedWrites?.delete(operation);
944
+ if (operation.stagedIn === this)
945
+ operation.stagedIn = undefined;
907
946
  if (operation.key === undefined)
908
947
  return;
909
948
  const writesForStore = this.writesByKey?.get(operation.store);
@@ -991,12 +1030,299 @@ class DatabaseTransaction {
991
1030
  if (recordLocks.size === 0)
992
1031
  this.recordLocks = undefined;
993
1032
  }
994
- noteCommittedLockVersions() {
1033
+ budgetCarrier(transaction) {
1034
+ const txn = transaction;
1035
+ if (txn?.commitStartedAt != null)
1036
+ return txn;
1037
+ if (this.saveCommits && txn && this.hasSiblingCommitInFlight(transaction ?? undefined)) {
1038
+ return txn;
1039
+ }
1040
+ if (txn?.immediateCommit)
1041
+ return txn;
1042
+ return this.saveCommits ? this : (this.root ?? this);
1043
+ }
1044
+ backfillActiveHandleWrites(handle) {
1045
+ if (handle.immediateCommit)
1046
+ return;
1047
+ for (let i = 0; i < this.writes.length; i++) {
1048
+ const w = this.writes[i];
1049
+ if (w?.saved && !w.stagedOn)
1050
+ w.stagedOn = handle;
1051
+ }
1052
+ }
1053
+ isSingleHandleTarget(txn) {
1054
+ return (!txn.immediateCommit &&
1055
+ (!this.activeCommitHandles || this.activeCommitHandles.size === 0) &&
1056
+ (!this.activeCommitHandle || this.activeCommitHandle === txn));
1057
+ }
1058
+ getHandleStagedTxnTime(txn) {
1059
+ if (!txn)
1060
+ return undefined;
1061
+ if (this.isSingleHandleTarget(txn) &&
1062
+ (this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
1063
+ return this.activeHandleStagedTxnTime || txn.stagedTxnTime;
1064
+ }
1065
+ return txn.stagedTxnTime;
1066
+ }
1067
+ setHandleStagedTxnTime(txn, txnTime) {
1068
+ if (this.isSingleHandleTarget(txn) &&
1069
+ (this.activeCommitHandle === txn || (!this.activeCommitHandle && (!this.transaction || this.transaction === txn)))) {
1070
+ if (!this.activeHandleStagedTxnTime)
1071
+ this.activeHandleStagedTxnTime = txnTime;
1072
+ return;
1073
+ }
1074
+ txn.stagedTxnTime ??= txnTime;
1075
+ }
1076
+ isHandleCommitInFlight(txn) {
1077
+ if (!txn)
1078
+ return false;
1079
+ if (this.activeCommitHandle === txn) {
1080
+ return Boolean(this.activeHandleInFlight || txn.commitInFlight);
1081
+ }
1082
+ return Boolean(txn.commitInFlight);
1083
+ }
1084
+ setHandleCommitInFlight(txn, inFlight) {
1085
+ if (this.isSingleHandleTarget(txn) && inFlight && !this.activeCommitHandle) {
1086
+ this.activeCommitHandle = txn;
1087
+ }
1088
+ if (this.activeCommitHandle === txn) {
1089
+ this.activeHandleInFlight = inFlight;
1090
+ if (!inFlight) {
1091
+ txn.commitInFlight = false;
1092
+ }
1093
+ return;
1094
+ }
1095
+ txn.commitInFlight = inFlight;
1096
+ }
1097
+ isHandleCommitted(txn) {
1098
+ if (!txn)
1099
+ return false;
1100
+ if (this.activeCommitHandle === txn) {
1101
+ return Boolean(this.activeHandleCommitted || txn.committed);
1102
+ }
1103
+ return Boolean(txn.committed);
1104
+ }
1105
+ setHandleCommitted(txn) {
1106
+ if (this.activeCommitHandle === txn) {
1107
+ this.activeHandleInFlight = false;
1108
+ this.activeHandleCommitted = true;
1109
+ txn.commitInFlight = false;
1110
+ return;
1111
+ }
1112
+ txn.commitInFlight = false;
1113
+ txn.committed = true;
1114
+ }
1115
+ trackActiveCommitHandle(transaction) {
1116
+ if (!transaction.immediateCommit &&
1117
+ !this.activeCommitHandle &&
1118
+ (!this.activeCommitHandles || this.activeCommitHandles.size === 0)) {
1119
+ this.activeCommitHandle = transaction;
1120
+ return;
1121
+ }
1122
+ if (this.activeCommitHandle === transaction)
1123
+ return;
1124
+ let handles = this.activeCommitHandles;
1125
+ if (!handles) {
1126
+ handles = this.activeCommitHandles = new Set();
1127
+ }
1128
+ if (this.activeCommitHandle) {
1129
+ const first = this.activeCommitHandle;
1130
+ if (this.activeHandleStagedTxnTime && !first.stagedTxnTime) {
1131
+ first.stagedTxnTime = this.activeHandleStagedTxnTime;
1132
+ }
1133
+ if (this.activeHandleInFlight)
1134
+ first.commitInFlight = true;
1135
+ if (this.activeHandleCommitted)
1136
+ first.committed = true;
1137
+ this.activeHandleStagedTxnTime = 0;
1138
+ this.activeHandleInFlight = false;
1139
+ this.activeHandleCommitted = false;
1140
+ this.backfillActiveHandleWrites(first);
1141
+ if (this.saveCommits) {
1142
+ if (this.commitStartedAt != null && first.commitStartedAt == null) {
1143
+ first.commitStartedAt = this.commitStartedAt;
1144
+ this.commitStartedAt = undefined;
1145
+ }
1146
+ if (this.commitOverdueLogged) {
1147
+ first.commitOverdueLogged = true;
1148
+ this.commitOverdueLogged = undefined;
1149
+ }
1150
+ }
1151
+ handles.add(this.activeCommitHandle);
1152
+ this.activeCommitHandle = undefined;
1153
+ }
1154
+ handles.add(transaction);
1155
+ }
1156
+ releaseActiveCommitHandle(transaction) {
1157
+ if (this.activeCommitHandle === transaction) {
1158
+ this.activeCommitHandle = undefined;
1159
+ this.activeHandleStagedTxnTime = 0;
1160
+ this.activeHandleInFlight = false;
1161
+ this.activeHandleCommitted = false;
1162
+ return;
1163
+ }
1164
+ const handles = this.activeCommitHandles;
1165
+ if (!handles)
1166
+ return;
1167
+ handles.delete(transaction);
1168
+ if (handles.size === 1) {
1169
+ for (const remaining of handles) {
1170
+ const rem = remaining;
1171
+ if (!rem.immediateCommit) {
1172
+ this.activeCommitHandle = remaining;
1173
+ this.activeHandleStagedTxnTime = rem.stagedTxnTime ?? 0;
1174
+ this.activeHandleInFlight = Boolean(rem.commitInFlight);
1175
+ this.activeHandleCommitted = Boolean(rem.committed);
1176
+ handles.clear();
1177
+ }
1178
+ }
1179
+ }
1180
+ }
1181
+ clearActiveCommitHandles() {
1182
+ if (this.activeCommitHandle !== undefined || this.activeHandleStagedTxnTime) {
1183
+ this.activeCommitHandle = undefined;
1184
+ this.activeHandleStagedTxnTime = 0;
1185
+ this.activeHandleInFlight = false;
1186
+ this.activeHandleCommitted = false;
1187
+ }
1188
+ this.activeCommitHandles?.clear();
1189
+ }
1190
+ hasActiveCommitHandle() {
1191
+ return (this.activeCommitHandle !== undefined ||
1192
+ (this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0));
1193
+ }
1194
+ /**
1195
+ * Retries so far of the handles with a durable transaction-log entry still retrying their commit on
1196
+ * this link, summed over overlapping commits so the total moves whenever any of them does, or -1
1197
+ * when there is none.
1198
+ */
1199
+ activeDurableLogRetries() {
1200
+ let retries = -1;
1201
+ const note = (handle) => {
1202
+ const txn = handle;
1203
+ if (!txn || !hasDurableLog(txn) || this.isHandleCommitted(txn) || txn.abortedByWrapper)
1204
+ return;
1205
+ retries = Math.max(retries, 0) + (txn.retries ?? 0);
1206
+ };
1207
+ note(this.activeCommitHandle);
1208
+ if (this.activeCommitHandles)
1209
+ for (const handle of this.activeCommitHandles)
1210
+ note(handle);
1211
+ note(this.transaction);
1212
+ return retries;
1213
+ }
1214
+ isActiveCommitHandle(transaction) {
1215
+ return this.activeCommitHandle === transaction || Boolean(this.activeCommitHandles?.has(transaction));
1216
+ }
1217
+ hasSiblingCommitInFlight(targetTransaction) {
1218
+ const excluded = targetTransaction ?? this.transaction;
1219
+ if (this.activeCommitHandle)
1220
+ return this.activeCommitHandle !== excluded;
1221
+ const active = this.activeCommitHandles;
1222
+ if (!active || active.size === 0)
1223
+ return false;
1224
+ if (!excluded)
1225
+ return true;
1226
+ return active.size > (active.has(excluded) ? 1 : 0);
1227
+ }
1228
+ isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
1229
+ if (!write)
1230
+ return false;
1231
+ if (targetTransaction?.immediateCommit) {
1232
+ return write.stagedOn !== targetTransaction;
1233
+ }
1234
+ if (write.deferSave && !write.saved && this.saveCommits)
1235
+ return ((!fromAbort || Boolean(targetTransaction) || Boolean(this.isCommitting)) &&
1236
+ !write.stagedOn &&
1237
+ !write.lockHandle?.released &&
1238
+ !write.lockHandle?.isExpired());
1239
+ if (!write.saved)
1240
+ return !fromAbort && Boolean(targetTransaction) && this.open === exports.TRANSACTION_STATE.OPEN && !write.stagedOn;
1241
+ const stagedOn = (write.stagedOn ?? this.activeCommitHandle ?? this.transaction);
1242
+ if (!stagedOn || stagedOn === targetTransaction || stagedOn.abortedByWrapper || this.isHandleCommitted(stagedOn))
1243
+ return false;
1244
+ if (excludeImmediate && stagedOn.immediateCommit)
1245
+ return false;
1246
+ if (!fromAbort &&
1247
+ Boolean(targetTransaction) &&
1248
+ this.open === exports.TRANSACTION_STATE.OPEN &&
1249
+ stagedOn === this.transaction)
1250
+ return true;
1251
+ if (hasSiblingInFlight &&
1252
+ (Boolean(targetTransaction) ||
1253
+ (!fromAbort && this.isActiveCommitHandle(stagedOn)) ||
1254
+ (fromAbort &&
1255
+ this.isActiveCommitHandle(stagedOn) &&
1256
+ ((!excludeImmediate && Boolean(stagedOn.immediateCommit)) || Boolean(stagedOn.isRetry)))))
1257
+ return true;
1258
+ return Boolean(this.isHandleCommitInFlight(stagedOn) || hasDurableLog(stagedOn));
1259
+ }
1260
+ hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, fromAbort = false, excludeImmediate = false) {
1261
+ if (!this.stagesWriteOnSave)
1262
+ return false;
1263
+ for (let i = 0; i < this.writes.length; i++) {
1264
+ if (this.isWriteOwnedByActiveCommit(this.writes[i], targetTransaction, hasSiblingInFlight, fromAbort, excludeImmediate))
1265
+ return true;
1266
+ }
1267
+ return false;
1268
+ }
1269
+ hasSavedWrite() {
1270
+ for (let i = 0; i < this.writes.length; i++) {
1271
+ if (this.writes[i]?.saved)
1272
+ return true;
1273
+ }
1274
+ return false;
1275
+ }
1276
+ hasSavedWriteForHandle(transaction, isImmediateCommit) {
1277
+ for (let i = 0; i < this.writes.length; i++) {
1278
+ const write = this.writes[i];
1279
+ if (!write?.saved)
1280
+ continue;
1281
+ if (isImmediateCommit ? write.stagedOn === transaction : !write.stagedOn || write.stagedOn === transaction) {
1282
+ return true;
1283
+ }
1284
+ }
1285
+ return false;
1286
+ }
1287
+ retainTransactionLockHolds(transaction) {
1288
+ if (!transaction || !this.hasLeaseProtectedWrite)
1289
+ return;
1290
+ const txn = transaction;
1291
+ if (txn.lockHolds || txn.holdBudgetSpent)
1292
+ return;
1293
+ const carrier = this.budgetCarrier(transaction);
1294
+ if (carrier.commitStartedAt == null)
1295
+ carrier.commitStartedAt = performance.now();
1296
+ const budget = this.commitConflictBudget();
1297
+ const holdDeadlineMono = Number.isFinite(budget) ? carrier.commitStartedAt + budget : undefined;
1298
+ let holds;
1299
+ for (let i = 0; i < this.writes.length; i++) {
1300
+ const write = this.writes[i];
1301
+ if (!write?.saved || (write.stagedOn && write.stagedOn !== transaction))
1302
+ continue;
1303
+ const handle = write.lockHandle;
1304
+ if (!handle?.retainForCommit || (holds && holds.has(handle)))
1305
+ continue;
1306
+ handle.retainForCommit(holdDeadlineMono);
1307
+ (holds ??= new Set()).add(handle);
1308
+ }
1309
+ if (holds)
1310
+ txn.lockHolds = holds;
1311
+ }
1312
+ noteCommittedLockVersions(committedTransaction) {
995
1313
  const recordLocks = this.recordLocks;
996
1314
  if (!recordLocks)
997
1315
  return;
1316
+ const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
1317
+ const filterByStagedOn = Boolean(committedTransaction || this.hasSiblingCommitInFlight(committedTransaction));
998
1318
  for (const write of this.writes) {
999
- if (write?.appliedRecordVersion == null)
1319
+ if (!write?.saved)
1320
+ continue;
1321
+ if (isImmediateCommit
1322
+ ? write.stagedOn !== committedTransaction
1323
+ : filterByStagedOn && write.stagedOn && write.stagedOn !== committedTransaction)
1324
+ continue;
1325
+ if (write.appliedRecordVersion == null)
1000
1326
  continue;
1001
1327
  const handle = write.lockHandle ?? recordLocks.get(write.store)?.get(writeKeyId(write.key));
1002
1328
  if (handle && !handle.released)
@@ -1005,9 +1331,32 @@ class DatabaseTransaction {
1005
1331
  }
1006
1332
  /**
1007
1333
  * 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.
1334
+ * reused transaction never bases a write on a previous batch's staged state. When concurrent
1335
+ * commits share one DatabaseTransaction, only the writes staged on `committedTransaction` are
1336
+ * removed while another native commit is still in flight.
1009
1337
  */
1010
- clearWrites() {
1338
+ clearWrites(committedTransaction, fromAbort = false) {
1339
+ if (committedTransaction)
1340
+ this.releaseActiveCommitHandle(committedTransaction);
1341
+ const isImmediateCommit = Boolean(committedTransaction?.immediateCommit);
1342
+ const hasSiblingInFlight = this.hasSiblingCommitInFlight(committedTransaction);
1343
+ if (this.stagesWriteOnSave &&
1344
+ (isImmediateCommit ||
1345
+ ((hasSiblingInFlight || !committedTransaction || this.commitsInFlight > 1 || this.saveCommits) &&
1346
+ this.hasWriteOwnedByActiveCommit(committedTransaction, hasSiblingInFlight, fromAbort)))) {
1347
+ for (const write of [...this.writes]) {
1348
+ if (!write || this.isWriteOwnedByActiveCommit(write, committedTransaction, hasSiblingInFlight, fromAbort))
1349
+ continue;
1350
+ this.detachWrite(write);
1351
+ }
1352
+ this.writes = this.writes.filter((write) => write);
1353
+ this.validated = this.writes.reduce((acc, w, idx) => (w?.saved ? idx + 1 : acc), 0);
1354
+ if (this.hasLeaseProtectedWrite && !this.writes.some((write) => write?.lockHandle)) {
1355
+ this.hasLeaseProtectedWrite = false;
1356
+ }
1357
+ return;
1358
+ }
1359
+ this.clearActiveCommitHandles();
1011
1360
  // A deferred write's `stagedIn` must not outlive this transaction's ability to commit it: save() can
1012
1361
  // fire after a commit or abort, and routing it back here would revive a write this transaction
1013
1362
  // already rolled back — whose blobs abort() has reclaimed. Cleared here, save() resolves the
@@ -1019,6 +1368,7 @@ class DatabaseTransaction {
1019
1368
  this.ownedWrites?.delete(write);
1020
1369
  }
1021
1370
  this.writes = [];
1371
+ this.validated = 0;
1022
1372
  this.writesByKey = undefined;
1023
1373
  // The commit-fence latch belongs to the discarded batch. A reused transaction that once staged
1024
1374
  // a locked write would otherwise re-scan every write of every later unlocked batch. Assigned
@@ -1098,12 +1448,13 @@ class DatabaseTransaction {
1098
1448
  * no record), and writes with no stored entry at all — a delete leaves a readable tombstone on any
1099
1449
  * audited or delete-tracking table, so only a delete on a table with neither is entry-less. Such a
1100
1450
  * `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).
1451
+ * transaction is stamped with one version). `savedOnly` also skips a deferred write not yet saved,
1452
+ * which a commit leaves in place for a later one.
1102
1453
  */
1103
- lastConfirmableEntry() {
1454
+ lastConfirmableEntry(savedOnly = false) {
1104
1455
  for (let i = this.writes.length - 1; i >= 0; i--) {
1105
1456
  const write = this.writes[i];
1106
- if (!write || write.skipReplicationConfirmation)
1457
+ if (!write || write.skipReplicationConfirmation || (savedOnly && !write.saved))
1107
1458
  continue;
1108
1459
  const entry = write.store.getEntry(write.key);
1109
1460
  if (entry)
@@ -1116,9 +1467,61 @@ class DatabaseTransaction {
1116
1467
  * rejection in that window is an unhandled rejection (fatal under --unhandled-rejections=strict).
1117
1468
  * The rejection still surfaces through commit()'s Promise.all.
1118
1469
  */
1119
- stageCompletion(completion) {
1470
+ stageCompletion(completion, transaction) {
1120
1471
  completion.then(undefined, () => { });
1472
+ const index = this.completions.length;
1121
1473
  this.completions.push(completion);
1474
+ if (transaction &&
1475
+ (this.saveCommits ||
1476
+ Boolean(transaction.immediateCommit) ||
1477
+ this.hasActiveCommitHandle() ||
1478
+ Boolean(this.completionHandles))) {
1479
+ (this.completionHandles ??= [])[index] = transaction;
1480
+ }
1481
+ }
1482
+ takeCompletions(transaction) {
1483
+ if (this.completions.length === 0)
1484
+ return NO_COMPLETIONS;
1485
+ const handles = this.completionHandles;
1486
+ const isImmediate = Boolean(transaction?.immediateCommit);
1487
+ if (!handles) {
1488
+ if (isImmediate)
1489
+ return NO_COMPLETIONS;
1490
+ const completions = this.completions;
1491
+ this.completions = [];
1492
+ return completions;
1493
+ }
1494
+ let allMatch = true;
1495
+ for (let i = 0; i < this.completions.length; i++) {
1496
+ const handle = handles[i];
1497
+ if (!(handle ? handle === transaction : !isImmediate)) {
1498
+ allMatch = false;
1499
+ break;
1500
+ }
1501
+ }
1502
+ if (allMatch) {
1503
+ const completions = this.completions;
1504
+ this.completions = [];
1505
+ this.completionHandles = undefined;
1506
+ return completions;
1507
+ }
1508
+ const taken = [];
1509
+ const remaining = [];
1510
+ const remainingHandles = [];
1511
+ for (let i = 0; i < this.completions.length; i++) {
1512
+ const completion = this.completions[i];
1513
+ const handle = handles[i];
1514
+ if (handle ? handle === transaction : !isImmediate) {
1515
+ taken.push(completion);
1516
+ }
1517
+ else {
1518
+ remaining.push(completion);
1519
+ remainingHandles.push(handle);
1520
+ }
1521
+ }
1522
+ this.completions = remaining;
1523
+ this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
1524
+ return taken;
1122
1525
  }
1123
1526
  /**
1124
1527
  * Discard staged completions that no commit() will ever aggregate (abort path). Their rejections
@@ -1126,12 +1529,41 @@ class DatabaseTransaction {
1126
1529
  * Clearing them also keeps a reused transaction's next commit() from rejecting with the previous
1127
1530
  * batch's error.
1128
1531
  */
1129
- drainCompletions() {
1532
+ drainCompletions(targetTransaction, preserveActiveWrites = false) {
1130
1533
  if (this.completions.length === 0)
1131
1534
  return;
1132
- const completions = this.completions;
1133
- this.completions = [];
1134
- for (const completion of completions)
1535
+ const handles = this.completionHandles;
1536
+ const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
1537
+ let drained;
1538
+ if (!preserveActiveWrites) {
1539
+ drained = this.completions;
1540
+ this.completions = [];
1541
+ this.completionHandles = undefined;
1542
+ }
1543
+ else {
1544
+ drained = [];
1545
+ const remaining = [];
1546
+ const remainingHandles = [];
1547
+ for (let i = 0; i < this.completions.length; i++) {
1548
+ const completion = this.completions[i];
1549
+ const handle = handles?.[i];
1550
+ const keep = isImmediateTarget
1551
+ ? handle !== targetTransaction
1552
+ : Boolean(handle) &&
1553
+ handle !== targetTransaction &&
1554
+ (this.isActiveCommitHandle(handle) || hasDurableLog(handle));
1555
+ if (keep) {
1556
+ remaining.push(completion);
1557
+ remainingHandles.push(handle);
1558
+ }
1559
+ else {
1560
+ drained.push(completion);
1561
+ }
1562
+ }
1563
+ this.completions = remaining;
1564
+ this.completionHandles = remainingHandles.length > 0 ? remainingHandles : undefined;
1565
+ }
1566
+ for (const completion of drained)
1135
1567
  completion.then(undefined, (error) => harperLogger.warn?.('A staged transaction completion failed after the transaction was aborted', error));
1136
1568
  }
1137
1569
  addWrite(operation) {
@@ -1205,7 +1637,7 @@ class DatabaseTransaction {
1205
1637
  // (the same rule the pre-submit fence applies below). Without the distinction,
1206
1638
  // `lock(); save(); unlock(); commit()` succeeded normally and threw 409 only when a conflict
1207
1639
  // retry or an open read iterator forced the replay to re-save it.
1208
- if (operation.saved ? lockHandle?.isLeaseExpired() : lockHandle?.isExpired()) {
1640
+ if (operation.saved ? lockHandle?.isLeaseExpired() && !hasDurableLog(transaction) : lockHandle?.isExpired()) {
1209
1641
  // Through `detachWrite`, which also repairs the per-key chain: dropping it from `writes` alone
1210
1642
  // leaves `writesByKey` pointing at a write that was refused, so a caller that catches this 409
1211
1643
  // and stages the same key again would take the rejected operation as its merge basis.
@@ -1229,7 +1661,10 @@ class DatabaseTransaction {
1229
1661
  operation.lockStamp = lockHandle.holderVersionCandidate();
1230
1662
  }
1231
1663
  }
1232
- let txnTime = operation.lockStamp ?? this.timestamp;
1664
+ const concurrentClosedWrite = Boolean(transaction?.immediateCommit) ||
1665
+ (!transaction && this.open !== exports.TRANSACTION_STATE.OPEN && !this.saveCommits);
1666
+ let txnTime = operation.lockStamp ?? this.getHandleStagedTxnTime(transaction) ?? (concurrentClosedWrite ? 0 : this.timestamp);
1667
+ const mintedTimestamp = !txnTime;
1233
1668
  // Only an OPEN transaction accepts new staged writes. After commit, this.transaction may still
1234
1669
  // be retained for outstanding read iterators; staging into it would silently discard the write
1235
1670
  // when doneReadTxn() aborts the handle, so such writes commit immediately on a fresh
@@ -1260,20 +1695,43 @@ class DatabaseTransaction {
1260
1695
  else {
1261
1696
  // if it is closed, we have to immediately commit, using our immediate transaction
1262
1697
  immediateCommit = true;
1698
+ if (!this.saveCommits)
1699
+ transaction.immediateCommit = true;
1263
1700
  }
1264
1701
  if (txnTime) {
1265
1702
  transaction.setTimestamp(txnTime);
1266
1703
  }
1267
1704
  }
1705
+ if (this.activeCommitHandle && this.activeCommitHandle !== transaction) {
1706
+ this.backfillActiveHandleWrites(this.activeCommitHandle);
1707
+ }
1708
+ else if (!this.activeCommitHandle && this.transaction && this.transaction !== transaction) {
1709
+ this.backfillActiveHandleWrites(this.transaction);
1710
+ }
1711
+ const shouldStampStagedOn = this.saveCommits ||
1712
+ immediateCommit ||
1713
+ Boolean(transaction?.immediateCommit) ||
1714
+ operation.stagedOn !== undefined ||
1715
+ (this.activeCommitHandles !== undefined && this.activeCommitHandles.size > 0) ||
1716
+ (this.activeCommitHandle !== undefined && this.activeCommitHandle !== transaction) ||
1717
+ (this.transaction != null && this.transaction !== transaction);
1268
1718
  if (this.isReplay) {
1269
1719
  // Replayed writes came FROM the transaction log; never re-append them —
1270
1720
  // replay iterates that same log, so re-appending prevents convergence
1271
1721
  // (boot hangs replaying its own output). Conflict retries stamp isRetry
1272
1722
  // at the retry sites in commit(); this is the replay-path equivalent.
1273
1723
  transaction.isRetry = true;
1724
+ transaction.retries = this.retries;
1274
1725
  }
1275
- if (!txnTime)
1276
- txnTime = this.timestamp = transaction.getTimestamp();
1726
+ if (!txnTime) {
1727
+ txnTime = transaction.getTimestamp();
1728
+ if (!concurrentClosedWrite)
1729
+ this.timestamp = txnTime;
1730
+ }
1731
+ if (!operation.lockStamp)
1732
+ this.setHandleStagedTxnTime(transaction, txnTime);
1733
+ if (this.saveCommits && mintedTimestamp)
1734
+ this.timestamp = 0;
1277
1735
  if (!operation.saved && operation.pendingPriorWrite) {
1278
1736
  const pendingWrites = [];
1279
1737
  for (let pending = operation.pendingPriorWrite; pending;) {
@@ -1301,50 +1759,76 @@ class DatabaseTransaction {
1301
1759
  // An entry read uncached through this same pinned-snapshot handle is already that base, until a retry resets
1302
1760
  // the snapshot or an earlier staged write to the key changes what a read returns; the resource withholds the
1303
1761
  // handle once its entry was replaced or evicted, and locked, snapshot-free and replayed writes always reload.
1762
+ const txnRetries = transaction?.retries ?? 0;
1304
1763
  const reusesBaseRead = operation.baseReadTxn === transaction &&
1305
- this.retries === 0 &&
1764
+ txnRetries === 0 &&
1306
1765
  !reloadEntry &&
1307
1766
  !operation.priorWrite &&
1308
1767
  !operation.lockHandle &&
1309
1768
  !this.snapshotFree &&
1310
1769
  !transaction.snapshotDisabled &&
1311
1770
  !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) {
1771
+ const wasSaved = Boolean(operation.saved);
1772
+ let completion;
1773
+ try {
1774
+ if (!reusesBaseRead && (reloadEntry || operation.entry === undefined || reloadsCommitBase)) {
1775
+ const uncachedRead = !!operation.reloadCommitBase || reloadEntry || this.isReplay;
1776
+ operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
1777
+ }
1778
+ if (!operation.saved) {
1779
+ // immediately execute in this transaction
1780
+ const validated = validateWrite(operation, writeVersion, this);
1781
+ if (validated === false) {
1782
+ operation.saved = true;
1783
+ if (shouldStampStagedOn)
1784
+ operation.stagedOn = transaction;
1785
+ operation.commit = () => { }; // noop if we try again
1786
+ closeWriteInstance(operation);
1787
+ if (immediateCommit) {
1788
+ this.detachWrite(operation);
1789
+ if (!this.activeCommitHandle)
1790
+ this.activeHandleStagedTxnTime = 0;
1791
+ abortNativeTransaction(transaction, 'aborting an immediate transaction with nothing to write', operation.store, this.startedFrom);
1792
+ }
1793
+ return;
1794
+ }
1320
1795
  operation.saved = true;
1321
- operation.commit = () => { }; // noop if we try again
1796
+ let result = operation.before?.();
1797
+ if (result?.then)
1798
+ this.stageCompletion(result, transaction);
1799
+ result = operation.beforeIntermediate?.();
1800
+ if (result?.then)
1801
+ this.stageCompletion(result, transaction);
1802
+ }
1803
+ if (shouldStampStagedOn)
1804
+ operation.stagedOn = transaction;
1805
+ if (lockHandle || this.recordLocks)
1806
+ operation.trackRecordVersion = true;
1807
+ if (operation.trackRecordVersion)
1808
+ operation.recordVersionApplied = false;
1809
+ try {
1810
+ completion = operation.commit(writeVersion, operation.entry, (wasSaved || this.isReplay) && txnRetries > 0, transaction, this);
1811
+ }
1812
+ finally {
1322
1813
  closeWriteInstance(operation);
1323
- return;
1324
1814
  }
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
1815
  }
1341
- finally {
1342
- closeWriteInstance(operation);
1816
+ catch (error) {
1817
+ // A handle created for an immediate commit is owned by nobody else: a throw before its commit
1818
+ // is submitted must release it rather than leak it.
1819
+ if (immediateCommit) {
1820
+ this.drainCompletions(transaction, true);
1821
+ this.detachWrite(operation);
1822
+ if (!this.activeCommitHandle)
1823
+ this.activeHandleStagedTxnTime = 0;
1824
+ abortNativeTransaction(transaction, 'aborting an immediate transaction whose write threw', operation.store, this.startedFrom);
1825
+ }
1826
+ throw error;
1343
1827
  }
1344
1828
  if (operation.trackRecordVersion)
1345
1829
  operation.appliedRecordVersion = operation.recordVersionApplied ? writeVersion : undefined;
1346
1830
  if (typeof completion?.then === 'function')
1347
- this.stageCompletion(completion);
1831
+ this.stageCompletion(completion, transaction);
1348
1832
  // Sticky record that THIS write staged with its audit entry appended (log entries batch on the
1349
1833
  // native transaction and are durably written by its commit attempt — even a failed one — so
1350
1834
  // they survive the abort-after-failed-commit of the retry paths). isRetry stagings
@@ -1353,6 +1837,7 @@ class DatabaseTransaction {
1353
1837
  // retries where a recommit round self-skips before a fresh-transaction replay.
1354
1838
  if (!operation.skipped && !transaction.isRetry) {
1355
1839
  operation.appendedAuditEntry = true;
1840
+ operation.appendedTo = transaction;
1356
1841
  }
1357
1842
  if (immediateCommit) {
1358
1843
  // immediately commit if the harper transaction is closed
@@ -1549,6 +2034,10 @@ class DatabaseTransaction {
1549
2034
  endCommitAttempt() {
1550
2035
  this.commitsInFlight--;
1551
2036
  if (!this.commitsInFlight) {
2037
+ this.clearActiveCommitHandles();
2038
+ this.commitStartedAt = undefined;
2039
+ if (this.commitOverdueLogged)
2040
+ this.commitOverdueLogged = undefined;
1552
2041
  this.nativeCommitSubmitted = false;
1553
2042
  this.nativeCommitAttemptEnded();
1554
2043
  }
@@ -1568,7 +2057,14 @@ class DatabaseTransaction {
1568
2057
  root.submittedCommitSupervised = false;
1569
2058
  root.deferredPoisonDeadline = undefined;
1570
2059
  root.unsubmittedCommitDeadline = undefined;
2060
+ root.loggedRetriesSeen = undefined;
2061
+ root.loggedRetriesChangedAt = undefined;
2062
+ root.loggedRetriesTick = undefined;
2063
+ root.loggedRetryAdvancing = undefined;
1571
2064
  root.stalledCommitLogged = false;
2065
+ root.commitStartedAt = undefined;
2066
+ if (root.commitOverdueLogged)
2067
+ root.commitOverdueLogged = undefined;
1572
2068
  const submittedLink = root.submittedLink;
1573
2069
  root.submittedLink = undefined;
1574
2070
  const submittedLinks = root.submittedLinks;
@@ -1643,35 +2139,57 @@ class DatabaseTransaction {
1643
2139
  // reused across retries — the native layer resets it in place (fresh snapshot) on IsBusy/TryAgain —
1644
2140
  // but reassigned to a fresh replay transaction when outstanding read iterators retain this.transaction
1645
2141
  let transaction = options.transaction ?? this.transaction;
2142
+ const retryRound = Boolean(options.transaction?.isRetry);
2143
+ const isImmediateCommit = Boolean(transaction?.immediateCommit);
1646
2144
  try {
2145
+ if (retryRound && this.activeCommitHandle && this.activeCommitHandle !== transaction) {
2146
+ this.backfillActiveHandleWrites(this.activeCommitHandle);
2147
+ }
1647
2148
  for (let i = 0; i < this.writes.length; i++) {
1648
2149
  let operation = this.writes[i];
1649
- if (!operation || (this.retries === 0 && operation.saved))
2150
+ if (!operation ||
2151
+ (isImmediateCommit
2152
+ ? !retryRound || !operation.saved || operation.stagedOn !== transaction
2153
+ : retryRound
2154
+ ? !operation.saved || (Boolean(operation.stagedOn) && operation.stagedOn !== transaction)
2155
+ : Boolean(operation.saved) || (this.saveCommits && Boolean(operation.deferSave))))
1650
2156
  continue;
1651
- this.save(operation, transaction, i < this.validated, options);
2157
+ this.save(operation, transaction, Boolean(operation.saved) || i < this.validated, options);
2158
+ if (!transaction)
2159
+ transaction = this.transaction;
1652
2160
  }
2161
+ if (transaction && (this.writes.length > 0 || hasDurableLog(transaction)))
2162
+ this.trackActiveCommitHandle(transaction);
1653
2163
  }
1654
2164
  catch (error) {
2165
+ if (!transaction)
2166
+ transaction = this.transaction ?? undefined;
1655
2167
  // abort() releases only this.transaction; a retry round's handle was detached before its
1656
2168
  // 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();
2169
+ if (transaction && transaction !== this.transaction)
2170
+ abortNativeTransaction(transaction, 'aborting a retry transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
2171
+ this.abort(false, true, transaction);
1660
2172
  throw error;
1661
2173
  }
1662
- this.validated = this.writes.length;
1663
- const completions = this.completions;
1664
- if (completions.length > 0)
1665
- this.completions = []; // reset
2174
+ if (!isImmediateCommit) {
2175
+ this.validated = this.saveCommits
2176
+ ? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
2177
+ : this.writes.length;
2178
+ }
2179
+ const completions = this.takeCompletions(transaction);
1666
2180
  const stagedWrites = this.writes.length;
1667
- if (completions.length > 0) {
2181
+ let inCommitPhase = false;
2182
+ if (completions.length > 0 && !isImmediateCommit) {
2183
+ inCommitPhase = true;
1668
2184
  this.setCommitPhase(true);
1669
2185
  }
1670
2186
  let commitResult;
1671
2187
  try {
1672
2188
  commitResult = (0, when_ts_1.when)(completions.length > 0 ? Promise.all(completions) : null, () => {
1673
- if (completions.length > 0)
2189
+ if (inCommitPhase) {
2190
+ inCommitPhase = false;
1674
2191
  this.setCommitPhase(false);
2192
+ }
1675
2193
  // The transaction can be aborted underneath us while we are parked in the await above — by the
1676
2194
  // monitor once the commit phase outlives its grace, or through the multi-store poison chain.
1677
2195
  // abort() cleared the write set and released the handle, so resuming would commit nothing and
@@ -1685,7 +2203,11 @@ class DatabaseTransaction {
1685
2203
  }
1686
2204
  if (stagedWrites > 0 && this.writes.length === 0 && this.open === exports.TRANSACTION_STATE.CLOSED)
1687
2205
  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) {
2206
+ if (!retryRound &&
2207
+ !isImmediateCommit &&
2208
+ (this.saveCommits
2209
+ ? this.writes.reduce((acc, w, idx) => (w && (w.saved || !w.deferSave) ? idx + 1 : acc), 0)
2210
+ : this.writes.length) > this.validated) {
1689
2211
  // 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
2212
  return this.commit({ ...options, continuation: true });
1691
2213
  }
@@ -1697,9 +2219,10 @@ class DatabaseTransaction {
1697
2219
  // and replay branches below deliberately commit a handle other than this.transaction.
1698
2220
  if (!transaction)
1699
2221
  transaction = this.transaction;
1700
- if (!options.transaction && this.writes.some((write) => write))
2222
+ if (!options.transaction && this.hasSavedWrite())
1701
2223
  this.renewChainForNativeCommit();
1702
- this.open = exports.TRANSACTION_STATE.CLOSED;
2224
+ if (!isImmediateCommit)
2225
+ this.open = exports.TRANSACTION_STATE.CLOSED;
1703
2226
  // RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
1704
2227
  // coordinatedRetry, or void on a normal commit/abort.
1705
2228
  let commitResolution;
@@ -1707,11 +2230,12 @@ class DatabaseTransaction {
1707
2230
  // recursions, immediate-commit re-entries, and a second top-level commit() (wrapper
1708
2231
  // commit after an explicit in-handler commit) must not steal a reference owned by an
1709
2232
  // outstanding iterator (doneReadTxn() would then never release the native handle).
1710
- if (!this.baseReadRefConsumed) {
2233
+ if (!isImmediateCommit && !this.baseReadRefConsumed) {
1711
2234
  this.baseReadRefConsumed = true;
1712
2235
  this.readTxnsUsed--;
1713
2236
  }
1714
- if (this.readTxnsUsed > 0) {
2237
+ let hadLeaseProtectedWrite = false;
2238
+ if (!isImmediateCommit && this.readTxnsUsed > 0) {
1715
2239
  // Outstanding iterators still stream through this.transaction — their native iterators
1716
2240
  // live inside it (GetIterator wraps its write batch + snapshot), so committing or
1717
2241
  // aborting the handle now would invalidate them mid-stream. Leave it open for the
@@ -1723,7 +2247,7 @@ class DatabaseTransaction {
1723
2247
  // kept the last iterator from finishing cleanly — a hung stream, the long-transaction
1724
2248
  // monitor's timeout abort — dropped writes the caller had already been told committed.
1725
2249
  this.writes = this.writes.filter((write) => write); // filter out removed entries
1726
- if (this.writes.length > 0) {
2250
+ if (this.hasSavedWrite() || hasDurableLog(transaction)) {
1727
2251
  if (!options.transaction) {
1728
2252
  if (!replayedWritesWarned) {
1729
2253
  replayedWritesWarned = true;
@@ -1741,24 +2265,40 @@ class DatabaseTransaction {
1741
2265
  const replayTransaction = new rocksdb_js_1.Transaction((this.writes[0].store.store ?? this.db.store), { coordinatedRetry: true });
1742
2266
  if (this.timestamp)
1743
2267
  replayTransaction.setTimestamp(this.timestamp);
1744
- this.retries++; // a replay round: commit handlers re-base on the reloaded entries
2268
+ const retries = (transaction?.retries ?? this.retries) + 1;
2269
+ replayTransaction.retries = retries;
2270
+ this.retries = retries; // a replay round: commit handlers re-base on the reloaded entries
2271
+ const prevStagedTxnTime = this.getHandleStagedTxnTime(transaction);
2272
+ if (transaction)
2273
+ this.releaseActiveCommitHandle(transaction);
2274
+ this.trackActiveCommitHandle(replayTransaction);
2275
+ if (prevStagedTxnTime)
2276
+ this.setHandleStagedTxnTime(replayTransaction, prevStagedTxnTime);
2277
+ transaction = replayTransaction;
1745
2278
  try {
1746
2279
  for (const operation of this.writes) {
2280
+ if (!operation?.saved)
2281
+ continue;
1747
2282
  this.save(operation, replayTransaction, true, options);
1748
2283
  }
1749
2284
  }
1750
2285
  catch (error) {
1751
- abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw');
1752
- this.abort();
2286
+ abortNativeTransaction(replayTransaction, 'aborting a replay transaction whose re-save threw', this.writes[0]?.store ?? this.db, this.startedFrom);
2287
+ this.releaseActiveCommitHandle(replayTransaction);
2288
+ this.abort(false, true, this.hasSiblingCommitInFlight(replayTransaction) ? replayTransaction : undefined);
1753
2289
  throw error;
1754
2290
  }
1755
- transaction = replayTransaction;
1756
2291
  }
1757
2292
  // with options.transaction set this is a retry round — the save loop above already
1758
2293
  // re-staged the writes into it
1759
2294
  this.assertDatabaseCommitAllowed(transaction);
1760
2295
  this.markNativeCommitSubmitted();
1761
2296
  commitResolution = transaction.commit();
2297
+ this.setHandleCommitInFlight(transaction, true);
2298
+ if (this.hasLeaseProtectedWrite) {
2299
+ hadLeaseProtectedWrite = true;
2300
+ this.retainTransactionLockHolds(transaction);
2301
+ }
1762
2302
  recordCommitLatency(commitResolution, performance.now());
1763
2303
  // Write-queue-depth accounting for this replay commit happens uniformly below, via
1764
2304
  // trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
@@ -1779,10 +2319,11 @@ class DatabaseTransaction {
1779
2319
  }
1780
2320
  else {
1781
2321
  // no more reads need to be performed, just commit/abort based if there are any writes
1782
- this.detachOwnedTransaction(); // any further operations operate immediately
2322
+ if (!isImmediateCommit)
2323
+ this.detachOwnedTransaction(); // any further operations operate immediately
1783
2324
  if (transaction) {
1784
2325
  this.writes = this.writes.filter((write) => write); // filter out removed entries
1785
- if (this.writes.length > 0) {
2326
+ if (this.hasSavedWriteForHandle(transaction, isImmediateCommit) || hasDurableLog(transaction)) {
1786
2327
  // Commit retries can construct fresh ranges on this live handle after read ownership ends.
1787
2328
  readTransactionOwners.delete(transaction);
1788
2329
  // Re-fence before submitting: the loop above skips operations already marked saved, so
@@ -1790,25 +2331,30 @@ class DatabaseTransaction {
1790
2331
  // retry/replay path re-saves every operation and is fenced there instead. Lease expiry
1791
2332
  // only: an unlock() inside the lease leaves the staged write valid, an elapsed lease
1792
2333
  // 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);
2334
+ if (this.hasLeaseProtectedWrite && !hasDurableLog(transaction)) {
2335
+ for (let i = 0; i < this.writes.length; i++) {
2336
+ const write = this.writes[i];
2337
+ if (!write?.saved ||
2338
+ (isImmediateCommit
2339
+ ? write.stagedOn !== transaction
2340
+ : Boolean(write.stagedOn) && write.stagedOn !== transaction))
2341
+ continue;
2342
+ const lapsed = write.lockHandle;
2343
+ if (!lapsed?.isLeaseExpired())
2344
+ continue;
2345
+ abortNativeTransaction(transaction, 'aborting a transaction whose record lock lapsed before submission', this.writes[0]?.store ?? this.db, this.startedFrom);
2346
+ // Every other terminal exit from commit() runs the logical cleanup too. Throwing
2347
+ // straight out would strand the OTHER locks this transaction holds, its staged
2348
+ // blobs, and the context's back-reference to a CLOSED transaction — and
2349
+ // transaction()'s onComplete has no rejection path to run it later.
2350
+ try {
2351
+ this.abort(false, true, transaction);
2352
+ }
2353
+ catch (error) {
2354
+ harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
2355
+ }
2356
+ throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
1810
2357
  }
1811
- throw (0, recordLock_ts_1.lockNotHeldError)(lapsed);
1812
2358
  }
1813
2359
  // The transaction was created with coordinatedRetry:true (see
1814
2360
  // getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
@@ -1817,6 +2363,11 @@ class DatabaseTransaction {
1817
2363
  this.assertDatabaseCommitAllowed(transaction);
1818
2364
  this.markNativeCommitSubmitted();
1819
2365
  commitResolution = transaction.commit();
2366
+ this.setHandleCommitInFlight(transaction, true);
2367
+ if (this.hasLeaseProtectedWrite) {
2368
+ hadLeaseProtectedWrite = true;
2369
+ this.retainTransactionLockHolds(transaction);
2370
+ }
1820
2371
  // Record how long this commit stays outstanding (submit → settle) as a distribution
1821
2372
  // metric. This is the same clock the overload check uses (trackOutstandingCommit
1822
2373
  // stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
@@ -1836,7 +2387,10 @@ class DatabaseTransaction {
1836
2387
  try {
1837
2388
  commitResolution = transaction.abort();
1838
2389
  }
1839
- catch {
2390
+ catch (abortError) {
2391
+ if (abortError?.code === 'ERR_TRANSACTION_ABANDONED') {
2392
+ harperLogger.error?.(`Aborted a transaction with a durable log entry (empty write set), ${describeCommitIdentity(this.db, this.startedFrom, transaction)}`, abortError);
2393
+ }
1840
2394
  // The transaction has uncommitted writes that were already cleared from
1841
2395
  // this.writes by a concurrent immediate-commit path (e.g. writes made with
1842
2396
  // an explicitly-reused closed transaction). Those writes are handled by the
@@ -1849,38 +2403,53 @@ class DatabaseTransaction {
1849
2403
  // Read the table off the write itself, not this.db, which is whichever table first
1850
2404
  // claimed this per-database transaction in txnForContext and so can name the wrong
1851
2405
  // table when a transaction spans more than one table in the same database.
1852
- trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
2406
+ trackOutstandingCommit(commitResolution, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
1853
2407
  // 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();
2408
+ // root's clock rather than restart it (or the per-handle clock on an unchained
2409
+ // ImmediateTransaction), so only the first submission stamps.
2410
+ const carrier = this.budgetCarrier(transaction);
2411
+ if (carrier.commitStartedAt == null)
2412
+ carrier.commitStartedAt = performance.now();
1858
2413
  const completions = [];
1859
2414
  const commitOutcome = commitResolution.then((commitResult) => {
2415
+ const txnWithRetry = transaction;
1860
2416
  if (commitResult === RETRY_NOW_VALUE) {
1861
- this.retries++;
1862
- harperLogger.debug?.('coordinated retry', transaction.id, this.retries);
2417
+ this.setHandleCommitInFlight(transaction, false);
2418
+ const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
2419
+ if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
2420
+ this.retries = retries;
2421
+ harperLogger.debug?.('coordinated retry', transaction.id, retries);
1863
2422
  // Mark this specific native transaction as a retry so RocksTransactionLogStore
1864
2423
  // 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
- }
2424
+ txnWithRetry.isRetry = true;
2425
+ const logged = hasDurableLog(transaction);
2426
+ const pastBudget = this.elapsedPastCommitBudget(transaction);
2427
+ if (pastBudget) {
2428
+ if (logged)
2429
+ this.noteOverdueLoggedCommit(transaction, pastBudget);
2430
+ else
2431
+ this.abandonCommitAfterDeadline(transaction, pastBudget);
2432
+ }
2433
+ if (retries > MAX_RETRIES && !this.sourceApply && !logged) {
2434
+ // giving up: poison and abort the whole linked chain so no link leaks its native
2435
+ // handle / read snapshot — or any unpublished transaction-log position — until GC.
2436
+ this.abortChainAfterRetries(transaction);
2437
+ throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} coordinated retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
2438
+ }
2439
+ if ((this.sourceApply || logged) && retries > 0 && retries % MAX_RETRIES === 0) {
2440
+ harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} coordinated retries; continuing to retry`);
2441
+ }
2442
+ // Randomized so two uncapped logged commits crossing on the same keys stop waking in lockstep.
2443
+ if (logged && retries > 2) {
2444
+ const backoff = (0, promises_1.setTimeout)(Math.ceil(Math.min(retries * 2, MAX_RETRY_DELAY_MS) * Math.random()));
2445
+ trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
2446
+ return backoff.then(() => this.commit({ ...options, transaction }));
1881
2447
  }
1882
2448
  return this.commit({ ...options, transaction });
1883
2449
  }
2450
+ const committedTransaction = transaction;
2451
+ const committedImmediate = Boolean(committedTransaction?.immediateCommit);
2452
+ this.setHandleCommitted(transaction);
1884
2453
  // onCommit may be async (e.g. RocksTransactionLogStore emits 'aftercommit'). Surface a
1885
2454
  // rejection — or a synchronous throw — via logging rather than failing the commit, since
1886
2455
  // the write is already durable.
@@ -1892,7 +2461,14 @@ class DatabaseTransaction {
1892
2461
  catch (error) {
1893
2462
  harperLogger.warn?.('onCommit handler failed after commit', error);
1894
2463
  }
1895
- if (this.next) {
2464
+ if (this.recordLocks)
2465
+ this.noteCommittedLockVersions(committedTransaction);
2466
+ releaseTransactionLockHolds(transaction);
2467
+ if (committedImmediate
2468
+ ? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasSiblingCommitInFlight(committedTransaction)
2469
+ : !this.saveCommits || !this.hasSiblingCommitInFlight(committedTransaction))
2470
+ this.releaseRecordLocks();
2471
+ if (this.next && !committedImmediate) {
1896
2472
  // never forward options.transaction (a retry/replay round's HEAD-store handle) to
1897
2473
  // the next store — it must commit its own writes through its own transaction
1898
2474
  let nextCommit;
@@ -1915,7 +2491,7 @@ class DatabaseTransaction {
1915
2491
  // if we want to wait for replication confirmation, we need to track the transaction times
1916
2492
  // and when replication notifications come in, we count the number of confirms until we reach the desired number
1917
2493
  const databaseName = this.writes[0]?.store.rootStore.databaseName;
1918
- const lastEntry = this.lastConfirmableEntry();
2494
+ const lastEntry = this.lastConfirmableEntry(true);
1919
2495
  if (confirmReplication && databaseName && lastEntry) {
1920
2496
  completions.push(confirmReplication(databaseName, lastEntry.version, this.replicatedConfirmation));
1921
2497
  }
@@ -1926,24 +2502,36 @@ class DatabaseTransaction {
1926
2502
  // later write retained survives). Deferred until here so a retry that *would* have
1927
2503
  // referenced the blob can flip skipped/superseded back to false first.
1928
2504
  for (const write of this.writes) {
1929
- if (write?.savedBlobs && (write.skipped || (write.superseded && !write.blobsAuditReferenced)))
2505
+ if (!write?.saved || !write.savedBlobs)
2506
+ continue;
2507
+ if (committedImmediate
2508
+ ? write.stagedOn !== committedTransaction
2509
+ : Boolean(write.stagedOn) && write.stagedOn !== committedTransaction)
2510
+ continue;
2511
+ if ((write.skipped &&
2512
+ !(write.blobsAuditReferenced &&
2513
+ (write.appendedTo === committedTransaction || hasDurableLog(write.appendedTo)))) ||
2514
+ (write.superseded && !write.blobsAuditReferenced))
1930
2515
  (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
1931
2516
  }
1932
- if (this.recordLocks)
1933
- this.noteCommittedLockVersions();
2517
+ const committedStagedTxnTime = this.getHandleStagedTxnTime(committedTransaction);
1934
2518
  // 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
2519
+ this.clearWrites(committedTransaction);
2520
+ if (!committedImmediate) {
2521
+ this.retries = 0;
2522
+ if (options.doneWriting)
2523
+ this.endScopeOwnership();
2524
+ this.releaseContext(!!options.doneWriting);
2525
+ }
2526
+ let txnTime = committedStagedTxnTime || this.timestamp || 0;
2527
+ if ((!committedImmediate || this.open !== exports.TRANSACTION_STATE.OPEN) &&
2528
+ this.writes.length === 0 &&
2529
+ !this.hasActiveCommitHandle())
2530
+ this.timestamp = 0; // reset the timestamp as well
1943
2531
  return Promise.all(completions).then(() => {
1944
2532
  // Only once the chained store's commit has settled, as on the synchronous path: a
1945
2533
  // partially failed mid-scope commit must not leave the scope resumable.
1946
- this.completeMidScopeCommit(options);
2534
+ this.completeMidScopeCommit(options, committedTransaction);
1947
2535
  return {
1948
2536
  txnTime,
1949
2537
  };
@@ -1951,10 +2539,14 @@ class DatabaseTransaction {
1951
2539
  // As on the synchronous path: a completion that failed (a chained store's commit,
1952
2540
  // a replication confirmation) leaves this commit partly landed, so ownership goes
1953
2541
  // with it rather than letting a later commit rotate on top.
1954
- this.endScopeOwnership();
2542
+ committedTransaction.committed = true;
2543
+ if (!committedImmediate)
2544
+ this.endScopeOwnership();
1955
2545
  throw error;
1956
2546
  });
1957
2547
  }, (error) => {
2548
+ const txnWithRetry = transaction;
2549
+ this.setHandleCommitInFlight(transaction, false);
1958
2550
  // Coordinated transactions surface conflicts as RETRY_NOW (handled in the
1959
2551
  // resolve branch above) and never reach here with ERR_BUSY. But not every
1960
2552
  // write transaction is coordinated — a write that reaches save() with no
@@ -1971,8 +2563,10 @@ class DatabaseTransaction {
1971
2563
  if (error.code === 'ERR_BUSY' || error.code === 'ERR_TRY_AGAIN') {
1972
2564
  // if the transaction failed due to concurrent changes, we need to retry. First record this as an increased risk of contention/retry
1973
2565
  // for future transactions
1974
- this.retries++;
1975
- harperLogger.debug?.('retrying', transaction.id, this.retries);
2566
+ const retries = (txnWithRetry.retries = (txnWithRetry.retries ?? 0) + 1);
2567
+ if (!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN)
2568
+ this.retries = retries;
2569
+ harperLogger.debug?.('retrying', transaction.id, retries);
1976
2570
  // ERR_BUSY and ERR_TRY_AGAIN are both retried by recommitting the SAME native
1977
2571
  // transaction. ERR_BUSY recovers because the save loop re-writes each key, re-tracking
1978
2572
  // it at the current sequence. ERR_TRY_AGAIN — a snapshot stranded outside the memtable
@@ -1985,35 +2579,47 @@ class DatabaseTransaction {
1985
2579
  // already-staged change-feed entry publishes only when the retry really commits — no
1986
2580
  // premature publish, and no fresh-transaction replay that would drop the entry.
1987
2581
  // Mark the native transaction as a retry so RocksTransactionLogStore skips re-staging entries.
1988
- transaction.isRetry = true;
2582
+ txnWithRetry.isRetry = true;
2583
+ if (error?.hasLog === true) {
2584
+ txnWithRetry.hasStagedLog = true;
2585
+ }
2586
+ const logged = hasDurableLog(transaction, error);
1989
2587
  // Before the backoff gate below: the budget can already be spent on the first
1990
2588
  // 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) {
2589
+ const pastBudget = this.elapsedPastCommitBudget(transaction);
2590
+ if (pastBudget) {
2591
+ if (logged)
2592
+ this.noteOverdueLoggedCommit(transaction, pastBudget);
2593
+ else
2594
+ this.abandonCommitAfterDeadline(transaction, pastBudget);
2595
+ }
2596
+ if (retries > 2) {
1995
2597
  // Transactions applying data from a canonical source of truth (replication peer or
1996
2598
  // external caching source) must never drop a write on a transient conflict: there is no
1997
2599
  // re-subscribe / sequence-id-resume path, so a dropped write would leave this node
1998
2600
  // permanently diverged (harper-pro#348). Such transactions retry without a cap; the source
1999
2601
  // 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
- }
2602
+ // compounding. Durably logged transactions likewise retry without a cap so a written
2603
+ // transaction-log entry is never abandoned.
2604
+ const neverDropOnConflict = this.sourceApply || logged;
2605
+ if (retries > MAX_RETRIES && !neverDropOnConflict) {
2606
+ // giving up: poison and abort the whole linked chain so no link leaks its native
2607
+ // handle / read snapshot until GC.
2608
+ this.abortChainAfterRetries(transaction);
2609
+ throw new hdbError_ts_1.ServerError(`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`);
2610
+ }
2611
+ // Uncapped retry can otherwise stall silently (debug logging is off in production);
2612
+ // surface periodic visibility into a stalled source-apply or logged commit.
2613
+ if (neverDropOnConflict && retries % MAX_RETRIES === 0) {
2614
+ harperLogger.warn?.(`${this.sourceApply ? 'Source-applied' : 'Logged'} transaction ${transaction.id} still in conflict after ${retries} retries; continuing to retry`);
2014
2615
  }
2015
2616
  // 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 }));
2617
+ const backoff = (0, promises_1.setTimeout)(Math.min(retries * retries, MAX_RETRY_DELAY_MS));
2618
+ if (logged) {
2619
+ // waitForDrain() must outlast the backoff too
2620
+ trackOutstandingCommit(backoff, this.writes[0]?.store ?? this.db, this.startedFrom, transaction);
2621
+ }
2622
+ return backoff.then(() => this.commit({ ...options, transaction }));
2017
2623
  }
2018
2624
  return this.commit({ ...options, transaction }); // try again
2019
2625
  }
@@ -2021,54 +2627,87 @@ class DatabaseTransaction {
2021
2627
  // terminal (non-conflict) failure: release the native handle so it doesn't leak;
2022
2628
  // usually already released by the failed commit itself, abort for the unexpected
2023
2629
  // 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);
2630
+ if (error?.hasLog) {
2631
+ txnWithRetry.isRetry = true;
2632
+ txnWithRetry.hasStagedLog = true;
2029
2633
  }
2634
+ abortNativeTransaction(transaction, 'aborting transaction after failed commit', this.writes[0]?.store ?? this.db, this.startedFrom);
2030
2635
  // A terminal failure is just as final as a success — release the context's
2031
2636
  // back-reference while handling the native transaction. performCommit's outer
2032
2637
  // rejection handler then aborts the wrapper and its linked chain.
2033
2638
  // A failed commit must never be followed by a resumed segment: this generation is
2034
2639
  // 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;
2640
+ if (!isImmediateCommit &&
2641
+ !this.hasWriteOwnedByActiveCommit(transaction, this.hasSiblingCommitInFlight(transaction), true, true)) {
2642
+ this.endScopeOwnership();
2643
+ this.releaseContext(!!options.doneWriting);
2644
+ }
2645
+ if (!hasDurableLog(transaction, error)) {
2646
+ for (const write of this.writes) {
2647
+ if (!write?.saved ||
2648
+ !write.savedBlobs ||
2649
+ (isImmediateCommit
2650
+ ? write.stagedOn !== transaction
2651
+ : Boolean(write.stagedOn) && write.stagedOn !== transaction))
2652
+ continue;
2653
+ (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
2654
+ }
2655
+ }
2656
+ this.clearWrites(transaction);
2657
+ releaseTransactionLockHolds(transaction);
2658
+ if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
2659
+ !this.hasActiveCommitHandle() &&
2660
+ this.writes.length === 0)
2661
+ this.releaseRecordLocks();
2662
+ if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle())
2663
+ this.timestamp = 0;
2039
2664
  throw error;
2040
2665
  }
2041
2666
  });
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;
2667
+ // Release any lock holds remaining if `commitOutcome` rejects (for example when a chained
2668
+ // store's commit or a completion fails) through the returned promise rather than a second
2669
+ // subscriber that would mark an unawaited commit rejection as handled.
2670
+ if (!hadLeaseProtectedWrite)
2671
+ return commitOutcome;
2672
+ return commitOutcome.then(undefined, (error) => {
2673
+ releaseTransactionLockHolds(transaction);
2053
2674
  throw error;
2054
2675
  });
2055
2676
  }
2056
2677
  for (const write of this.writes) {
2057
- if (write?.savedBlobs && (write.skipped || (write.superseded && !write.blobsAuditReferenced)))
2678
+ if (!write?.saved)
2679
+ continue;
2680
+ if (isImmediateCommit
2681
+ ? write.stagedOn !== transaction
2682
+ : this.hasSiblingCommitInFlight(transaction) && Boolean(write.stagedOn) && write.stagedOn !== transaction)
2683
+ continue;
2684
+ if (write.savedBlobs &&
2685
+ ((write.skipped &&
2686
+ !(write.blobsAuditReferenced && (write.appendedTo === transaction || hasDurableLog(write.appendedTo)))) ||
2687
+ (write.superseded && !write.blobsAuditReferenced)))
2058
2688
  (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
2059
2689
  }
2060
2690
  if (this.recordLocks)
2061
- this.noteCommittedLockVersions();
2062
- this.clearWrites();
2063
- this.releaseRecordLocks();
2064
- if (options.doneWriting)
2065
- this.endScopeOwnership();
2066
- this.releaseContext(!!options.doneWriting);
2691
+ this.noteCommittedLockVersions(transaction);
2692
+ const committedStagedTxnTime = this.getHandleStagedTxnTime(transaction);
2693
+ this.clearWrites(transaction);
2694
+ if (isImmediateCommit
2695
+ ? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle() && this.writes.length === 0
2696
+ : !this.saveCommits || (!this.hasActiveCommitHandle() && this.writes.length === 0))
2697
+ this.releaseRecordLocks();
2698
+ if (!isImmediateCommit) {
2699
+ if (options.doneWriting)
2700
+ this.endScopeOwnership();
2701
+ this.releaseContext(!!options.doneWriting);
2702
+ }
2067
2703
  const txnResolution = {
2068
- txnTime: this.timestamp,
2704
+ txnTime: committedStagedTxnTime || this.timestamp,
2069
2705
  };
2070
- this.timestamp = 0; // reset like the async path (~1279) so stale lock stamps don't persist
2071
- if (this.next) {
2706
+ if ((!isImmediateCommit || this.open !== exports.TRANSACTION_STATE.OPEN) &&
2707
+ this.writes.length === 0 &&
2708
+ !this.hasActiveCommitHandle())
2709
+ this.timestamp = 0;
2710
+ if (this.next && !isImmediateCommit) {
2072
2711
  // now run any other transactions
2073
2712
  options.timestamp = txnResolution.txnTime;
2074
2713
  // as above: the next store must not inherit this store's explicit native transaction
@@ -2087,7 +2726,7 @@ class DatabaseTransaction {
2087
2726
  return nextResolution?.then((nextResolution) => {
2088
2727
  // Only once the chained store's own commit has SETTLED: rotating first would leave the
2089
2728
  // scope resumable after a partially failed mid-scope commit.
2090
- this.completeMidScopeCommit(options);
2729
+ this.completeMidScopeCommit(options, transaction);
2091
2730
  return {
2092
2731
  txnTime: txnResolution.txnTime,
2093
2732
  next: nextResolution,
@@ -2102,18 +2741,20 @@ class DatabaseTransaction {
2102
2741
  });
2103
2742
  txnResolution.next = nextResolution;
2104
2743
  }
2105
- this.completeMidScopeCommit(options);
2744
+ this.completeMidScopeCommit(options, transaction);
2106
2745
  return txnResolution;
2107
2746
  });
2108
2747
  }
2109
2748
  catch (error) {
2110
- this.setCommitPhase(false);
2111
- this.abortAfterCommitError(error);
2749
+ if (inCommitPhase)
2750
+ this.setCommitPhase(false);
2751
+ this.abortAfterCommitError(error, transaction);
2112
2752
  }
2113
2753
  if (commitResult?.then)
2114
2754
  return commitResult.catch((error) => {
2115
- this.setCommitPhase(false);
2116
- this.abortAfterCommitError(error);
2755
+ if (inCommitPhase)
2756
+ this.setCommitPhase(false);
2757
+ this.abortAfterCommitError(error, transaction);
2117
2758
  });
2118
2759
  return commitResult;
2119
2760
  }
@@ -2141,8 +2782,12 @@ class DatabaseTransaction {
2141
2782
  this.#scopeOwned = false;
2142
2783
  this.snapshotFree = false;
2143
2784
  }
2144
- completeMidScopeCommit(options) {
2785
+ completeMidScopeCommit(options, committedTransaction) {
2786
+ if (committedTransaction?.immediateCommit)
2787
+ return;
2145
2788
  this.next = null;
2789
+ if (this.saveCommits && this.hasActiveCommitHandle())
2790
+ return;
2146
2791
  this.rotateAfterMidScopeCommit(options);
2147
2792
  }
2148
2793
  /** See completeMidScopeCommit, which is the only caller and carries the reasoning. */
@@ -2157,82 +2802,147 @@ class DatabaseTransaction {
2157
2802
  this.open = exports.TRANSACTION_STATE.OPEN;
2158
2803
  this.snapshotFree = true;
2159
2804
  this.writesAbandoned = false;
2805
+ if (this.#context && (this.#context.transaction === this || this.#context.transaction === exports.RELEASED_TRANSACTION)) {
2806
+ this.#context.transaction = this;
2807
+ }
2160
2808
  }
2161
2809
  assertDatabaseCommitAllowed(transaction) {
2810
+ if (hasDurableLog(transaction))
2811
+ return;
2162
2812
  const rootStore = this.writes[0]?.store?.rootStore;
2163
2813
  if (!databaseCommitsSuspended(rootStore))
2164
2814
  return;
2165
2815
  const retryable = !this.root && !this.snapshotFree;
2166
2816
  if (this.transaction === transaction)
2167
2817
  this.detachOwnedTransaction();
2168
- abortNativeTransaction(transaction, 'aborting a transaction while its database is closing');
2818
+ abortNativeTransaction(transaction, 'aborting a transaction while its database is closing', this.writes[0]?.store ?? this.db, this.startedFrom);
2169
2819
  try {
2170
- this.abort();
2820
+ this.abort(false, true, transaction);
2171
2821
  }
2172
2822
  catch (error) {
2173
2823
  harperLogger.debug?.('cleaning up a transaction while its database is closing', error);
2174
2824
  }
2175
2825
  throw new hdbError_ts_1.DatabaseClosingError(rootStore?.databaseName ?? rootStore?.path ?? 'unknown', retryable);
2176
2826
  }
2177
- abortAfterCommitError(error) {
2827
+ abortAfterCommitError(error, targetTransaction) {
2178
2828
  try {
2829
+ const retryTxn = targetTransaction;
2830
+ const retryTxnCommitted = this.isHandleCommitted(retryTxn);
2831
+ const retryTxnInFlight = this.isHandleCommitInFlight(retryTxn);
2832
+ if (retryTxn && retryTxnCommitted)
2833
+ retryTxn.committed = true;
2834
+ if (retryTxn)
2835
+ this.releaseActiveCommitHandle(retryTxn);
2836
+ if (retryTxn && retryTxn !== this.transaction && !retryTxnCommitted && !retryTxnInFlight) {
2837
+ releaseTransactionLockHolds(retryTxn);
2838
+ abortNativeTransaction(retryTxn, 'aborting a retry transaction after pre-commit failure', this.writes[0]?.store ?? this.db, this.startedFrom);
2839
+ }
2179
2840
  // Not "always retain": abort(true) keeps the native handle only while read iterators still
2180
2841
  // own it. resources/transaction.ts's own post-error abort passes the same argument, so the
2181
2842
  // two layers cannot contradict each other one frame apart.
2182
- this.abort(true);
2843
+ this.abort(true, true, targetTransaction);
2183
2844
  }
2184
2845
  catch (abortError) {
2185
2846
  harperLogger.debug?.('aborting transaction after a failed commit', abortError);
2186
2847
  }
2187
2848
  throw error;
2188
2849
  }
2189
- abort(retainReadTransaction = false, cascade = true) {
2190
- const next = cascade ? this.next : undefined;
2191
- if (cascade && !retainReadTransaction)
2850
+ abort(retainReadTransaction = false, cascade = true, targetTransaction) {
2851
+ const nativeTxn = targetTransaction ?? this.transaction;
2852
+ const isImmediateTarget = Boolean(targetTransaction?.immediateCommit);
2853
+ const hasSiblingInFlight = this.hasSiblingCommitInFlight(targetTransaction);
2854
+ const preserveActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true);
2855
+ const preserveNonImmediateActiveWrites = isImmediateTarget || this.hasWriteOwnedByActiveCommit(targetTransaction, hasSiblingInFlight, true, true);
2856
+ const cascadeNext = cascade && !preserveNonImmediateActiveWrites;
2857
+ const next = cascadeNext ? this.next : undefined;
2858
+ if (cascadeNext && !retainReadTransaction)
2192
2859
  this.next = null;
2193
2860
  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;
2861
+ const ownsCurrentTransaction = !isImmediateTarget && (!hasSiblingInFlight || !targetTransaction || this.transaction === targetTransaction);
2862
+ const currentTxn = this.transaction;
2863
+ const currentTxnActive = Boolean(currentTxn) &&
2864
+ !currentTxn.abortedByWrapper &&
2865
+ !this.isHandleCommitted(currentTxn) &&
2866
+ (this.isHandleCommitInFlight(currentTxn) ||
2867
+ hasDurableLog(currentTxn) ||
2868
+ (Boolean(targetTransaction) && currentTxn !== targetTransaction && this.isActiveCommitHandle(currentTxn)));
2869
+ if (ownsCurrentTransaction && !currentTxnActive) {
2870
+ if (this.transaction)
2871
+ this.releaseActiveCommitHandle(this.transaction);
2872
+ const hasOpenReadIterator = retainReadTransaction &&
2873
+ this.transaction &&
2874
+ (this.readTxnsUsed > 1 || (this.baseReadRefConsumed && this.readTxnsUsed > 0));
2875
+ if (hasOpenReadIterator) {
2876
+ this.releaseRetainedWriteIntents();
2877
+ if (!this.baseReadRefConsumed) {
2878
+ this.doneReadTxn();
2879
+ this.baseReadRefConsumed = true;
2880
+ }
2202
2881
  }
2882
+ else {
2883
+ while (this.readTxnsUsed > 0)
2884
+ this.doneReadTxn(); // release the read snapshot when we abort, we assume we don't need it
2885
+ }
2886
+ // A write-only transaction never took a read reference (getReadTxn was never called), so the
2887
+ // loop above releases nothing even though save() created a native handle. Keep an iterator's
2888
+ // handle alive, but release every other remaining handle through the shared cleanup path.
2889
+ if (this.transaction && !hasOpenReadIterator)
2890
+ this.releaseReadTxn();
2203
2891
  }
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();
2892
+ if (!isImmediateTarget)
2893
+ this.open = exports.TRANSACTION_STATE.CLOSED;
2894
+ this.drainCompletions(targetTransaction, preserveActiveWrites);
2895
+ const targetLogged = hasDurableLog(targetTransaction);
2216
2896
  for (const write of this.writes) {
2217
- if (write?.savedBlobs)
2897
+ if (!write)
2898
+ continue;
2899
+ if (preserveActiveWrites && this.isWriteOwnedByActiveCommit(write, targetTransaction, hasSiblingInFlight, true))
2900
+ continue;
2901
+ if (targetLogged &&
2902
+ write.saved &&
2903
+ (isImmediateTarget
2904
+ ? write.stagedOn === targetTransaction
2905
+ : !write.stagedOn || write.stagedOn === targetTransaction))
2906
+ continue;
2907
+ if (write.savedBlobs)
2218
2908
  (0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
2219
2909
  }
2220
2910
  }
2221
2911
  finally {
2222
2912
  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();
2913
+ if (!isImmediateTarget)
2914
+ this.open = exports.TRANSACTION_STATE.CLOSED;
2915
+ const endScopeOnAbort = !isImmediateTarget && (!targetTransaction || !preserveNonImmediateActiveWrites);
2916
+ if (endScopeOnAbort)
2917
+ this.endScopeOwnership(); // the scope is over; nothing may rotate this instance again
2918
+ this.clearWrites(targetTransaction, true);
2919
+ if (targetTransaction) {
2920
+ releaseTransactionLockHolds(targetTransaction);
2921
+ }
2922
+ if ((!isImmediateTarget || this.open !== exports.TRANSACTION_STATE.OPEN) && !this.hasActiveCommitHandle()) {
2923
+ this.timestamp = 0; // a lock stamp pinned for this write set must not leak into the next
2924
+ this.retries = 0;
2925
+ }
2926
+ if ((isImmediateTarget
2927
+ ? this.open !== exports.TRANSACTION_STATE.OPEN && !this.hasActiveCommitHandle()
2928
+ : !preserveActiveWrites) &&
2929
+ this.writes.length === 0) {
2930
+ releaseTransactionLockHolds(nativeTxn);
2931
+ if (this.transaction && this.transaction !== nativeTxn)
2932
+ releaseTransactionLockHolds(this.transaction);
2933
+ this.releaseRecordLocks();
2934
+ }
2227
2935
  // A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
2228
2936
  // Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
2229
2937
  // starting a fresh one) so the rest of the logical operation fails atomically via the
2230
2938
  // poison check in addWrite()/commit(), rather than silently landing a later write on a
2231
2939
  // brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
2232
2940
  // 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();
2941
+ if (endScopeOnAbort) {
2942
+ this.releaseContext(!this.timedOut && !this.disconnected);
2943
+ if (!this.root)
2944
+ this.releaseRequestAbortListener();
2945
+ }
2236
2946
  }
2237
2947
  finally {
2238
2948
  if (next && !(next.nativeCommitSubmitted && next.commitsInFlight)) {
@@ -2259,10 +2969,10 @@ class DatabaseTransaction {
2259
2969
  * Source-applied writes are exempt for the same reason they are exempt from the attempt cap:
2260
2970
  * there is no resubscribe/sequence-resume path, so dropping one permanently diverges this node.
2261
2971
  */
2262
- elapsedPastCommitBudget() {
2972
+ elapsedPastCommitBudget(transaction) {
2263
2973
  if (this.sourceApply)
2264
2974
  return 0;
2265
- const startedAt = (this.root ?? this).commitStartedAt;
2975
+ const startedAt = this.budgetCarrier(transaction).commitStartedAt;
2266
2976
  if (startedAt == null)
2267
2977
  return 0;
2268
2978
  const elapsed = performance.now() - startedAt;
@@ -2283,18 +2993,54 @@ class DatabaseTransaction {
2283
2993
  const elapsed = Math.round(elapsedMs);
2284
2994
  const budget = this.commitConflictBudget();
2285
2995
  const retryable = !this.root && !this.snapshotFree;
2996
+ const retries = headTransaction?.retries ?? this.retries;
2286
2997
  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, ` +
2998
+ let headTransactionId;
2999
+ try {
3000
+ headTransactionId = headTransaction?.id;
3001
+ }
3002
+ catch { }
3003
+ harperLogger.error?.(`Abandoning a write transaction: its commit has been in write-intent conflict for ${elapsed}ms ` +
3004
+ `(exceeds the ${budget}ms limit) across ${retries} retries, ` +
2289
3005
  describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
2290
3006
  `.` +
2291
- (0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransaction?.id) +
3007
+ (0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
2292
3008
  ` Another transaction holds a conflicting write intent and has not completed; the request is ` +
2293
3009
  `failed with a 503${retryable ? '' : ' (not retryable — an earlier store in this transaction already committed)'} ` +
2294
3010
  `rather than waiting further.`);
2295
3011
  }
2296
3012
  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);
3013
+ 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);
3014
+ }
3015
+ /**
3016
+ * A durably logged commit past its conflict budget cannot be aborted; release its deferred
3017
+ * record-lock holds (`holdBudgetSpent` prevents re-retaining them) and log once per chain/cooldown.
3018
+ */
3019
+ noteOverdueLoggedCommit(headTransaction, elapsedMs) {
3020
+ headTransaction.holdBudgetSpent = true;
3021
+ releaseTransactionLockHolds(headTransaction);
3022
+ const carrier = this.budgetCarrier(headTransaction);
3023
+ if (carrier.commitOverdueLogged)
3024
+ return;
3025
+ if (!allowStuckCommitLog('overdue', performance.now()))
3026
+ return;
3027
+ carrier.commitOverdueLogged = true;
3028
+ const elapsed = Math.round(elapsedMs);
3029
+ const budget = this.commitConflictBudget();
3030
+ const retries = headTransaction?.retries ?? this.retries;
3031
+ let headTransactionId;
3032
+ try {
3033
+ headTransactionId = headTransaction?.id;
3034
+ }
3035
+ catch { }
3036
+ harperLogger.error?.(`A durably logged write transaction is still in write-intent conflict after ${elapsed}ms ` +
3037
+ `(exceeds the ${budget}ms limit) across ${retries} retries, ` +
3038
+ describeCommitIdentity(this.writes[0]?.store, this.startedFrom, headTransaction) +
3039
+ `.` +
3040
+ (0, longLivedTransactions_ts_1.describeHolderCandidates)(this.writes[0]?.store?.rootStore?.path, headTransactionId) +
3041
+ ` Because its transaction-log entry is already durable on disk, it cannot be aborted and ` +
3042
+ `will continue retrying until the conflicting write intent releases (pinning the local ` +
3043
+ `transaction-log read watermark for subscriptions and replication while uncommitted).`);
2298
3044
  }
2299
3045
  /**
2300
3046
  * Give up on a chain of linked transactions after exhausting conflict retries: poison every link
@@ -2307,36 +3053,51 @@ class DatabaseTransaction {
2307
3053
  * from the replay transaction; retry exhaustion aborts both.
2308
3054
  */
2309
3055
  abortChainAfterRetries(headTransaction) {
3056
+ const isImmediateCommit = Boolean(headTransaction?.immediateCommit);
3057
+ const hasSiblingInFlight = this.hasSiblingCommitInFlight(headTransaction);
3058
+ if (isImmediateCommit ||
3059
+ (hasSiblingInFlight && this.hasWriteOwnedByActiveCommit(headTransaction, hasSiblingInFlight, true, true))) {
3060
+ if (!isImmediateCommit && this.transaction === headTransaction)
3061
+ this.detachOwnedTransaction();
3062
+ releaseTransactionLockHolds(headTransaction);
3063
+ abortNativeTransaction(headTransaction, 'aborting conflicted transaction in chain after exhausting retries', this.writes[0]?.store ?? this.db, this.startedFrom);
3064
+ try {
3065
+ this.abort(false, false, headTransaction);
3066
+ }
3067
+ catch (abortError) {
3068
+ harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
3069
+ }
3070
+ return;
3071
+ }
2310
3072
  for (let txn = this; txn; txn = txn.next) {
2311
3073
  txn.open = exports.TRANSACTION_STATE.CLOSED;
2312
3074
  }
2313
- for (let txn = this; txn; txn = txn.next) {
3075
+ for (let txn = this, next; txn; txn = next) {
3076
+ next = txn.next;
3077
+ txn.next = null;
2314
3078
  // Detach first so the abort() below performs only non-native cleanup, and so its doneReadTxn
2315
3079
  // loop cannot spin on a nulled handle. Not to avoid a double abort: rocksdb-js tolerates
2316
3080
  // abort-after-abort, and it is abort-after-COMMIT that throws.
2317
- const detached = txn.detachOwnedTransaction();
3081
+ const detached = txn === this &&
3082
+ hasSiblingInFlight &&
3083
+ txn.transaction &&
3084
+ txn.transaction !== headTransaction &&
3085
+ (this.isActiveCommitHandle(txn.transaction) || hasDurableLog(txn.transaction))
3086
+ ? null
3087
+ : txn.detachOwnedTransaction();
2318
3088
  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
- }
3089
+ releaseTransactionLockHolds(committingTransaction);
3090
+ abortNativeTransaction(committingTransaction, 'aborting conflicted transaction in chain after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
2325
3091
  // With outstanding iterators, the head owns a retained read handle while the retry commits
2326
3092
  // through a separate replay handle. Both must be aborted after retry exhaustion.
2327
3093
  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
- }
3094
+ abortNativeTransaction(detached, 'aborting retained read transaction after exhausting retries', txn.writes[0]?.store ?? txn.db, txn.startedFrom);
2334
3095
  }
2335
3096
  try {
2336
3097
  // abort() synchronously walks savedBlobs and can call write.store.getEntry(), which can throw
2337
3098
  // (closed store, decode error). Catch and continue so one link's wrapper-cleanup failure can't
2338
3099
  // strand later links' native handles — they were already detached/aborted above regardless.
2339
- txn.abort();
3100
+ txn.abort(false, false, committingTransaction);
2340
3101
  }
2341
3102
  catch (abortError) {
2342
3103
  harperLogger.debug?.('cleaning up conflicted transaction in chain after exhausting retries', abortError);
@@ -2430,18 +3191,7 @@ class DatabaseTransaction {
2430
3191
  */
2431
3192
  poisonAfterStalledSubmittedCommit() {
2432
3193
  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) {
3194
+ for (const txn of collectCommitLinks(root, this)) {
2445
3195
  txn.postSubmitPoisoned = true;
2446
3196
  // Not on a link an earlier poison already aborted: abort() cleared its write set, so excusing
2447
3197
  // its continuation from the poison check would let it resume onto nothing and report success.
@@ -2465,7 +3215,7 @@ class DatabaseTransaction {
2465
3215
  // throws. abort() rather than the native abort alone: it reclaims blobs a replayed write
2466
3216
  // staged.
2467
3217
  this.detachOwnedTransaction();
2468
- abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed');
3218
+ abortNativeTransaction(transaction, 'aborting a transaction whose synchronous commit failed', this.writes[0]?.store ?? this.db, this.startedFrom);
2469
3219
  try {
2470
3220
  this.abort();
2471
3221
  }
@@ -2505,10 +3255,12 @@ class ImmediateTransaction extends DatabaseTransaction {
2505
3255
  }
2506
3256
  save(...args) {
2507
3257
  const operation = args[0]; // the staged write, not a transaction — commit() re-enters here with it
2508
- if (this.isCommitting) {
3258
+ if (operation?.deferSave)
3259
+ operation.deferSave = false;
3260
+ if (this.isCommitting || args[1] || args[3]) {
2509
3261
  // Stage into the transaction commit() is committing (on a retry round, the one being retried)
2510
3262
  // and reload within it; with no handle super.save() would open its own and nest a commit().
2511
- super.save(operation, args[1], true);
3263
+ super.save(operation, args[1], true, args[3]);
2512
3264
  }
2513
3265
  else {
2514
3266
  this.admitRequestWrite(); // a deferred save reaches here without passing through addWrite()
@@ -2542,7 +3294,7 @@ class ImmediateTransaction extends DatabaseTransaction {
2542
3294
  return;
2543
3295
  for (const [store, storeMap] of recordLocks) {
2544
3296
  for (const [keyId, handle] of storeMap)
2545
- if (handle.released)
3297
+ if (handle.released || handle.isExpired())
2546
3298
  storeMap.delete(keyId);
2547
3299
  if (storeMap.size === 0)
2548
3300
  recordLocks.delete(store);
@@ -2704,6 +3456,12 @@ function reportIfLongLived(txn, thresholdMs, now, reportBudget) {
2704
3456
  /** Diagnostic grace after a native submission outlives the ordinary transaction timeout. */
2705
3457
  const MAX_DEFERRED_POISON_TICKS = 2;
2706
3458
  const STALLED_COMMIT_LOG_MIN_INTERVAL_MS = 1000;
3459
+ // A logged retry parked on a write intent moves its retry counter at least every native park timeout,
3460
+ // so a counter that has not moved for two of them is a native attempt that never settled — whatever
3461
+ // the monitor's tick interval (STORAGE_MAXTRANSACTIONOPENTIME can be set far below the park timeout).
3462
+ const configuredParkTimeoutMs = Number(process.env.ROCKSDB_JS_PARK_TIMEOUT_MS);
3463
+ // Same acceptance rule as rocksdb-js: anything but a finite positive number falls back to its 5000 ms default.
3464
+ const LOGGED_RETRY_STALL_MS = 2 * (Number.isFinite(configuredParkTimeoutMs) && configuredParkTimeoutMs > 0 ? configuredParkTimeoutMs : 5000);
2707
3465
  let lastStalledCommitWarningAt = -Infinity;
2708
3466
  let lastStalledCommitErrorAt = -Infinity;
2709
3467
  function submittedCommitStillInFlight(root) {
@@ -2717,12 +3475,36 @@ function submittedCommitStillInFlight(root) {
2717
3475
  }
2718
3476
  return false;
2719
3477
  }
3478
+ // Every link the monitor would poison for a stalled commit, so the two walks agree on which links count.
3479
+ function collectCommitLinks(root, txn) {
3480
+ const links = new Set();
3481
+ const collect = (start) => {
3482
+ for (let link = start; link; link = link.next)
3483
+ links.add(link);
3484
+ };
3485
+ collect(root);
3486
+ collect(txn);
3487
+ collect(root.submittedLink);
3488
+ if (root.submittedLinks)
3489
+ for (const link of root.submittedLinks)
3490
+ collect(link);
3491
+ return links;
3492
+ }
3493
+ function chainDurableLogRetries(root, txn) {
3494
+ let retries = -1;
3495
+ for (const link of collectCommitLinks(root, txn)) {
3496
+ const linkRetries = link.activeDurableLogRetries();
3497
+ if (linkRetries >= 0)
3498
+ retries = Math.max(retries, 0) + linkRetries;
3499
+ }
3500
+ return retries;
3501
+ }
2720
3502
  /**
2721
3503
  * True when this tick must leave `txn` alone because some link has submitted writes whose native
2722
3504
  * outcome is not known yet. Merely entering commit() is not enough: a wholly pre-submit transaction
2723
3505
  * remains subject to shouldSpareCommitPhase() and its ordinary bounded grace.
2724
3506
  */
2725
- function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
3507
+ function deferForCommitInFlight(txn, url, expiration = txnExpiration, tick = monitorTick) {
2726
3508
  const root = txn.root ?? txn;
2727
3509
  if (!txn.isChainCommitting() || !root.commitSubmitted)
2728
3510
  return false;
@@ -2735,27 +3517,36 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
2735
3517
  // diagnostic grace, nor report a legitimately slow one early.
2736
3518
  const now = performance.now();
2737
3519
  const deadline = (root.deferredPoisonDeadline ??= now + MAX_DEFERRED_POISON_TICKS * expiration);
3520
+ // A handle with a durable log entry retrying a conflict has a known outcome — it commits once the
3521
+ // conflicting intent clears — so while its retry counter keeps advancing it is neither stalled nor
3522
+ // poisoned, and noteOverdueLoggedCommit is the operator's signal. A counter that has not moved for
3523
+ // LOGGED_RETRY_STALL_MS is a native attempt that never settled, which is the stall handled below.
3524
+ const loggedRetries = chainDurableLogRetries(root, txn);
3525
+ let loggedRetry = false;
3526
+ if (loggedRetries >= 0) {
3527
+ if (root.loggedRetriesTick === tick) {
3528
+ loggedRetry = Boolean(root.loggedRetryAdvancing);
3529
+ }
3530
+ else {
3531
+ if (root.loggedRetriesSeen !== loggedRetries) {
3532
+ root.loggedRetriesSeen = loggedRetries;
3533
+ root.loggedRetriesChangedAt = now;
3534
+ }
3535
+ loggedRetry = now - root.loggedRetriesChangedAt < LOGGED_RETRY_STALL_MS;
3536
+ root.loggedRetriesTick = tick;
3537
+ root.loggedRetryAdvancing = loggedRetry;
3538
+ }
3539
+ }
2738
3540
  let hasUnsubmittedCommit = false;
2739
3541
  let unsubmittedCommitGraceExpired = false;
2740
3542
  if (now >= deadline) {
2741
3543
  // Only reclaim after the commit has genuinely stalled. Submitted writes no longer depend on these
2742
3544
  // separate snapshots; pre-submit handles can still carry writes needed by the attempt.
2743
3545
  txn.releaseStalledCommitReadResources();
2744
- if (!txn.isProtectedCommit()) {
3546
+ if (!loggedRetry && !txn.isProtectedCommit()) {
2745
3547
  if (!root.postSubmitPoisoned)
2746
3548
  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) {
3549
+ for (const link of collectCommitLinks(root, txn)) {
2759
3550
  if (link.committing && !link.nativeCommitSubmitted) {
2760
3551
  hasUnsubmittedCommit = true;
2761
3552
  break;
@@ -2775,14 +3566,16 @@ function deferForCommitInFlight(txn, url, expiration = txnExpiration) {
2775
3566
  txn.open === exports.TRANSACTION_STATE.CLOSED &&
2776
3567
  !(txn === root && submittedCommitStillInFlight(root)))
2777
3568
  return hasUnsubmittedCommit && !unsubmittedCommitGraceExpired;
2778
- if (!root.stalledCommitLogged && now - lastStalledCommitErrorAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
3569
+ if (!loggedRetry &&
3570
+ !root.stalledCommitLogged &&
3571
+ now - lastStalledCommitErrorAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
2779
3572
  lastStalledCommitErrorAt = now;
2780
3573
  root.stalledCommitLogged = true;
2781
3574
  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
3575
  `on this transaction is rejected until it settles, and the worker may require a restart, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
2783
3576
  }
2784
3577
  }
2785
- else if (now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
3578
+ else if (!loggedRetry && now - lastStalledCommitWarningAt >= STALLED_COMMIT_LOG_MIN_INTERVAL_MS) {
2786
3579
  lastStalledCommitWarningAt = now;
2787
3580
  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
3581
  }
@@ -2933,10 +3726,20 @@ function setMaxOutstandingTxnDuration(ms) {
2933
3726
  MAX_OUTSTANDING_TXN_DURATION = ms;
2934
3727
  return previous;
2935
3728
  }
3729
+ /** Test seam: overrides the per-retry backoff cap and returns the previous value. */
3730
+ function setMaxRetryDelayMs(ms) {
3731
+ const previous = MAX_RETRY_DELAY_MS;
3732
+ MAX_RETRY_DELAY_MS = ms;
3733
+ return previous;
3734
+ }
2936
3735
  /** Test seam: whether the monitor supervises this logical transaction for its writes. */
2937
3736
  function isWriteSupervised(txn) {
2938
3737
  return supervisedWriteRoots.has(txn);
2939
3738
  }
3739
+ /** Test seam: returns the current open-transaction monitor interval. */
3740
+ function getTxnExpiration() {
3741
+ return txnExpiration;
3742
+ }
2940
3743
  function setTxnExpiration(ms) {
2941
3744
  clearInterval(timer);
2942
3745
  txnExpiration = ms;