@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
@@ -1099,7 +1099,7 @@ export function allowedMethods(resource: any) {
1099
1099
  */
1100
1100
  function selectFromObject(object, propertyResolvers, context) {
1101
1101
  // TODO: eventually we will do aggregate functions here
1102
- const record = object.getRecord?.();
1102
+ const record = typeof object.getRecord === 'function' ? object.getRecord() : undefined;
1103
1103
  if (record) {
1104
1104
  const ownData = object.getChanges?.();
1105
1105
  return (property) => {
@@ -1109,25 +1109,36 @@ function selectFromObject(object, propertyResolvers, context) {
1109
1109
  }
1110
1110
  if (ownData && property in ownData) {
1111
1111
  return ownData[property];
1112
- } else if ((resolver = propertyResolvers?.[property])) {
1112
+ } else if (
1113
+ propertyResolvers &&
1114
+ Object.hasOwn(propertyResolvers, property) &&
1115
+ (resolver = propertyResolvers[property])
1116
+ ) {
1113
1117
  return resolver(object, context);
1114
1118
  } else return record[property];
1115
1119
  };
1116
1120
  } else if (propertyResolvers) {
1117
1121
  return (property) => {
1118
- const resolver = propertyResolvers[property];
1119
- return resolver ? resolver(object, context) : object[property];
1122
+ const resolver = Object.hasOwn(propertyResolvers, property) ? propertyResolvers[property] : undefined;
1123
+ if (resolver) return resolver(object, context);
1124
+ const value = object[property];
1125
+ return typeof value === 'function' ? undefined : value;
1126
+ };
1127
+ } else
1128
+ return (property) => {
1129
+ const value = object[property];
1130
+ return typeof value === 'function' ? undefined : value;
1120
1131
  };
1121
- } else return (property) => object[property];
1122
1132
  }
