@harperfast/harper 5.2.12 → 5.2.14

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 (269) hide show
  1. package/components/Application.ts +7 -7
  2. package/components/componentLoader.ts +70 -33
  3. package/dist/components/Application.js +1 -7
  4. package/dist/components/Application.js.map +1 -1
  5. package/dist/components/componentLoader.d.ts +6 -0
  6. package/dist/components/componentLoader.js +60 -31
  7. package/dist/components/componentLoader.js.map +1 -1
  8. package/dist/resources/RocksTransactionLogStore.d.ts +4 -2
  9. package/dist/resources/RocksTransactionLogStore.js +12 -6
  10. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  11. package/dist/resources/Table.d.ts +2 -2
  12. package/dist/resources/Table.js +222 -65
  13. package/dist/resources/Table.js.map +1 -1
  14. package/dist/resources/replayLogs.js +25 -21
  15. package/dist/resources/replayLogs.js.map +1 -1
  16. package/dist/resources/transactionBroadcast.js +4 -6
  17. package/dist/resources/transactionBroadcast.js.map +1 -1
  18. package/dist/security/certificateVerification/index.js +40 -11
  19. package/dist/security/certificateVerification/index.js.map +1 -1
  20. package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
  21. package/dist/security/certificateVerification/trustedIssuers.js +79 -0
  22. package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
  23. package/dist/security/certificateVerification/types.d.ts +1 -0
  24. package/dist/security/keys.js +4 -0
  25. package/dist/security/keys.js.map +1 -1
  26. package/dist/server/loadRootComponents.js +4 -2
  27. package/dist/server/loadRootComponents.js.map +1 -1
  28. package/dist/server/threads/manageThreads.d.ts +1 -0
  29. package/dist/server/threads/manageThreads.js +175 -24
  30. package/dist/server/threads/manageThreads.js.map +1 -1
  31. package/dist/utility/errors/hdbError.d.ts +8 -0
  32. package/dist/utility/errors/hdbError.js +29 -0
  33. package/dist/utility/errors/hdbError.js.map +1 -1
  34. package/npm-shrinkwrap.json +2 -2
  35. package/package.json +1 -1
  36. package/resources/DESIGN.md +5 -0
  37. package/resources/RocksTransactionLogStore.ts +12 -6
  38. package/resources/Table.ts +216 -66
  39. package/resources/replayLogs.ts +25 -22
  40. package/resources/transactionBroadcast.ts +4 -4
  41. package/security/certificateVerification/index.ts +54 -13
  42. package/security/certificateVerification/trustedIssuers.ts +76 -0
  43. package/security/certificateVerification/types.ts +1 -0
  44. package/security/keys.ts +3 -0
  45. package/server/DESIGN.md +14 -13
  46. package/server/loadRootComponents.js +9 -2
  47. package/server/threads/manageThreads.js +176 -23
  48. package/studio/web/404.html +79 -0
  49. package/studio/web/assets/{Chat-CokJOjyX.js → Chat-C97PMlIn.js} +274 -247
  50. package/studio/web/assets/FloatingChat-BUlsFGMV.js +23 -0
  51. package/studio/web/assets/_basePickBy-QJXUw_pr.js +1 -0
  52. package/studio/web/assets/_baseUniq-DR4vVjyC.js +1 -0
  53. package/studio/web/assets/{abnfDiagram-VCTEODGH-B0BebmD2.js → abnfDiagram-O67JEVCF-DpPZWMOf.js} +1 -1
  54. package/studio/web/assets/{alertDialog-CQyAJJhl.js → alertDialog-UrbiMgQt.js} +1 -1
  55. package/studio/web/assets/{apiToken-B_g12QrM.js → apiToken-CPhbc_zN.js} +1 -1
  56. package/studio/web/assets/applications-D3Z6BVDl.js +296 -0
  57. package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +1 -0
  58. package/studio/web/assets/architectureDiagram-NJMV4G6O-OmgLraID.js +36 -0
  59. package/studio/web/assets/authStore-5jSS5pTW.js +3 -0
  60. package/studio/web/assets/{blockDiagram-I7D4REHJ-BqguiadH.js → blockDiagram-BEXU5L5S-Cv7CD3Ai.js} +15 -4
  61. package/studio/web/assets/{button-BIsUKRZq.js → button-MFP9n8Ck.js} +1 -1
  62. package/studio/web/assets/c4Diagram-YGBWAQC7-Ds1vKYnN.js +38 -0
  63. package/studio/web/assets/channel-BDtouPd6.js +1 -0
  64. package/studio/web/assets/chevron-up-CFe8x5Bp.js +1 -0
  65. package/studio/web/assets/chunk-3FUC2YCW-DpnBPH8B.js +2 -0
  66. package/studio/web/assets/{chunk-GVQU2GXP-sbwVIQ8i.js → chunk-5DYCD2WN-LLLCQSzg.js} +1 -1
  67. package/studio/web/assets/{chunk-PWAF6VOD-1z1THyS5.js → chunk-742MDFTN-BL8J_pIO.js} +1 -1
  68. package/studio/web/assets/chunk-7INBJB4K-C-hJHiiK.js +62 -0
  69. package/studio/web/assets/chunk-7M6MHVWA-C6ZqkFbA.js +213 -0
  70. package/studio/web/assets/chunk-7PRAP22T-CUXM8BG6.js +1 -0
  71. package/studio/web/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  72. package/studio/web/assets/{chunk-FOHPRMQF-DHwB1DNv.js → chunk-FOHPRMQF-DzxwRta7.js} +9 -9
  73. package/studio/web/assets/{chunk-SVP7TREG-jtdAHw0S.js → chunk-GTNCS2PH-DJ2BFvS1.js} +1 -1
  74. package/studio/web/assets/{chunk-F27PBJKO-BVA5EPhV.js → chunk-GWA4HPMP-C42bSqLL.js} +1 -1
  75. package/studio/web/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  76. package/studio/web/assets/chunk-MBY4JIJT-C-5jz6Lo.js +72 -0
  77. package/studio/web/assets/chunk-NETBCI7D-D-bzlN4r.js +1 -0
  78. package/studio/web/assets/chunk-O7XYJQB3-ep14zA23.js +128 -0
  79. package/studio/web/assets/chunk-UA2S7LBM-BMRWRHED.js +1 -0
  80. package/studio/web/assets/{chunk-POPQ4Y6H-ClWhhkwW.js → chunk-WEXAMYUT-DdLYVdX1.js} +1 -1
  81. package/studio/web/assets/{chunk-XXDRQBXY-BoGZu3HH.js → chunk-XXDRQBXY-CbPh2l8S.js} +1 -1
  82. package/studio/web/assets/{chunk-OSK3NFVY-ByciRftO.js → chunk-Z7XXMR3K-BmKEDjcP.js} +3 -3
  83. package/studio/web/assets/{chunk-75Z2AOVW-QGQD6th2.js → chunk-ZIGJFQKS-DLZgdNxA.js} +2 -2
  84. package/studio/web/assets/classDiagram-v2-NBCMYWYE-DOI5vgKT.js +217 -0
  85. package/studio/web/assets/cose-bilkent-JH36ORCC-BSOszL11.js +1 -0
  86. package/studio/web/assets/createLucideIcon-bziYxJli.js +1 -0
  87. package/studio/web/assets/{css.worker-BD9HbRGW.js → css.worker-l1psR_wG.js} +13 -10
  88. package/studio/web/assets/{cssMode-DAjzPoBf.js → cssMode-DyLZPMjo.js} +1 -1
  89. package/studio/web/assets/cynefin-OW5HDTMX-BSW-DB7S.js +1 -0
  90. package/studio/web/assets/cynefinDiagram-VND7K2PF-CbAHEwxt.js +62 -0
  91. package/studio/web/assets/cytoscape.esm-BZkf-uXn.js +321 -0
  92. package/studio/web/assets/dagre-6A5THRUB-Csxewgs4.js +4 -0
  93. package/studio/web/assets/diagram-22UHCM2B-a0rt8tY4.js +200 -0
  94. package/studio/web/assets/{diagram-VX7I27RA-DccVJet6.js → diagram-3UASUU5V-lEj2SVNh.js} +2 -2
  95. package/studio/web/assets/{diagram-VSXAHHWV-fhEdmkwM.js → diagram-ATOU4E4O-HcrAeB-q.js} +3 -3
  96. package/studio/web/assets/{diagram-S7CK7UJ4-aoCVTtcy.js → diagram-CDSNMT55-CYxiuyEx.js} +3 -3
  97. package/studio/web/assets/{diagram-Z3DM3KII-D-RyJJb7.js → diagram-MLGK6HIB-ChbCJTgJ.js} +1 -1
  98. package/studio/web/assets/diagram-MPIPVDR6-DaX9Y-N3.js +41 -0
  99. package/studio/web/assets/dialog-DTepachC.js +1 -0
  100. package/studio/web/assets/{dist-kYfyPZP9.js → dist-BRuBKNNy.js} +1 -1
  101. package/studio/web/assets/{dist-DP8UjMB_.js → dist-Cz2Y6-6G.js} +2 -2
  102. package/studio/web/assets/download-CtostjYn.js +1 -0
  103. package/studio/web/assets/{ebnfDiagram-PWID7BFC-DJGpIpz_.js → ebnfDiagram-ZINNZB2B-CwAsENB1.js} +1 -1
  104. package/studio/web/assets/{editor-VUNQ-5Ye.js → editor-CFQ5kbiT.js} +1 -1
  105. package/studio/web/assets/editor.worker-CkkvdXvO.js +33 -0
  106. package/studio/web/assets/elk-276RUBZZ-Hexg9E4D.js +27 -0
  107. package/studio/web/assets/{erDiagram-RLTQ6QDP-CIfNlgkC.js → erDiagram-OPXOYQCR-CeC-wwO1.js} +15 -15
  108. package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +1 -0
  109. package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +1 -0
  110. package/studio/web/assets/{ganttDiagram-EL5Y4UJY-BxToTzzD.js → ganttDiagram-FUAMR5RP-C2_Hhl1-.js} +2 -2
  111. package/studio/web/assets/{getAnalytics-GHK8ORfM.js → getAnalytics-CjthMQqF.js} +1 -1
  112. package/studio/web/assets/gitGraph-4MIJSDKK-xIk5Vuew.js +1 -0
  113. package/studio/web/assets/{gitGraphDiagram-WWUBYQGX-Dwntd4-x.js → gitGraphDiagram-X574FWY7-CMxJ34up.js} +4 -4
  114. package/studio/web/assets/graphlib-C9Tl7QrK.js +1 -0
  115. package/studio/web/assets/{html-CFqhtdfa.js → html-DdMBJEGu.js} +1 -1
  116. package/studio/web/assets/{html.worker-DsMmVtuG.js → html.worker-DdwM4IrA.js} +12 -9
  117. package/studio/web/assets/{htmlMode-CpgOTfjz.js → htmlMode-DNtBx3OV.js} +1 -1
  118. package/studio/web/assets/index-CPoDzQTe.css +1 -0
  119. package/studio/web/assets/index-zZCxAIEb.js +824 -0
  120. package/studio/web/assets/index.lazy-CKbsHbSN.js +2 -0
  121. package/studio/web/assets/info-A6RAGUB7-o49TEbj-.js +1 -0
  122. package/studio/web/assets/infoDiagram-VRGFBTTK-Cjhh7yzZ.js +2 -0
  123. package/studio/web/assets/{ishikawaDiagram-5VMMS53U-D9Xh2r6X.js → ishikawaDiagram-OU5B5YK6-C63aXRmh.js} +2 -2
  124. package/studio/web/assets/{javascript-9l5JXJwc.js → javascript-Dy6KbeHT.js} +1 -1
  125. package/studio/web/assets/{journeyDiagram-3NMN7TZE-CokIi6ll.js → journeyDiagram-ZHPQQLJL-D_o0P5Mu.js} +3 -3
  126. package/studio/web/assets/json.worker-CSi2hF-a.js +65 -0
  127. package/studio/web/assets/{jsonMode-BJBH-iR0.js → jsonMode-BiuDVrzl.js} +1 -1
  128. package/studio/web/assets/{kanban-definition-UXKFOSKX-CukSFJfX.js → kanban-definition-PNTS6WVX-CTI9SSxY.js} +7 -7
  129. package/studio/web/assets/{katex-CXMH3UgJ.js → katex-ZlcWpGUi.js} +2 -2
  130. package/studio/web/assets/languageServices-Dk8_6B25.js +1 -0
  131. package/studio/web/assets/linear-Ds1AeO4V.js +1 -0
  132. package/studio/web/assets/{lspLanguageFeatures-sNVDopx3.js → lspLanguageFeatures-DwJKXzQa.js} +2 -2
  133. package/studio/web/assets/{mermaid-parser.core-BlEsOWNO.js → mermaid-parser.core-C2lcH_jP.js} +3 -3
  134. package/studio/web/assets/mermaid.core-BG5bjfIG.js +44 -0
  135. package/studio/web/assets/{mindmap-definition-YA3MSWOX-IprMc_0j.js → mindmap-definition-NLK3R4M7-BK7ho6JD.js} +24 -24
  136. package/studio/web/assets/{notifications-BukjVuVe.js → notifications-DJqAtWxD.js} +1 -1
  137. package/studio/web/assets/{notifications-DvBJIIO0.js → notifications-DsDtb41c.js} +1 -1
  138. package/studio/web/assets/packet-AYTQ26CC-BHPy6KBV.js +1 -0
  139. package/studio/web/assets/{pegDiagram-XKGWAZYB-BNuPDLZY.js → pegDiagram-GJSIUBJH-DzjwG3jO.js} +1 -1
  140. package/studio/web/assets/pie-WAS4IAKB-D0cP-DTU.js +1 -0
  141. package/studio/web/assets/{pieDiagram-E7YTZNPT-DqNb6Ht2.js → pieDiagram-5QR66LMP-C_hOP9Lv.js} +2 -2
  142. package/studio/web/assets/profile-zTGUN51x.js +1 -0
  143. package/studio/web/assets/{quadrantDiagram-AXDQQJYC-BGH9E2YR.js → quadrantDiagram-O4NWA36T-CZW8PbV1.js} +3 -3
  144. package/studio/web/assets/radar-RG4KPBEZ-C8fk0U81.js +1 -0
  145. package/studio/web/assets/railroad-74A4TZTK-B60FWm17.js +1 -0
  146. package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +1 -0
  147. package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +1 -0
  148. package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +1 -0
  149. package/studio/web/assets/{railroadDiagram-O6MQD6OU-DGTPh2KZ.js → railroadDiagram-XR7U4H2S-DyyNmgbU.js} +1 -1
  150. package/studio/web/assets/regions-SOtABPB9.js +1 -0
  151. package/studio/web/assets/{register-CU-gCfLP.js → register-7JezVakc.js} +39 -39
  152. package/studio/web/assets/{requirementDiagram-BXWQKSXE-BJnO6uLz.js → requirementDiagram-PLB6GJNP-C4H-mLKu.js} +11 -11
  153. package/studio/web/assets/{rolldown-runtime-hePW80VL.js → rolldown-runtime-Dd_uD5pT.js} +1 -1
  154. package/studio/web/assets/sankeyDiagram-IPEJSGJF-DHDyga4c.js +40 -0
  155. package/studio/web/assets/sequenceDiagram-PO4LG4MO-CdNcHXRI.js +169 -0
  156. package/studio/web/assets/setComponentFile-BavlhfY0.js +1 -0
  157. package/studio/web/assets/{setup-DG3Ws0Gg.js → setup-04yVzr2I.js} +2 -2
  158. package/studio/web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  159. package/studio/web/assets/{src-CMc7m_Ut.js → src-D0vMJQRr.js} +1 -1
  160. package/studio/web/assets/stateDiagram-v2-GCMORJYK-CpKuXq15.js +291 -0
  161. package/studio/web/assets/status-g_mNYE7j.js +61 -0
  162. package/studio/web/assets/swimlanes-2SLR337P-NRHa2D8I.js +1 -0
  163. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +8 -0
  164. package/studio/web/assets/{tabs-BrHu7gJi.js → tabs-CnQ_zenf.js} +1 -1
  165. package/studio/web/assets/{time-Cclkb7kQ.js → time-Ci1v4fvT.js} +1 -1
  166. package/studio/web/assets/timeline-definition-EJHVYXUP-D39Y-xpq.js +120 -0
  167. package/studio/web/assets/toggleHighContrast-i2FjCrzO.js +49 -0
  168. package/studio/web/assets/treeView-Q6P3EWNA-CRWR9OqD.js +1 -0
  169. package/studio/web/assets/treemap-WGGIJYW6-ZfXyJgLO.js +1 -0
  170. package/studio/web/assets/{ts.worker-DpaaFKhy.js → ts.worker-DDMUrpqh.js} +118 -115
  171. package/studio/web/assets/{tsMode-D3T_5QzW.js → tsMode-DGeWfx0W.js} +5 -5
  172. package/studio/web/assets/{typescript-BegjUvtB.js → typescript-62_1FUbs.js} +1 -1
  173. package/studio/web/assets/{useEntityRestURL-BicFpx_N.js → useEntityRestURL-DSVwev9J.js} +1 -1
  174. package/studio/web/assets/useLocalStorage-BYoZF60G.js +1 -0
  175. package/studio/web/assets/usecaseDiagram-POWQR4AR-jf-zIDZ_.js +328 -0
  176. package/studio/web/assets/vendor-core-BdkdbUek.js +63 -0
  177. package/studio/web/assets/vendor-datadog-fCQhBHGk.js +6 -0
  178. package/studio/web/assets/vendor-react-izWchTRL.js +9 -0
  179. package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +2 -0
  180. package/studio/web/assets/vendor-ui-aE1tHcW7.js +41 -0
  181. package/studio/web/assets/vennDiagram-UO4OBE2U-BKYAZUh_.js +34 -0
  182. package/studio/web/assets/wardley-WFR3VGLG-BO7f36ci.js +1 -0
  183. package/studio/web/assets/{wardleyDiagram-VM6X3IG4-CjrkKWUR.js → wardleyDiagram-VNRHLVJA-Djb0JDQc.js} +3 -3
  184. package/studio/web/assets/{workers-Bmx4l_9B.js → workers-Cs52GyHv.js} +1 -1
  185. package/studio/web/assets/x-vYntCxm4.js +1 -0
  186. package/studio/web/assets/{xml-Bg40Rcra.js → xml-Co5ow_Ix.js} +1 -1
  187. package/studio/web/assets/{xychartDiagram-S5SC5T6Z-Biok4GYV.js → xychartDiagram-PMCCYNJV-BQh9ubvb.js} +3 -3
  188. package/studio/web/assets/{yaml-DolDvfXE.js → yaml-BcngpXye.js} +1 -1
  189. package/studio/web/assets/yaml.worker-DAK2o_H1.js +479 -0
  190. package/studio/web/auth/fabric-globe-dark.png +0 -0
  191. package/studio/web/auth/fabric-globe-light.png +0 -0
  192. package/studio/web/auth/fabric-platform-dark.png +0 -0
  193. package/studio/web/auth/fabric-platform-light.png +0 -0
  194. package/studio/web/index.html +15 -15
  195. package/utility/errors/hdbError.ts +26 -0
  196. package/studio/web/assets/FloatingChat-h9BJo6YG.js +0 -23
  197. package/studio/web/assets/applications-Dlq5kJ2O.js +0 -296
  198. package/studio/web/assets/architecture-7GRP2DOG-LB-MLAAb.js +0 -1
  199. package/studio/web/assets/architectureDiagram-5GKGNRK7-7SW3GD-K.js +0 -36
  200. package/studio/web/assets/authStore-C3Nfubqr.js +0 -3
  201. package/studio/web/assets/c4Diagram-7LVT6UL2-LBNf8t_X.js +0 -38
  202. package/studio/web/assets/channel-yictG-U-.js +0 -1
  203. package/studio/web/assets/chevron-up-DtKGqDn3.js +0 -1
  204. package/studio/web/assets/chunk-4HAMMTFA-DWtTut21.js +0 -62
  205. package/studio/web/assets/chunk-DU6HZSFF-Chq20Ba5.js +0 -127
  206. package/studio/web/assets/chunk-GMAD6QVW-BeS7S07A.js +0 -72
  207. package/studio/web/assets/chunk-IMKFNOWR-Bnh3tAVd.js +0 -231
  208. package/studio/web/assets/chunk-L3NEJ4N5-COfUyKII.js +0 -1
  209. package/studio/web/assets/chunk-P2QGCYS3-CP1VhG_c.js +0 -1
  210. package/studio/web/assets/chunk-SHT3W25Y-LpQkMsah.js +0 -168
  211. package/studio/web/assets/chunk-TICWLB2K-VOwzetX-.js +0 -206
  212. package/studio/web/assets/classDiagram-ZZMXUADV-VaEwSy_g.js +0 -1
  213. package/studio/web/assets/classDiagram-v2-VYDZK3BY-VaEwSy_g.js +0 -1
  214. package/studio/web/assets/cose-bilkent-JH36ORCC-Cuuf67Bh.js +0 -1
  215. package/studio/web/assets/createLucideIcon-CzW9508A.js +0 -1
  216. package/studio/web/assets/cynefin-OW5HDTMX-BbdbCvub.js +0 -1
  217. package/studio/web/assets/cynefinDiagram-5FMLGOSQ-TP-aIqbt.js +0 -62
  218. package/studio/web/assets/cytoscape.esm-C2x7inlt.js +0 -321
  219. package/studio/web/assets/dagre-GXQ25YYZ-DShnGpGo.js +0 -4
  220. package/studio/web/assets/dagre-PrKaheQc.js +0 -1
  221. package/studio/web/assets/diagram-UQ7AKVKN-DglXtQ6x.js +0 -41
  222. package/studio/web/assets/dialog-Cn2uWgD4.js +0 -1
  223. package/studio/web/assets/download-B5T5r7ss.js +0 -1
  224. package/studio/web/assets/editor.worker-DwcOiOeP.js +0 -30
  225. package/studio/web/assets/eventmodeling-NTZA5JFV-CLxnp2CR.js +0 -1
  226. package/studio/web/assets/flowDiagram-HODETNUW-BIbhmz9f.js +0 -1
  227. package/studio/web/assets/gitGraph-4MIJSDKK-D2s2w8lE.js +0 -1
  228. package/studio/web/assets/graphlib-DS17s2tU.js +0 -1
  229. package/studio/web/assets/index-7RMEgVG1.css +0 -1
  230. package/studio/web/assets/index-ZQ1upnh7.js +0 -824
  231. package/studio/web/assets/index.lazy-HDafSRxx.js +0 -2
  232. package/studio/web/assets/info-A6RAGUB7-DYjkvb0C.js +0 -1
  233. package/studio/web/assets/infoDiagram-27XIBGKW-Bnp1FJE5.js +0 -2
  234. package/studio/web/assets/json.worker-fO_hOLCP.js +0 -62
  235. package/studio/web/assets/languageServices-0ZvgO63b.js +0 -1
  236. package/studio/web/assets/linear-Cnt8506Q.js +0 -1
  237. package/studio/web/assets/mermaid.core-BlkGaMIH.js +0 -44
  238. package/studio/web/assets/packet-AYTQ26CC-Bi3V04Zi.js +0 -1
  239. package/studio/web/assets/pie-WAS4IAKB-_6DoDbng.js +0 -1
  240. package/studio/web/assets/profile-B-jyYdzV.js +0 -1
  241. package/studio/web/assets/radar-RG4KPBEZ-DDdVczcL.js +0 -1
  242. package/studio/web/assets/railroad-74A4TZTK-BJUP4Jds.js +0 -1
  243. package/studio/web/assets/railroad-abnf-HS5TGJTU-Bm3L1L0h.js +0 -1
  244. package/studio/web/assets/railroad-ebnf-LZEXJU2U-CKzLGlkw.js +0 -1
  245. package/studio/web/assets/railroad-peg-WCYAUIDC-S8xLjslx.js +0 -1
  246. package/studio/web/assets/regions-GN6Mu5aH.js +0 -1
  247. package/studio/web/assets/sankeyDiagram-P5KCCOFB-0vSOdymH.js +0 -40
  248. package/studio/web/assets/sequenceDiagram-WJ2MYXX4-hETizDWE.js +0 -162
  249. package/studio/web/assets/setComponentFile-D7Fww0lC.js +0 -1
  250. package/studio/web/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
  251. package/studio/web/assets/stateDiagram-D77RDMKH-CdYQ_KtC.js +0 -1
  252. package/studio/web/assets/stateDiagram-v2-MP3YSRHH-CdKuzQMT.js +0 -1
  253. package/studio/web/assets/status-koJd66XF.js +0 -61
  254. package/studio/web/assets/swimlanes-42K2YHIH-B8cHIpU4.js +0 -1
  255. package/studio/web/assets/swimlanesDiagram-VR7AAH4N-DmOSJwaH.js +0 -8
  256. package/studio/web/assets/timeline-definition-24CTP7MA-BJWYSXqF.js +0 -120
  257. package/studio/web/assets/toggleHighContrast-CGksgPgv.js +0 -49
  258. package/studio/web/assets/treeView-Q6P3EWNA-qxe_v6CQ.js +0 -1
  259. package/studio/web/assets/treemap-WGGIJYW6-dDo97XXF.js +0 -1
  260. package/studio/web/assets/useLocalStorage-BqMR3D8_.js +0 -1
  261. package/studio/web/assets/vendor-core-c2JRRJpV.js +0 -58
  262. package/studio/web/assets/vendor-datadog-CLUcJXOo.js +0 -6
  263. package/studio/web/assets/vendor-react-CJV_K1u4.js +0 -9
  264. package/studio/web/assets/vendor-tanstack-DxzraizX.js +0 -1
  265. package/studio/web/assets/vendor-ui-BUjK0h8a.js +0 -41
  266. package/studio/web/assets/vennDiagram-4TSXK5OY-A3i-lCdl.js +0 -34
  267. package/studio/web/assets/wardley-WFR3VGLG-B0ik-_6g.js +0 -1
  268. package/studio/web/assets/x-DIzaLEdK.js +0 -1
  269. package/studio/web/assets/yaml.worker-BDL8_iuk.js +0 -454
@@ -49,6 +49,7 @@ import {
49
49
  ServerError,
50
50
  AccessViolation,
51
51
  ValidationError,
52
+ appendErrorContext,
52
53
  type ValidationIssue,
53
54
  } from '../utility/errors/hdbError.ts';
54
55
  import * as signalling from '../utility/signalling.ts';
@@ -139,6 +140,20 @@ const EVICTION_BATCH_SIZE = 100;
139
140
  // letting an unbounded number of open transactions (and their snapshots) accumulate.
140
141
  const MAX_INFLIGHT_EVICTION_BATCHES = 4;
141
142
  const CACHEABLE_STATUS_CODES = new Set([200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501]);
143
+ // Smallest forward sample `getRecordCount` will extrapolate a record rate from; below it the scan runs
144
+ // to completion and reports an exact count.
145
+ const MIN_ESTIMATOR_SAMPLE = 1_000;
146
+ // Budget intervals the forward scan may spend before it must estimate rather than keep scanning.
147
+ const MAX_ESTIMATE_CHECKPOINTS = 20;
148
+ // A store estimate's `count`, or 0 when the store answered with a shape that cannot be trusted --
149
+ // DESIGN.md's invariant for this API family is that such an answer degrades rather than poisons.
150
+ function usableCount(estimate: any): number {
151
+ const { count, confidence } = estimate ?? {};
152
+ // `confidence` needs its own finiteness check, not just the range: `null >= 0 && null <= 1` is true
153
+ if (!Number.isFinite(count) || count < 0 || !Number.isFinite(confidence) || confidence < 0 || confidence > 1)
154
+ return 0;
155
+ return count;
156
+ }
142
157
  envMngr.initSync();