1123
1133
  export function transformForSelect(select, resource) {
1124
- const propertyResolvers = resource.propertyResolvers;
1125
- const context = resource.getContext?.();
1134
+ const propertyResolvers = resource?.propertyResolvers;
1135
+ const context = resource?.getContext?.();
1126
1136
  let subTransforms;
1127
1137
  if (typeof select === 'string')
1128
1138
  // if select is a single string then return property value
1129
1139
  return function transform(object) {
1130
- if (object.then) return object.then(transform);
1140
+ if (object == null || (!resource && typeof object !== 'object')) return object;
1141
+ if (typeof object.then === 'function') return object.then(transform);
1131
1142
  if (Array.isArray(object)) return object.map(transform);
1132
1143
  return selectFromObject(object, propertyResolvers, context)(select);
1133
1144
  };
@@ -1135,7 +1146,8 @@ export function transformForSelect(select, resource) {
1135
1146
  // if it is an array, return an array
1136
1147
  if (select.asArray)
1137
1148
  return function transform(object) {
1138
- if (object.then) return object.then(transform);
1149
+ if (object == null || (!resource && typeof object !== 'object')) return object;
1150
+ if (typeof object.then === 'function') return object.then(transform);
1139
1151
  if (Array.isArray(object)) return object.map(transform);
1140
1152
  const results = [];
1141
1153
  const getProperty = handleProperty(selectFromObject(object, propertyResolvers, context));
@@ -1145,18 +1157,27 @@ export function transformForSelect(select, resource) {
1145
1157
  return results;
1146
1158
  };
1147
1159
  const forceNulls = select.forceNulls;
1160
+ const selectsPrototype = select.some((property) => (property.name || property) === '__proto__');
1148
1161
  return function transform(object) {
1149
- if (object.then) return object.then(transform);
1162
+ if (object == null || (!resource && typeof object !== 'object')) return object;
1163
+ if (typeof object.then === 'function') return object.then(transform);
1150
1164
  if (Array.isArray(object))
1151
1165
  return object.map((value) => (value && typeof value === 'object' ? transform(value) : value));
1152
1166
  // finally the case of returning objects
1153
1167
  const selectedData = {};
1154
1168
  const getProperty = handleProperty(selectFromObject(object, propertyResolvers, context));
1169
+ if (selectsPrototype)
1170
+ Object.defineProperty(selectedData, '__proto__', {
1171
+ value: undefined,
1172
+ writable: true,
1173
+ enumerable: true,
1174
+ configurable: true,
1175
+ });
1155
1176
  let promises;
1156
1177
  for (const property of select) {
1157
1178
  let value = getProperty(property);
1158
1179
  if (value === undefined && forceNulls) value = null;
1159
- if (value?.then) {
1180
+ if (typeof value?.then === 'function') {
1160
1181
  if (!promises) promises = [];
1161
1182
  promises.push(value.then((value) => (selectedData[property.name || property] = value)));
1162
1183
  } else selectedData[property.name || property] = value;
@@ -1172,14 +1193,18 @@ export function transformForSelect(select, resource) {
1172
1193
  } else if (typeof property === 'object') {
1173
1194
  // TODO: Handle aggregate functions
1174
1195
  if (property.name) {
1175
- if (!subTransforms) subTransforms = {};
1196
+ const value = getProperty(property.name);
1197
+ if (!value || typeof value !== 'object' || value instanceof Date) return value;
1198
+ if (!subTransforms) subTransforms = Object.create(null);
1176
1199
  // TODO: Get the resource, cache this transform, and apply above
1177
1200
  let transform = subTransforms[property.name];
1178
1201
  if (!transform) {
1179
- const resource = propertyResolvers[property.name]?.definition?.tableClass;
1202
+ const resource =
1203
+ propertyResolvers && Object.hasOwn(propertyResolvers, property.name)
1204
+ ? propertyResolvers[property.name]?.definition?.tableClass
1205
+ : undefined;
1180
1206
  transform = subTransforms[property.name] = transformForSelect(property.select || property, resource);
1181
1207
  }
1182
- const value = getProperty(property.name);
1183
1208
  return transform(value);
1184
1209
  } else return getProperty(property);
1185
1210
  } else return property;
@@ -167,6 +167,7 @@ export class RocksTransactionLogStore extends EventEmitter {
167
167
  };
168
168
  }
169
169
  log.addEntry(entryBinary, options.transaction.id);
170
+ options.transaction.hasStagedLog = true;
170
171
  }
171
172
 
172
173
  /** A log holds one origin, which keeps `txnLogKey` unique within it; see resources/DESIGN.md. */
@@ -4331,7 +4331,9 @@ export function makeTable(options): TableResourceClass {
4331
4331
  return this;
4332
4332
  }
4333
4333
  /**
4334
- * Release the lock this instance holds. Resolves true when this call cleared the native key lock.
4334
+ * Release the lock this instance holds. Resolves true when this call cleared the native key lock;
4335
+ * false when it was already released, or when a commit of this lock's writes is still in flight
4336
+ * and the native key is returned once that commit settles.
4335
4337
  * Works for both held (`{ hold: true }`) and transaction-scoped locks. After unlock() the
4336
4338
  * instance is no longer lock-writable; writes through it require a fresh lock.
4337
4339
  */
@@ -4639,7 +4641,13 @@ export function makeTable(options): TableResourceClass {
4639
4641
  }
4640
4642
  },
4641
4643
  before: writeToSource(),
4642
- commit: (txnTime: number, existingEntry: Entry, retry: boolean, transaction: any) => {
4644
+ commit: (
4645
+ txnTime: number,
4646
+ existingEntry: Entry,
4647
+ retry: boolean,
4648
+ transaction: any,
4649
+ committedBy?: DatabaseTransaction
4650
+ ) => {
4643
4651
  // Whether a prior attempt of THIS write appended its own audit entry (sticky, set in
4644
4652
  // save(); log entries are not part of the aborted rocks transaction, so they survive).
4645
4653
  // Only such a write can find its own orphaned entry in the dedup lookups below and must
@@ -4730,6 +4738,19 @@ export function makeTable(options): TableResourceClass {
4730
4738
  let belowAuditFloor = false;
4731
4739
  let dedupVersionCouldBeRetained: (version: number) => boolean;
4732
4740
  if (audit) {
4741
+ if (
4742
+ stagedOwnAuditEntry &&
4743
+ committedBy?.sourceApply &&
4744
+ isRocksDB &&
4745
+ !replaying &&
4746
+ precedesExisting === 0 &&
4747
+ txnLogKey === txnTime &&
4748
+ (existingEntry.nodeId ?? 0) === (options?.nodeId ?? getThisNodeId(auditStore)) &&
4749
+ !existingEntry.additionalAuditRefs?.length
4750
+ ) {
4751
+ write.skipped = true;
4752
+ return;
4753
+ }
4733
4754
  // A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
4734
4755
  // its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
4735
4756
  // out-of-order versions, read with read-your-writes consistency, so this skips the duplicate up
@@ -7932,6 +7953,7 @@ export function makeTable(options): TableResourceClass {
7932
7953
  },
7933
7954
  };
7934
7955
  propertyResolvers.$highlights.directReturn = true;
7956
+ primaryKeyAttribute = undefined;
7935
7957
  for (const attribute of this.attributes) {
7936
7958
  if (attribute.isPrimaryKey) primaryKeyAttribute = attribute;
7937
7959
  attribute.resolve = null; // reset this
@@ -36,3 +36,20 @@ Still open: `findLastAggregationTime` seeds both markers after a restart from th
36
36
  analytics record, whose `time` is an end-of-cycle stamp, so a restart mid-drain can skip the rest of
37
37
  the backlog; and `storeMetric` discards `table.put()`'s result, so the cursor advances past raw
38
38
  records whose aggregate rows failed to commit. Both need a cursor persisted with the rows.
39
+
40
+ ## Aggregation folds each measure over only the samples that carry it (`resources/analytics/write.ts`)
41
+
42
+ A key's raw samples need not carry the same measures: callback metrics (`recordAction(fn)`) and
43
+ analytics listeners report whatever fields they have, and a raw backlog can come from another
44
+ version. `aggregation()` keeps, per fold target (an action, or one thread's record for `byThread`
45
+ metrics), the count of samples that carried each measure in the cycle-local `measureCounts` map —
46
+ not on the action, which `storeMetric` persists and aggregate listeners receive. A measure's first
47
+ sample seeds it, `max[A-Z]`-named measures fold with `Math.max`, and the rest are a running mean
48
+ weighted by that count. A measure in every sample has a count equal to the target's `count`, so it
49
+ folds exactly as before; keep the running mean, since sum-then-divide rounds differently.
50
+
51
+ The `byThread` combination sums every thread record's numeric fields into the entry, so a measure
52
+ the key's first sample lacked, or that only some threads carry, is kept. That holds only while a
53
+ thread record carries nothing but measures and its `count`, which the thread count then replaces:
54
+ the fold keeps `total`/`ratio` on non-thread actions only, so a `ratio` on a thread record is
55
+ always a caller's measure.
@@ -967,6 +967,8 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
967
967
  const aggregateActions = new Map();
968
968
  const distributions = new Map();
969
969
  const threadsToAverage = [];
970
+ // A measure's weight is the count of only the samples that carried it, per action or thread record.
971
+ const measureCounts = new Map<object, Record<string, number>>();
970
972
  let lastTime: number | undefined;
971
973
  let stoppedAtWindowEdge = false;
972
974
  for (const { key, value } of rawAnalyticsTable.primaryStore.getRange({
@@ -992,7 +994,8 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
992
994
  if (type !== undefined) key += '-' + type;
993
995
  let action = aggregateActions.get(key);
994
996
  if (action) {
995
- if (action.threads) {
997
+ const byThread = Boolean(action.byThread);
998
+ if (byThread) {
996
999
  const actionForThread = action.threads[threadId];
997
1000
  if (actionForThread) action = actionForThread;
998
1001
  else {
@@ -1001,17 +1004,27 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
1001
1004
  }
1002
1005
  }
1003
1006
  if (!action.count) action.count = 1;
1004
- const previousCount = action.count;
1007
+ let counts = measureCounts.get(action);
1008
+ if (!counts) {
1009
+ // Until its first fold, a target holds only its first sample.
1010
+ counts = Object.create(null);
1011
+ for (const measureName in action) {
1012
+ if (typeof action[measureName] === 'number') counts[measureName] = action.count;
1013
+ }
1014
+ measureCounts.set(action, counts);
1015
+ }
1005
1016
  for (const measureName in measures) {
1006
1017
  const value = measures[measureName];
1007
- if (typeof value === 'number') {
1008
- action[measureName] = MAX_MEASURE_NAME.test(measureName)
1009
- ? Math.max(action[measureName], value)
1010
- : (action[measureName] * previousCount + value * count) / (previousCount + count);
1011
- }
1018
+ if (typeof value !== 'number') continue;
1019
+ const measureCount = counts[measureName];
1020
+ if (measureCount === undefined) action[measureName] = value;
1021
+ else if (MAX_MEASURE_NAME.test(measureName)) action[measureName] = Math.max(action[measureName], value);
1022
+ else action[measureName] = (action[measureName] * measureCount + value * count) / (measureCount + count);
1023
+ counts[measureName] = (measureCount ?? 0) + count;
1012
1024
  }
1013
1025
  action.count += count;
1014
- if (total >= 0) {
1026
+ // A thread's record is built from measures alone, so it has no total to add to.
1027
+ if (total >= 0 && !byThread) {
1015
1028
  action.total += total;
1016
1029
  action.ratio = action.total / action.count;
1017
1030
  }
@@ -1043,15 +1056,15 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
1043
1056
  let { path, method, type, metric, count, total, distribution, threads, ...measures } = entry;
1044
1057
  threads = threads.filter((thread) => thread);
1045
1058
  for (const measureName in measures) {
1046
- if (typeof entry[measureName] !== 'number') continue;
1047
- let total = 0;
1048
- for (const thread of threads) {
1059
+ if (typeof entry[measureName] === 'number') entry[measureName] = 0;
1060
+ }
1061
+ // A thread can carry measures the entry's first sample lacked.
1062
+ for (const thread of threads) {
1063
+ for (const measureName in thread) {
1049
1064
  const value = thread[measureName];
1050
- if (typeof value === 'number') {
1051
- total += value;
1052
- }
1065
+ if (typeof value === 'number')
1066
+ entry[measureName] = (typeof entry[measureName] === 'number' ? entry[measureName] : 0) + value;
1053
1067
  }
1054
- entry[measureName] = total;
1055
1068
  }
1056
1069
  entry.count = threads.length;
1057
1070
  delete entry.threads;
@@ -654,6 +654,21 @@ const DROPPED_ROW_PREFIX = '/dropped/';
654
654
  function droppedRowKey(tableName: string): string {
655
655
  return DROPPED_ROW_PREFIX + tableName;
656
656
  }
657
+ /**
658
+ * Whether a catalog row names its table's primary store: one flagged `isPrimaryKey`, or the nameless bare
659
+ * `<table>/` row of a keyless table, which can lose the flag while its store still holds primary records.
660
+ */
661
+ export function isPrimaryCatalogRow(key: unknown, value: any): boolean {
662
+ if (value?.isPrimaryKey) return true;
663
+ // exactly one slash, at the end: initStores reads `<table>/<attribute>` by splitting on the first slash
664
+ return (
665
+ value != null &&
666
+ value.name == null &&
667
+ typeof key === 'string' &&
668
+ key.length > 1 &&
669
+ key.indexOf('/') === key.length - 1
670
+ );
671
+ }
657
672
  /**
658
673
  * Once a name has been dropped, later creates of it take a generation even after the retirement journal is
659
674
  * reclaimed. Keep the row untimed and shaped `{ table, tableId }`: later versions read it as an untimed drop
@@ -1325,7 +1340,7 @@ function initStores(
1325
1340
  definedTables?.add(tableName);
1326
1341
  let tableDef = tablesToLoad.get(tableName);
1327
1342
  if (!tableDef) tablesToLoad.set(tableName, (tableDef = { attributes: [] }));
1328
- if (attribute_name == null || value.isPrimaryKey) tableDef.primary = value;
1343
+ if (isPrimaryCatalogRow(key, value)) tableDef.primary = value;
1329
1344
  if (value.dropping) tableDef.tombstone = value;
1330
1345
  if (attribute_name != null) tableDef.attributes.push(value);
1331
1346
  Object.defineProperty(value, 'key', { value: key, configurable: true });
@@ -1505,7 +1520,8 @@ function initStores(
1505
1520
  (attributesDbi as any).putSync(NEXT_TABLE_ID, tableId + 1);
1506
1521
  (attributesDbi as any).putSync(primaryAttribute.key, primaryAttribute);
1507
1522
  }
1508
- const dbiInit = createOpenDBIObject(!primaryAttribute.isPrimaryKey, primaryAttribute.isPrimaryKey);
1523
+ // primary by selection, not by its flag (isPrimaryCatalogRow)
1524
+ const dbiInit = createOpenDBIObject(false, true);
1509
1525
  dbiInit.compression = primaryAttribute.compression;
1510
1526
  if (dbiInit.compression) {
1511
1527
  const compressionThreshold =
@@ -3671,6 +3687,25 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
3671
3687
  if (Table.primaryStore.rootStore.status === 'closed') {
3672
3688
  throw new Error(`Can not use a closed data store from ${tableName} class`);
3673
3689
  }
3690
+ if (Table.primaryKey == null) {
3691
+ // a keyless table cannot adopt a key without re-keying its records
3692
+ for (const attribute of attributes) {
3693
+ if (!attribute.isPrimaryKey) continue;
3694
+ if (origin === 'cluster')
3695
+ logger.warn(
3696
+ `Ignoring peer redefinition of ${databaseName}.${tableName}.${attribute.name} ` +
3697
+ `(isPrimaryKey: local table has no primary key, peer true)`
3698
+ );
3699
+ else if (schemaDefinedExplicit)
3700
+ logger.error(
3701
+ `Table '${databaseName}.${tableName}' has no primary key, so its declared primary key ` +
3702
+ `'${attribute.name}' is ignored and the table stays keyless. Recreate the table with that ` +
3703
+ `primary key, or migrate its records, to use it.`
3704
+ );
3705
+ delete attribute.isPrimaryKey;
3706
+ if (properties?.[attribute.name]) delete properties[attribute.name].primaryKey;
3707
+ }
3708
+ }
3674
3709
  // Reject moving the primary key to a different attribute on a table that already has records.
3675
3710
  // The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
3676
3711
  // leave describe reporting the new attribute while every record — old and newly inserted — stays
@@ -5736,7 +5771,8 @@ function completeInterruptedDrop(
5736
5771
  }>;
5737
5772
  const bareEntry = catalogRows.find(({ key }) => key === tableName + '/');
5738
5773
  const primaryEntry =
5739
- (bareEntry?.value?.isPrimaryKey ? bareEntry : undefined) ?? catalogRows.find(({ value }) => value?.isPrimaryKey);
5774
+ catalogRows.find(({ value }) => value?.isPrimaryKey) ??
5775
+ (bareEntry && isPrimaryCatalogRow(bareEntry.key, bareEntry.value) ? bareEntry : undefined);
5740
5776
  const tombstoneEntry = bareEntry ?? primaryEntry;
5741
5777
  if (rootStore instanceof RocksDatabase && !fullTextRetired) {
5742
5778
  const names = [
@@ -5795,7 +5831,7 @@ function completeInterruptedDrop(
5795
5831
  // recreate silently inherit the previous table's records.
5796
5832
  for (const { key, value } of catalogRows) {
5797
5833
  const objectStorage =
5798
- value?.isPrimaryKey || (value?.indexed?.type && CUSTOM_INDEXES[value.indexed.type]?.useObjectStore);
5834
+ isPrimaryCatalogRow(key, value) || (value?.indexed?.type && CUSTOM_INDEXES[value.indexed.type]?.useObjectStore);
5799
5835
  const store = (rootStore as any).openDB(key, createOpenDBIObject(!objectStorage, objectStorage) as any);
5800
5836
  try {
5801
5837
  // dropSync (not drop): this function is synchronous, and its callers rely on
@@ -37,14 +37,24 @@ Consequences that shape the code:
37
37
  no-op. The key is `lockAttemptKey(tableId, id)` = `[LOCK_KEY_PREFIX, tableId, ...id]`, distinct from
38
38
  `getFromSource`'s bare-id single-flight lock.
39
39
  - **`RecordLockHandle`** (`recordLock.ts`) carries `store`, `key`, `keyId`, `acquiredAt`, `expiresAt`,
40
- `hold`, `released`, and `expired`. `release()` is synchronous: it sets `released`, clears the lease
41
- timer, and calls `store.unlock(key)`. A lease timer sets `expired = true` then calls `store.unlock()`
42
- on fire; any write staged through an expired or released handle throws 409 in
43
- `DatabaseTransaction.save()` before the write reaches the store — from `lockNotHeldError()`, which
44
- names the actual cause, since an expired lease and a handle already released (by `unlock()` or by
45
- the commit) send a caller after different bugs. `acquireRecordKey` loops
46
- `tryLock` → await wake → retry until acquired or `waitMs` elapsed (then 423). The contender wait
47
- timer uses `.unref()` so it does not prevent process exit.
40
+ `hold`, `released`, and `expired`. `release()` sets `released` and clears the lease timer; a lease
41
+ timer sets `expired = true` and `released = true` on fire. When no native commit is holding the
42
+ handle, both immediately call `store.unlock(key)` and emit `onRelease` (and `release()` returns `true`).
43
+ While a submitted native `RocksTransaction` is in flight (`retainForCommit(holdDeadlineMono)` called at
44
+ first native submission in `DatabaseTransaction.performCommit()`), `store.unlock(key)` and `onRelease`
45
+ are deferred (and `release()` returns `false`) until `releaseCommitHold()` releases the last commit
46
+ hold — at local commit landing (immediately before `releaseRecordLocks()`), in `commitOutcome`'s
47
+ rejection handler on any unlogged give-up, or at the commit-conflict budget boundary via
48
+ `noteOverdueLoggedCommit` or the handle's own hold-deadline timer when a native commit never settles
49
+ (harper#3236) — so a logged commit that retries past its lease cannot be overtaken by a same-node
50
+ contender or emit an early `LOCK_RELEASE` while within its conflict budget. `isExpired()` flips
51
+ immediately when the timer fires or `revokeLease()`/`release()` runs (while `isLeaseExpired()` reflects
52
+ only lease expiry or `revokeLease()`, not `release()`), so any new write staged through an expired or
53
+ released handle (and any unlogged conflict retry) throws 409 in `DatabaseTransaction.save()` or
54
+ `performCommit()` — from `lockNotHeldError()`, which names the actual cause, since an expired lease and
55
+ a handle already released (by `unlock()` or by the commit) send a caller after different bugs.
56
+ `acquireRecordKey` loops `tryLock` → await wake → retry until acquired or `waitMs` elapsed (then 423).
57
+ The contender wait timer uses `.unref()` so it does not prevent process exit.
48
58
  - **Re-entrancy is per-transaction.** `DatabaseTransaction.recordLocks` is a lazily allocated
49
59
  `Map<store, Map<keyId, handle>>` (O(1) lookup). `registerRecordLock`, `recordLockFor`, and
50
60
  `unregisterRecordLock` manage it. `lock()` consults it before calling `tryLock`; a re-entrant call
@@ -53,7 +63,8 @@ Consequences that shape the code:
53
63
  - **Release.** Every handle — scoped or hold — is registered in `link.recordLocks` and kept on the
54
64
  locked instance as `#lockHandle`; every commit or abort calls `releaseRecordLocks()`, which calls
55
65
  `handle.release()` on each non-hold handle, so a `{ hold: true }` handle outlives the transaction.
56
- `unlock()` calls `handle.release()` directly (synchronous, returns false if already released).
66
+ `unlock()` calls `handle.release()` directly (synchronous; returns false if already released, or
67
+ while a commit hold defers the native unlock until the in-flight commit settles).
57
68
  `unlock()` also accepts scoped handles: it calls `release()` early and unregisters the handle so the transaction commit does
58
69
  not release it again. After any `unlock()` call `#lockWritable` is cleared so writes through the
59
70
  instance are no longer accepted. In an explicit transaction with no staged writes and no open
@@ -1066,9 +1077,12 @@ feature that has not yet been measured.
1066
1077
  >
1067
1078
  > **§2's exclusion invariant is wider than what (1) and (2) leave.** It requires a successor to
1068
1079
  > exclude every predecessor capability that can still admit _or commit_; what ships excludes
1069
- > admission, and the commit half holds only up to the pre-submission expiry fence
1070
- > (the `hasLeaseProtectedWrite` fence in `DatabaseTransaction.performCommit()`) — a native commit that clears that fence and settles
1071
- > afterwards is limitation (2)'s third route.
1080
+ > admission, and the commit half holds up to the pre-submission expiry fence
1081
+ > (the `hasLeaseProtectedWrite` fence in `DatabaseTransaction.performCommit()`) plus, on the local
1082
+ > node and for clean recall drain, `KeyLockHandle`'s per-handle commit hold (`retainForCommit()` /
1083
+ > `releaseCommitHold()`, harper#3236) up to `commitConflictBudget()` — a native commit that clears the
1084
+ > pre-submission fence and settles past `commitConflictBudget()` (or after the home's own monotonic
1085
+ > delegation lease expires on a peer) is limitation (2)'s third route.
1072
1086
  >
1073
1087
  > **There is no caller-side mitigation for (2) — the obvious candidate is unreachable and, where it
1074
1088
  > is reachable, makes things worse.** `X-Replicate-To` / `confirm=` is meant to be super-user only —
@@ -1306,11 +1320,14 @@ drain and caps, §§5/6/8, plus the three inherited substrate defects in §11) a
1306
1320
  machine is gone, and the home ring, the delegation table, recall with §6 steps 1, 2 and 4, ordered
1307
1321
  fencing tokens, the caps, the §4.3 restart quarantine, the generation monotonicity floor and the
1308
1322
  transport-swap grant fence are in place, with all three inherited defects fixed. **§6 step 3 —
1309
- settlement — is not implemented**: a recall revokes capability and then writes the release without
1310
- waiting for a native commit already submitted to complete. That is exactly limitation (2)'s third
1311
- route in §10, which the contract already states, so the gap is disclosed rather than hidden — but §6
1312
- must not be read as fully implemented. Closing it means hanging the release hook off
1313
- logical-transaction settlement rather than off the last admission unlocking. harper-pro#822 supplies
1323
+ settlement — is bounded by the commit-conflict budget (`retainForCommit(holdDeadlineMono)` /
1324
+ `releaseCommitHold()`, harper#3236)**: a submitted native `RocksTransaction` retains a commit hold
1325
+ on each write's `KeyLockHandle` until local commit landing (or `commitOutcome` rejection), so
1326
+ `revokeLease()` defers `store.unlock(key)` and `onRelease` (`LOCK_RELEASE` emission) while the
1327
+ commit is within `commitConflictBudget()`. Past `commitConflictBudget()`, `KeyLockHandle`'s
1328
+ `#holdTimer` (as well as `noteOverdueLoggedCommit` between retry attempts) releases the hold so a
1329
+ wedged conflict cannot pin the key forever, and a peer's own monotonic delegation lease (§5.2) can
1330
+ still expire first — which is limitation (2)'s third route in §10. harper-pro#822 supplies
1314
1331
  the operator-agreed home map and `establishLockFreshness()`; the feature is opt-in
1315
1332
  (`replication.recordLocks: true`). A `lockRelease` that reaches a non-coordinating thread is handed
1316
1333
  to the transport's `relayControlEntry` (harper-pro#977). Without the hook, a thread that has a
@@ -52,10 +52,11 @@ export interface ResolvedRecordLockOptions {
52
52
 
53
53
  /**
54
54
  * The handle returned by `lock()` and held by each gated write until its transaction completes.
55
- * `release()` is synchronous: it calls `store.unlock(key)` which is ownerless and fires any
56
- * waiting callbacks cross-thread. The `expired` flag is set in the holder thread by the lease
57
- * timer, so a stale holder cannot accidentally call unlock after its lease already fired and
58
- * released the key to a new holder.
55
+ * `release()` is synchronous: it calls `store.unlock(key)` (or defers it while a commit hold
56
+ * acquired via `retainForCommit` is still active), which is ownerless and fires any waiting
57
+ * callbacks cross-thread. The lease timer sets `expired` in the holder thread when it fires
58
+ * (while a commit hold is active, only the native unlock is deferred), so a stale holder cannot
59
+ * accidentally call unlock after its lease already fired and released the key to a new holder.
59
60
  */
60
61
  export interface RecordLockHandle {
61
62
  store: any;
@@ -107,7 +108,11 @@ export interface RecordLockHandle {
107
108
  * version has been noted; otherwise advances by the minimum monotonic step from the recorded floor.
108
109
  */
109
110
  nextHolderVersion(): number;
111
+ /** True when this call returned the native key; false if already released, or while a commit hold defers the unlock. */
110
112
  release(): boolean;
113
+ /** Defer `store.unlock(key)` until an in-flight native commit settles or `holdDeadlineMono` expires. */
114
+ retainForCommit?(holdDeadlineMono?: number): void;
115
+ releaseCommitHold?(): void;
111
116
  /**
112
117
  * Flip this handle to hold mode and reset its lease timer. Used during scoped→hold upgrades
113
118
  * so all instances that reference this handle object remain valid (no retire + replace).
@@ -192,6 +197,10 @@ class KeyLockHandle implements RecordLockHandle {
192
197
  // an NTP step nor a manual clock change can extend a lease past what peers assumed.
193
198
  #deadlineMono: number | undefined;
194
199
  #onRelease: (() => void) | undefined;
200
+ #commitHolds = 0;
201
+ #deferredUnlock = false;
202
+ #holdDeadlineMono: number | undefined;
203
+ #holdTimer: ReturnType<typeof setTimeout> | undefined;
195
204
 
196
205
  constructor(store: any, key: any[], keyId: unknown, lease?: number, hold = false, acquiredAt?: number) {
197
206
  this.store = store;
@@ -238,8 +247,9 @@ class KeyLockHandle implements RecordLockHandle {
238
247
  // bound and the key may belong to someone else.
239
248
  #leaseLapsed(): boolean {
240
249
  if (this.#deadlineMono === undefined || performance.now() < this.#deadlineMono) return false;
241
- // Finish the expiry rather than only reporting it: the native key must actually go back so a
242
- // stalled holder does not keep a key every participant has already written off.
250
+ // Finish the expiry rather than only reporting it: once no in-flight commit holds the handle,
251
+ // the native key must go back so a stalled holder does not keep a key every participant has
252
+ // already written off.
243
253
  if (!this.released) this.#onLeaseExpire();
244
254
  return true;
245
255
  }
@@ -259,8 +269,8 @@ class KeyLockHandle implements RecordLockHandle {
259
269
  // no-op in the one state it was written for.
260
270
  this.expired = true;
261
271
  if (this.released) {
262
- // The native key is already back and `onRelease` already fired; only the fence flag and the
263
- // lease timer are left to settle.
272
+ // `release()` already ran (returning the native key or deferring the unlock onto an active
273
+ // commit hold); only the fence flag and the lease timer are left to settle.
264
274
  clearTimeout(this.#timer);
265
275
  return;
266
276
  }
@@ -294,22 +304,84 @@ class KeyLockHandle implements RecordLockHandle {
294
304
  return next;
295
305
  }
296
306
 
297
- #onLeaseExpire() {
298
- if (this.released) return;
299
- this.released = true;
300
- this.expired = true;
301
- clearTimeout(this.#timer);
307
+ retainForCommit(holdDeadlineMono?: number): void {
308
+ if (this.released && !this.#deferredUnlock) return;
309
+ this.#commitHolds++;
310
+ if (holdDeadlineMono !== undefined && Number.isFinite(holdDeadlineMono)) {
311
+ this.#holdDeadlineMono = Math.max(this.#holdDeadlineMono ?? holdDeadlineMono, holdDeadlineMono);
312
+ if (this.#deferredUnlock) this.#armHoldTimer();
313
+ }
314
+ }
315
+
316
+ releaseCommitHold(): void {
317
+ if (this.#commitHolds <= 0) return;
318
+ if (--this.#commitHolds === 0) {
319
+ clearTimeout(this.#holdTimer);
320
+ this.#holdTimer = undefined;
321
+ this.#holdDeadlineMono = undefined;
322
+ if (this.#deferredUnlock) {
323
+ this.#deferredUnlock = false;
324
+ this.#unlockAndEmitRelease('record lock deferred release failed (store may have been dropped)');
325
+ }
326
+ }
327
+ }
328
+
329
+ // True when the native key went back now; false when the unlock waits on the commit hold.
330
+ #deferOrFlushUnlock(warningMessage: string): boolean {
331
+ if (this.#holdDeadlineMono !== undefined && performance.now() >= this.#holdDeadlineMono) {
332
+ this.#commitHolds = 0;
333
+ this.#deferredUnlock = false;
334
+ this.#holdDeadlineMono = undefined;
335
+ clearTimeout(this.#holdTimer);
336
+ this.#holdTimer = undefined;
337
+ this.#unlockAndEmitRelease(warningMessage);
338
+ return true;
339
+ }
340
+ this.#deferredUnlock = true;
341
+ this.#armHoldTimer();
342
+ return false;
343
+ }
344
+
345
+ #armHoldTimer() {
346
+ clearTimeout(this.#holdTimer);
347
+ this.#holdTimer = undefined;
348
+ if (!this.#deferredUnlock || this.#holdDeadlineMono === undefined) return;
349
+ const remaining = Math.max(0, Math.ceil(this.#holdDeadlineMono - performance.now()));
350
+ this.#holdTimer = setTimeout(() => {
351
+ this.#holdTimer = undefined;
352
+ this.#commitHolds = 0;
353
+ this.#holdDeadlineMono = undefined;
354
+ if (this.#deferredUnlock) {
355
+ this.#deferredUnlock = false;
356
+ this.#unlockAndEmitRelease('record lock commit hold timer failed (store may have been dropped)');
357
+ }
358
+ }, remaining).unref();
359
+ }
360
+
361
+ #unlockAndEmitRelease(warningMessage: string) {
302
362
  try {
303
363
  this.store.unlock(this.key);
304
364
  } catch (err) {
305
365
  if (!warnedLeaseTimerStores.has(this.store)) {
306
366
  warnedLeaseTimerStores.add(this.store);
307
- harperLogger.warn?.('record lock lease timer failed (store may have been dropped)', err);
367
+ harperLogger.warn?.(warningMessage, err);
308
368
  }
309
369
  }
310
370
  this.#emitRelease();
311
371
  }
312
372
 
373
+ #onLeaseExpire() {
374
+ if (this.released) return;
375
+ this.released = true;
376
+ this.expired = true;
377
+ clearTimeout(this.#timer);
378
+ if (this.#commitHolds > 0) {
379
+ this.#deferOrFlushUnlock('record lock lease timer failed (store may have been dropped)');
380
+ return;
381
+ }
382
+ this.#unlockAndEmitRelease('record lock lease timer failed (store may have been dropped)');
383
+ }
384
+
313
385
  // The cluster release is a durability optimization, not the safety mechanism — peers expire the
314
386
  // hold on their own lease bound — so neither a throw nor a rejected promise may escape here. This
315
387
  // runs from a lease timer and from post-commit cleanup, where an escaping rejection would take
@@ -331,15 +403,10 @@ class KeyLockHandle implements RecordLockHandle {
331
403
  if (this.released) return false;
332
404
  this.released = true;
333
405
  clearTimeout(this.#timer);
334
- try {
335
- this.store.unlock(this.key);
336
- } catch (err) {
337
- if (!warnedLeaseTimerStores.has(this.store)) {
338
- warnedLeaseTimerStores.add(this.store);
339
- harperLogger.warn?.('record lock release failed (store may have been dropped)', err);
340
- }
406
+ if (this.#commitHolds > 0) {
407
+ return this.#deferOrFlushUnlock('record lock release failed (store may have been dropped)');
341
408
  }
342
- this.#emitRelease();
409
+ this.#unlockAndEmitRelease('record lock release failed (store may have been dropped)');
343
410
  return true;
344
411
  }
345
412