143
158
  const LMDB_PREFETCH_WRITES = envMngr.get(CONFIG_PARAMS.STORAGE_PREFETCHWRITES);
144
159
  const LOCK_TIMEOUT = 10000;
@@ -1595,8 +1610,7 @@ export function makeTable(options) {
1595
1610
  records: './', // an href to the records themselves
1596
1611
  name: tableName,
1597
1612
  database: databaseName,
1598
- auditSize:
1599
- auditStore instanceof RocksDatabase ? auditStore.getKeysCount() : auditStore?.getStats().entryCount,
1613
+ auditSize: auditStore?.getStats().entryCount,
1600
1614
  attributes,
1601
1615
  recordCount: undefined,
1602
1616
  estimatedRecordRange: undefined,
@@ -2581,7 +2595,10 @@ export function makeTable(options) {
2581
2595
  // getRange yields ascending by audit-log key, so the first entry is the oldest retained.
2582
2596
  // Mirror replicationConnection's retention check and the cleanup key basis (localTime ??
2583
2597
  // version). Fall back to the nominal time-based purge floor when the log is empty/unavailable.
2584
- for (const entry of auditStore.getRange({ start: 1, log: options?.nodeId })) {
2598
+ for (const entry of auditStore.getRange({
2599
+ start: 1,
2600
+ log: auditStore.logFor(options?.nodeId, options?.viaNodeId).name,
2601
+ })) {
2585
2602
  oldestRetainedAuditTime = entry.localTime ?? entry.version;
2586
2603
  break;
2587
2604
  }
@@ -2605,7 +2622,7 @@ export function makeTable(options) {
2605
2622
  // A recommit of the same transaction survived that skip only because the old write batch
2606
2623
  // still carried the put; a fresh-transaction replay (ERR_TRY_AGAIN) would drop the write.
2607
2624
  if (isRocksDB && !stagedOwnAuditEntry && dedupVersionCouldBeRetained(txnTime)) {
2608
- const priorAudit = auditStore.get(txnTime, tableId, id, options?.nodeId);
2625
+ const priorAudit = auditStore.get(txnTime, tableId, id, options?.nodeId, options?.viaNodeId);
2609
2626
  if (
2610
2627
  priorAudit &&
2611
2628
  priorAudit.version === txnTime &&
@@ -2669,7 +2686,7 @@ export function makeTable(options) {
2669
2686
  const isReDeliveredDuplicate = () => {
2670
2687
  if (stagedOwnAuditEntry) return false;
2671
2688
  if (!dedupVersionCouldBeRetained(txnTime)) return false; // pre-retention version — skip the end-of-log scan (best-effort; see above)
2672
- const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId);
2689
+ const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId, options?.viaNodeId);
2673
2690
  return (
2674
2691
  duplicate &&
2675
2692
  duplicate.version === txnTime &&
@@ -3135,6 +3152,7 @@ export function makeTable(options) {
3135
3152
  commit: (txnTime, existingEntry, retry, transaction: any) => {
3136
3153
  write.stagedEntry = undefined; // reset per round; set below once the removal is applied
3137
3154
  write.superseded = false; // reset per round, as in the update path
3155
+ write.skipped = false;
3138
3156
  // what a preceding write in this transaction left for this key is what gets removed
3139
3157
  // from the indices here, not the pre-transaction record (harper#1968)
3140
3158
  const priorStagedOp = priorStagedWrite(write);
@@ -3153,6 +3171,20 @@ export function makeTable(options) {
3153
3171
  if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId) < 0) {
3154
3172
  return;
3155
3173
  }
3174
+ const removalNodeId = options?.nodeId ?? 0; // this node's own id is 0
3175
+ const stagedRemoval = { value: undefined, version: txnTime, nodeId: removalNodeId };
3176
+ // re-delivered onto the removal it already made: writing again would append an audit entry
3177
+ // that every peer forwards and re-logs in turn. On a recommit the kept write batch shows this write's own
3178
+ // round-one tombstone as the existing entry, so a retry round repeats the first round's decision.
3179
+ const redelivered = retry
3180
+ ? write.redelivered
3181
+ : existingRecord == null && isSameRemoval(removalBefore(write, existingEntry), txnTime, removalNodeId);
3182
+ write.redelivered = redelivered;
3183
+ if (redelivered) {
3184
+ write.stagedEntry = stagedRemoval;
3185
+ write.skipped = true;
3186
+ return;
3187
+ }
3156
3188
  updateIndices(id, existingRecord, null, transaction && { transaction });
3157
3189
  if (audit || trackDeletes) {
3158
3190
  updateRecord(
@@ -3176,7 +3208,7 @@ export function makeTable(options) {
3176
3208
  // Only RocksDB's remove() takes an options object; on LMDB the 2nd arg is ifVersion, and its writes already join the batched txn.
3177
3209
  removeEntry(primaryStore, existingEntry, isRocksDB && transaction ? { transaction } : undefined);
3178
3210
  }
3179
- write.stagedEntry = { value: undefined }; // the key holds no record for the rest of this transaction
3211
+ write.stagedEntry = stagedRemoval; // the key holds no record for the rest of this transaction
3180
3212
  // the removal supersedes the nearest record an earlier write in this transaction stored
3181
3213
  // (older ones were already marked by their staged successors), so its saved blobs are
3182
3214
  // cleaned up post-commit unless its audit entry references them
@@ -4896,82 +4928,174 @@ export function makeTable(options) {
4896
4928
  const exactCount = options?.exactCount;
4897
4929
  const TIME_LIMIT = options?.timeLimit ?? 1000 / 2; // one second time limit, enforced by seeing if we are halfway through at 500ms
4898
4930
  const start = performance.now();
4899
- // `entryCount` (the exact key count) is only needed once the scan blows the time budget --
4900
- // to decide whether to estimate and as the extrapolation base. On RocksDB it is a full
4901
- // key-only scan, so we defer it: tables that finish within budget (the common case) and
4902
- // `exact_count` requests never pay for it. `halfway`/`entryCount` stay 0 until first computed.
4903
4931
  let entryCount = 0;
4904
- let halfway = 0;
4905
- let counted = false;
4906
- let completeForExact = false;
4932
+ let remainderPhysical = 0;
4933
+ let estimator;
4934
+ // feature-detected per DESIGN.md's invariant for this API family; LMDB stores do not implement it
4935
+ const canEstimate =
4936
+ typeof primaryStore.createCountEstimator === 'function' && typeof primaryStore.estimateCount === 'function';
4937
+ let estimatorFailed = false;
4938
+ let warnedNoBase = false;
4939
+ let checkpoints = 0;
4940
+ let checkpointedEntries = 0;
4907
4941
  let recordCount = 0;
4908
4942
  let entriesScanned = 0;
4943
+ let lastKey;
4909
4944
  let limit: number;
4910
- for (const { value } of primaryStore.getRange({ start: true, lazy: true, snapshot: false })) {
4945
+ let nextCheckAt = start + TIME_LIMIT;
4946
+ for (const { key, value } of primaryStore.getRange({ start: true, lazy: true, snapshot: false })) {
4911
4947
  if (value != null) recordCount++;
4912
4948
  entriesScanned++;
4949
+ lastKey = key;
4913
4950
  await rest();
4914
- if (!exactCount && !completeForExact && performance.now() - start > TIME_LIMIT) {
4915
- if (!counted) {
4916
- counted = true;
4917
- entryCount = isRocksDB
4918
- ? primaryStore.getKeysCount({ start: undefined })
4919
- : primaryStore.getStats().entryCount;
4920
- halfway = Math.floor(entryCount / 2);
4951
+ // a table too small to reach the floor is small enough to finish exactly
4952
+ if (exactCount || entriesScanned < MIN_ESTIMATOR_SAMPLE) continue;
4953
+ const now = performance.now();
4954
+ if (now <= nextCheckAt) continue;
4955
+ nextCheckAt = now + TIME_LIMIT;
4956
+ checkpoints++;
4957
+ if (canEstimate && !estimatorFailed) {
4958
+ try {
4959
+ estimator ??= primaryStore.createCountEstimator({ start: true });
4960
+ estimator.advance(lastKey, entriesScanned - checkpointedEntries);
4961
+ checkpointedEntries = entriesScanned;
4962
+ entryCount = usableCount(estimator.estimate());
4963
+ } catch (error) {
4964
+ // a store closing concurrently -- drop_table can, while this scan is parked in a yield
4965
+ logger.debug?.('Count estimator unavailable, falling back to an exact scan', error);
4966
+ estimatorFailed = true;
4967
+ estimator = undefined;
4968
+ entryCount = 0;
4921
4969
  }
4922
- if (entriesScanned < halfway) {
4923
- // it is taking too long, so we will just take this sample and a sample from the end to estimate
4924
- limit = entriesScanned;
4925
- break;
4970
+ } else if (!canEstimate) {
4971
+ // `canEstimate` false is not the same as "LMDB": a RocksDB store whose native module predates
4972
+ // the estimator API lands here too, and `RocksDatabase.getStats()` carries no `entryCount`.
4973
+ try {
4974
+ const stats = primaryStore.getStats?.();
4975
+ entryCount = Number.isFinite(stats?.entryCount) && stats.entryCount > 0 ? stats.entryCount : 0;
4976
+ } catch {
4977
+ entryCount = 0;
4978
+ }
4979
+ }
4980
+ if (!entryCount && canEstimate && !estimatorFailed) {
4981
+ // Range estimates are block-granular and can report 0 for a store whose entries are still
4982
+ // in the memtable. Without a base the escape cannot fire at all, so fall back to the
4983
+ // whole-store property rather than silently walking the table.
4984
+ try {
4985
+ const wholeStore = primaryStore.getEstimatedKeyCount();
4986
+ entryCount = Number.isFinite(wholeStore) && wholeStore > 0 ? wholeStore : 0;
4987
+ } catch {
4988
+ entryCount = 0;
4989
+ }
4990
+ if (!entryCount && !warnedNoBase) {
4991
+ warnedNoBase = true;
4992
+ logger.debug?.(`No usable key-count estimate for ${tableName}; counting records by full scan`);
4993
+ }
4994
+ }
4995
+ // Zero is "no usable base": degrade to the exact scan. The checkpoint ceiling is what stops a
4996
+ // base that keeps undershooting from holding the halfway test false forever and walking the
4997
+ // whole table; the reverse sample is bounded by `limit` in turn.
4998
+ if (
4999
+ entryCount > 0 &&
5000
+ (checkpoints >= MAX_ESTIMATE_CHECKPOINTS || entriesScanned < Math.floor(entryCount / 2))
5001
+ ) {
5002
+ if (canEstimate) {
5003
+ try {
5004
+ const remaining = primaryStore.estimateCount({ start: lastKey, exclusiveStart: true });
5005
+ // widened by its own reported untrustworthiness: block-granular, so it can land below
5006
+ // the live count it is meant to bound
5007
+ const remainingCount = usableCount(remaining);
5008
+ remainderPhysical = remainingCount > 0 ? remainingCount * (2 - remaining.confidence) : 0;
5009
+ } catch {
5010
+ remainderPhysical = 0;
5011
+ }
5012
+ // A zero or unusable remainder is valid -- entries still in the memtable read as none
5013
+ // through range statistics -- but it would leave `baseMax` resting on the sampled ends
5014
+ // alone. The whole-store property is a separate, non-range source, so fall back to it.
5015
+ if (!remainderPhysical) {
5016
+ try {
5017
+ const wholeStore = primaryStore.getEstimatedKeyCount();
5018
+ if (Number.isFinite(wholeStore)) remainderPhysical = Math.max(wholeStore - entriesScanned, 0);
5019
+ } catch {
5020
+ remainderPhysical = 0;
5021
+ }
5022
+ }
4926
5023
  }
4927
- // Past the halfway point already: finishing the scan for an exact count is cheaper
4928
- // than estimating. Set the flag so we stop re-evaluating the budget on each remaining iteration.
4929
- completeForExact = true;
5024
+ limit = entriesScanned;
5025
+ break;
4930
5026
  }
4931
5027
  }
4932
5028
  if (limit) {
4933
5029
  // in this case we are going to make an estimate of the table count using the first thousand
4934
5030
  // entries and last thousand entries
4935
5031
  const firstRecordCount = recordCount;
5032
+ const firstKey = lastKey;
4936
5033
  recordCount = 0;
4937
5034
  // Bound the reverse scan explicitly. The getRange `limit` option is honored by lmdb-js but
4938
5035
  // ignored by rocksdb-js; without this break the scan reads the whole table, so `recordRate`
4939
5036
  // blows up to ~entryCount/(2*limit) and the estimate scales with entryCount^2 -- the source
4940
5037
  // of the wildly inflated `record_count` (e.g. 20,000,000 for ~105k rows) on large RocksDB
4941
- // tables. The early-exit above guarantees limit < entryCount/2, so the two samples stay disjoint.
5038
+ // tables.
4942
5039
  let reverseScanned = 0;
4943
- for (const { value } of primaryStore.getRange({
5040
+ // Sized independently of the forward scan. `entriesScanned` is whatever the forward pass
5041
+ // covered before it escaped, and the checkpoint ceiling lets that run twenty budget
5042
+ // intervals when the base keeps undershooting; matching it here would read that same count
5043
+ // again and double the wall clock of the call this path exists to bound.
5044
+ const reverseLimit = Math.min(limit, MIN_ESTIMATOR_SAMPLE);
5045
+ // Disjointness is enforced against the forward scan's own last key rather than inferred from
5046
+ // the base, which is an estimate that can overshoot by more than 2x.
5047
+ let sampledWholeTable = false;
5048
+ for (const { key, value } of primaryStore.getRange({
4944
5049
  start: '\uffff',
4945
5050
  reverse: true,
4946
5051
  lazy: true,
4947
- limit,
5052
+ limit: reverseLimit,
4948
5053
  snapshot: false,
4949
5054
  })) {
5055
+ if (compareKeys(key, firstKey) <= 0) {
5056
+ sampledWholeTable = true;
5057
+ break;
5058
+ }
4950
5059
  if (value != null) recordCount++;
4951
5060
  reverseScanned++;
4952
5061
  await rest();
4953
- if (reverseScanned >= limit) break;
5062
+ if (reverseScanned >= reverseLimit) break;
4954
5063
  }
5064
+ // the samples met, so between them they covered every entry
5065
+ if (sampledWholeTable) return { recordCount: recordCount + firstRecordCount };
4955
5066
  // Use the actual entries sampled, not limit*2: the reverse scan can yield fewer than `limit`
4956
5067
  // (concurrent deletions under snapshot:false, or an overestimated entryCount), and counting
4957
5068
  // those un-scanned slots would inflate the denominator and underestimate the rate.
4958
- const sampleSize = limit + reverseScanned;
4959
- const recordRate = (recordCount + firstRecordCount) / sampleSize;
4960
- const variance =
4961
- Math.pow((recordCount - firstRecordCount + 1) / limit / 2, 2) + // variance between samples
4962
- (recordRate * (1 - recordRate)) / sampleSize;
4963
- const sd = Math.max(Math.sqrt(variance) * entryCount, 1);
4964
- const estimatedRecordCount = Math.round(recordRate * entryCount);
4965
- // TODO: This uses a normal/Wald interval, but a binomial confidence interval is probably better calculated using
4966
- // Wilson score interval or Agresti-Coull interval (I think the latter is a little easier to calculate/implement).
4967
- const lowerCiLimit = Math.max(estimatedRecordCount - 1.96 * sd, recordCount + firstRecordCount);
4968
- const upperCiLimit = Math.min(estimatedRecordCount + 1.96 * sd, entryCount);
4969
- let significantUnit = Math.pow(10, Math.round(Math.log10(sd)));
4970
- if (significantUnit > estimatedRecordCount) significantUnit = significantUnit / 10;
4971
- recordCount = Math.round(estimatedRecordCount / significantUnit) * significantUnit;
5069
+ const sampledRecords = recordCount + firstRecordCount;
5070
+ const recordRate = sampledRecords / (limit + reverseScanned);
5071
+ // Endpoints for the extrapolation base, spanning both ways an estimated base can be wrong:
5072
+ // every remaining entry superseded (only what was sampled is live) through every remaining
5073
+ // entry live (the uncalibrated physical count). Churn concentrated outside the sampled ends
5074
+ // calibrates to nothing, so an interval derived from the estimator's confidence would sit
5075
+ // narrowly around the wrong number. Both endpoints are themselves estimates on RocksDB, so
5076
+ // this is a widened heuristic interval, not a guaranteed bound on the live count.
5077
+ const baseMin = entriesScanned + reverseScanned;
5078
+ const baseMax = Math.max(entriesScanned + remainderPhysical, entryCount, baseMin);
5079
+ const estimatedRecordCount = Math.round(recordRate * Math.max(entryCount, baseMin));
5080
+ // The samples counted these directly, and the entries between them can only add; everything
5081
+ // outside the samples could be live. A statistical interval inside those endpoints would be
5082
+ // narrowest exactly where the ends are least representative of the middle -- sampled ends
5083
+ // that are all deletion entries give a rate of 0, collapsing an upper end to ~0 with live
5084
+ // rows in between -- so the endpoints are the evidence itself.
5085
+ const lower = sampledRecords;
5086
+ const upper = Math.round(baseMax);
5087
+ // Report only the precision the interval supports, but never so coarse a unit that the
5088
+ // estimate rounds away: `baseMax` is physical and can exceed a calibrated estimate by
5089
+ // orders of magnitude, which a single division cannot walk back.
5090
+ let significantUnit = Math.pow(10, Math.round(Math.log10(Math.max((upper - lower) / 2, 1))));
5091
+ while (significantUnit > estimatedRecordCount && significantUnit > 1) significantUnit /= 10;
5092
+ recordCount = Math.min(
5093
+ Math.max(Math.round(estimatedRecordCount / significantUnit) * significantUnit, lower),
5094
+ upper
5095
+ );
4972
5096
  return {
4973
5097
  recordCount,
4974
- estimatedRange: [Math.round(lowerCiLimit), Math.round(upperCiLimit)],
5098
+ estimatedRange: [lower, upper],
4975
5099
  };
4976
5100
  }
4977
5101
  return {
@@ -5857,6 +5981,23 @@ export function makeTable(options) {
5857
5981
  return ids;
5858
5982
  }
5859
5983
 
5984
+ /**
5985
+ * What left a delete's key without a record: the nearest earlier write in the transaction that staged
5986
+ * state, else the stored entry. Only writes marked skipped are passed over, because an invalidate or
5987
+ * relocate stores a null stub without staging it.
5988
+ */
5989
+ function removalBefore(write: any, existingEntry: Entry | undefined): Partial<Entry> | undefined {
5990
+ for (let prior = write.priorWrite; prior; prior = prior.priorWrite) {
5991
+ if (prior.stagedEntry) return prior.stagedEntry;
5992
+ if (!prior.skipped) return;
5993
+ }
5994
+ if (!(existingEntry?.metadataFlags & INVALIDATED)) return existingEntry;
5995
+ }
5996
+
5997
+ function isSameRemoval(entry: Partial<Entry> | undefined, txnTime: number, nodeId: number): boolean {
5998
+ return entry != null && entry.value == null && entry.version === txnTime && (entry.nodeId ?? 0) === nodeId;
5999
+ }
6000
+
5860
6001
  function precedesExistingVersion(txnTime: number, existingEntry: Partial<Entry>, nodeId?: number): number {
5861
6002
  if (nodeId === undefined) {
5862
6003
  nodeId = getThisNodeId(auditStore);
@@ -6089,25 +6230,34 @@ export function makeTable(options) {
6089
6230
  }
6090
6231
  resolve(resolvedEntry);
6091
6232
  } catch (error) {
6092
- error.message += ` while resolving record ${id} for ${tableName}`;
6093
- if (
6094
- existingRecord &&
6095
- (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
6096
- !context?.mustRevalidate) ||
6097
- (context?.staleIfError &&
6098
- (error.statusCode === 500 ||
6099
- error.statusCode === 502 ||
6100
- error.statusCode === 503 ||
6101
- error.statusCode === 504)))
6102
- ) {
6103
- // these are conditions under which we can use stale data after an error
6104
- resolve({
6105
- key: id,
6106
- version: existingVersion,
6107
- value: existingRecord,
6108
- } as any);
6109
- logger.trace?.(error.message, '(returned stale record)');
6110
- } else reject(error);
6233
+ // A source may reject with anything at all, so deciding how to settle is itself
6234
+ // fallible: `message` is not assignable on every error (a DOMException from
6235
+ // AbortSignal.timeout), and a nullish rejection makes the reads below throw.
6236
+ // Leaving this promise unsettled hangs the caller forever, so every path here
6237
+ // has to end in resolve() or reject().
6238
+ try {
6239
+ appendErrorContext(error, ` while resolving record ${id} for ${tableName}`);
6240
+ if (
6241
+ existingRecord &&
6242
+ (((error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED' || error.code === 'EAI_AGAIN') &&
6243
+ !context?.mustRevalidate) ||
6244
+ (context?.staleIfError &&
6245
+ (error.statusCode === 500 ||
6246
+ error.statusCode === 502 ||
6247
+ error.statusCode === 503 ||
6248
+ error.statusCode === 504)))
6249
+ ) {
6250
+ // these are conditions under which we can use stale data after an error
6251
+ resolve({
6252
+ key: id,
6253
+ version: existingVersion,
6254
+ value: existingRecord,
6255
+ } as any);
6256
+ logger.trace?.((error as Error)?.message, '(returned stale record)');
6257
+ } else reject(error);
6258
+ } catch (settlingError) {
6259
+ reject(error ?? settlingError);
6260
+ }
6111
6261
  const resolveDuration = performance.now() - start;
6112
6262
  recordAction(resolveDuration, 'cache-resolution', tableName, null, 'fail');
6113
6263
  if (responseHeaders)
@@ -75,8 +75,9 @@ export function replayLogs(rootStore: RocksDatabase, tables: any): Promise<void>
75
75
  tableById.set(table.tableId, table);
76
76
  }
77
77
  // replay all the logs
78
- let transaction: DatabaseTransaction;
78
+ let transaction: DatabaseTransaction | undefined;
79
79
  let lastTimestamp = 0;
80
+ let openCommitEnded = false;
80
81
  let writes = 0;
81
82
  let skipped = 0;
82
83
  // Track forward progress so a backlog of unwritable entries can't grind the boot thread
@@ -110,8 +111,27 @@ export function replayLogs(rootStore: RocksDatabase, tables: any): Promise<void>
110
111
  originatingOperation,
111
112
  username,
112
113
  extendedType,
114
+ endTxn,
113
115
  } = auditRecord;
114
116
  try {
117
+ // A replay transaction is one native commit ("Boot replay transactions" in DESIGN.md).
118
+ if (transaction && (openCommitEnded || lastTimestamp !== version)) {
119
+ const ending = transaction;
120
+ transaction = undefined;
121
+ try {
122
+ ending.directCommitSync();
123
+ } catch (error) {
124
+ logger.error('Error committing replay transaction', error);
125
+ }
126
+ // The wall-clock budget (harper#1316) is checked only here, between native commits with
127
+ // nothing staged, so aborting never tears a commit in half.
128
+ if (shouldAbortSlowReplay(performance.now() - replayStartTime, replayTimeoutMs)) {
129
+ logger.fatal(
130
+ `Aborting transaction-log replay in ${(rootStore as any).databaseName} database: replay has exceeded the wall-clock time limit (${writes} written, ${skipped} skipped). The transaction log contains a pathologically deep out-of-order write history that is too expensive to reconcile during boot (harper#1316). Re-clone this node from a healthy leader to recover the unreplayed data.`
131
+ );
132
+ break;
133
+ }
134
+ }
115
135
  if (classifyAuditEntryForReplay(extendedType, tableId, true) === 'corrupt-header') {
116
136
  skipped++;
117
137
  noProgressRun++;
@@ -165,28 +185,9 @@ export function replayLogs(rootStore: RocksDatabase, tables: any): Promise<void>
165
185
  warnedReplayHappening = true;
166
186
  console.warn('Harper was not properly shutdown, replaying transaction logs to synchronize database');
167
187
  }
168
- if (lastTimestamp !== version) {
188
+ if (!transaction) {
169
189
  lastTimestamp = version;
170
- try {
171
- // commit the last transaction since we are starting a new one
172
- transaction?.directCommitSync();
173
- } catch (error) {
174
- logger.error('Error committing replay transaction', error);
175
- }
176
- // Abort if replay has exceeded the total wall-clock budget even while making progress
177
- // (harper#1316, facet a). shouldAbortStalledReplay resets its counters on every write,
178
- // so a slow-but-progressing replay (deep out-of-order audit chain walk per entry) can
179
- // peg the boot thread indefinitely without tripping it. Checked only here, at a version
180
- // boundary: the prior version's transaction was just committed in full and the new one
181
- // is not yet staged, so aborting never tears a same-version (same source-transaction)
182
- // write batch in half. Re-clone to recover the unreplayed remainder.
183
- if (shouldAbortSlowReplay(performance.now() - replayStartTime, replayTimeoutMs)) {
184
- logger.fatal(
185
- `Aborting transaction-log replay in ${(rootStore as any).databaseName} database: replay has exceeded the wall-clock time limit (${writes} written, ${skipped} skipped). The transaction log contains a pathologically deep out-of-order write history that is too expensive to reconcile during boot (harper#1316). Re-clone this node from a healthy leader to recover the unreplayed data.`
186
- );
187
- transaction = undefined as any; // already committed above; nothing staged for the new version
188
- break;
189
- }
190
+ openCommitEnded = false;
190
191
  transaction = new DatabaseTransaction();
191
192
  transaction.db = primaryStore;
192
193
  transaction.timestamp = version;
@@ -281,6 +282,8 @@ export function replayLogs(rootStore: RocksDatabase, tables: any): Promise<void>
281
282
  logger.error(`Error writing from replay of log`, err, {
282
283
  version,
283
284
  });
285
+ } finally {
286
+ if (endTxn) openCommitEnded = true;
284
287
  }
285
288
  }
286
289
  try {
@@ -192,7 +192,7 @@ function notifyFromTransactionData(subscriptions, auditLogIterable?, allowYield
192
192
  let ancestorLevel = 0;
193
193
  do {
194
194
  // we iterate through the key hierarchy, notifying all subscribers for each key,
195
- // so for an id like resource/foo/bar, we notify subscribers for resource/foo/bar, resource/foo/, resource/foo, resource/, and resource
195
+ // so for an id like resource/foo/bar, we notify subscribers for resource/foo/bar, resource/foo/, resource/, and the root (null)
196
196
  // this allows for efficient subscriptions to children ids/topics
197
197
  const keySubscriptions = tableSubscriptions.get(matchingKey);
198
198
  if (keySubscriptions) {
@@ -230,9 +230,9 @@ function notifyFromTransactionData(subscriptions, auditLogIterable?, allowYield
230
230
  if (lastSlash !== matchingKey.length - 1) {
231
231
  ancestorLevel++; // don't increase the ancestor level for this going from resource/ to resource
232
232
  }
233
- if (lastSlash > -1) {
234
- matchingKey = matchingKey.slice(0, lastSlash + 1);
235
- } else matchingKey = null;
233
+ // lastIndexOf clamps a negative fromIndex to 0, so '/' would otherwise yield itself forever
234
+ const parentKey = lastSlash > -1 ? matchingKey.slice(0, lastSlash + 1) : null;
235
+ matchingKey = parentKey === matchingKey ? null : parentKey;
236
236
  } while (true);
237
237
  }
238
238
  } else if (auditRecord.type === 'reload') {
@@ -27,10 +27,14 @@ import { extractCertificateChain, extractRevocationUrls, bufferToPem } from './v
27
27
  import { getCachedCertificateVerificationConfig } from './verificationConfig.ts';
28
28
  import { verifyOCSP } from './ocspVerification.ts';
29
29
  import { verifyCRL } from './crlVerification.ts';
30
- import type { PeerCertificate, CertificateVerificationResult } from './types.ts';
30
+ import { resolveTrustedIssuer } from './trustedIssuers.ts';
31
+ import type { PeerCertificate, CertificateVerificationResult, FailureMode } from './types.ts';
31
32
 
32
33
  const logger = loggerWithTag('cert-verification');
33
34
 
35
+ const warnedUnresolvedLeaves = new Set<string>();
36
+ const MAX_WARNED_LEAVES = 10_000;
37
+
34
38
  /**
35
39
  * Verify certificate revocation status using OCSP and/or CRL
36
40
  * @param peerCertificate - Peer certificate object from TLS connection
@@ -52,14 +56,32 @@ export async function verifyCertificate(
52
56
  return { valid: true, status: 'disabled', method: 'disabled' };
53
57
  }
54
58
 
59
+ // ahead of the chain check so an explicitly disabled control can never reject
60
+ if (config.crl.enabled === false && config.ocsp.enabled === false) {
61
+ logger.debug?.('Both CRL and OCSP disabled - verification disabled');
62
+ return { valid: true, status: 'disabled', method: 'disabled' };
63
+ }
64
+
55
65
  // Extract certificate chain
56
66
  const certChain = extractCertificateChain(peerCertificate);
57
67
  logger.trace?.(`Certificate chain length: ${certChain.length}`);
58
68
 
59
- // Check if we have sufficient chain for verification (need certificate and issuer)
60
- if (certChain.length < 2 || !certChain[0].issuer) {
61
- logger.debug?.('Certificate chain insufficient for revocation checking - need certificate and issuer');
62
- return { valid: true, status: 'no-issuer-cert', method: 'disabled' };
69
+ if (certChain.length === 0) {
70
+ return unresolvedIssuerResult(config.failureMode, peerCertificate, 'no certificate data');
71
+ }
72
+ // Revocation checking needs the issuer. The socket omits it on every resumed TLS session and on
73
+ // Node 26.8.0/26.8.1 (nodejs/node#65579); recover it from the CA set that authorized the client.
74
+ if (!certChain[0].issuer) {
75
+ const issuer = resolveTrustedIssuer(certChain[0].cert, peerCertificate.fingerprint256);
76
+ if (!issuer) {
77
+ return unresolvedIssuerResult(
78
+ config.failureMode,
79
+ peerCertificate,
80
+ 'issuer is neither in the presented chain nor among the configured certificate authorities'
81
+ );
82
+ }
83
+ logger.debug?.('Issuer certificate resolved from the configured certificate authorities');
84
+ certChain[0].issuer = issuer;
63
85
  }
64
86
 
65
87
  // Extract certificate revocation URLs in single parse operation
@@ -117,17 +139,36 @@ export async function verifyCertificate(
117
139
  logger.debug?.('Skipping OCSP - no responder URLs in certificate');
118
140
  }
119
141
 
120
- // All methods tried or skipped - determine failure handling
121
- // If both methods are explicitly disabled (not just unavailable), treat as disabled
122
- if (config.crl.enabled === false && config.ocsp.enabled === false) {
123
- logger.debug?.('Both CRL and OCSP disabled - verification disabled');
124
- return { valid: true, status: 'disabled', method: 'disabled' };
125
- }
126
-
127
- // Methods are enabled but unavailable - apply failure mode
142
+ // All methods tried or skipped - apply failure mode
128
143
  if (config.failureMode === 'fail-closed') {
129
144
  return { valid: false, status: 'no-verification-available', method: 'disabled' };
130
145
  }
131
146
 
132
147
  return { valid: true, status: 'verification-unavailable-allowed', method: 'disabled' };
133
148
  }
149
+
150
+ /**
151
+ * Revocation status cannot be established without the issuer, so the configured failure mode
152
+ * decides: refuse under fail-closed, allow under fail-open. Either way this is a security control
153
+ * not running, so it is logged at warn (once per certificate) rather than debug.
154
+ */
155
+ function unresolvedIssuerResult(
156
+ failureMode: FailureMode | undefined,
157
+ peerCertificate: PeerCertificate,
158
+ reason: string
159
+ ): CertificateVerificationResult {
160
+ const valid = failureMode !== 'fail-closed';
161
+ const leafKey = peerCertificate.fingerprint256 ?? peerCertificate.subject?.CN ?? '';
162
+ if (!warnedUnresolvedLeaves.has(leafKey)) {
163
+ if (warnedUnresolvedLeaves.size >= MAX_WARNED_LEAVES) warnedUnresolvedLeaves.clear();
164
+ warnedUnresolvedLeaves.add(leafKey);
165
+ logger.warn?.(
166
+ `Cannot check revocation status for client certificate ${peerCertificate.subject?.CN ?? 'unknown'}: ${reason}; ${
167
+ valid ? 'allowing connection (fail-open)' : 'rejecting connection (fail-closed)'
168
+ }`
169
+ );
170
+ } else {
171
+ logger.debug?.(`Cannot check revocation status for ${peerCertificate.subject?.CN ?? 'unknown'}: ${reason}`);
172
+ }
173
+ return { valid, status: 'no-issuer-cert', method: 'disabled' };
174
+ }