harper 5.3.2 → 5.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +3 -3
- package/bin/copyDb.ts +4 -3
- package/components/DESIGN.md +12 -0
- package/components/packageComponent.ts +42 -17
- package/config/DESIGN.md +4 -0
- package/config/configUtils.ts +8 -8
- package/config/parseConfigFile.ts +11 -7
- package/dist/bin/cliOperations.js +3 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.js +3 -3
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/components/packageComponent.d.ts +9 -7
- package/dist/components/packageComponent.js +41 -16
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/config/configUtils.js +4 -3
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +1 -0
- package/dist/config/parseConfigFile.js +7 -3
- package/dist/config/parseConfigFile.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +80 -11
- package/dist/resources/DatabaseTransaction.js +1100 -281
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +1 -1
- package/dist/resources/LMDBTransaction.js +4 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/Resource.js +40 -14
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.js +1 -0
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.js +97 -34
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +34 -16
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/databases.d.ts +5 -0
- package/dist/resources/databases.js +38 -4
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/recordLock.d.ts +9 -4
- package/dist/resources/recordLock.js +79 -19
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -0
- package/dist/server/DurableSubscriptionsSession.js +24 -24
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/mqtt.js +59 -29
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.js +18 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +8 -2
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.js +3 -2
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -1
- package/dist/utility/npmUtilities.js +18 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/package.json +1 -1
- package/resources/DESIGN.md +46 -10
- package/resources/DatabaseTransaction.ts +1268 -279
- package/resources/LMDBTransaction.ts +4 -3
- package/resources/Resource.ts +39 -14
- package/resources/RocksTransactionLogStore.ts +1 -0
- package/resources/Table.ts +120 -41
- package/resources/analytics/DESIGN.md +17 -0
- package/resources/analytics/write.ts +28 -15
- package/resources/databases.ts +40 -4
- package/resources/record-locks.md +34 -17
- package/resources/recordLock.ts +89 -22
- package/server/DESIGN.md +9 -1
- package/server/DurableSubscriptionsSession.ts +22 -23
- package/server/mqtt.ts +54 -29
- package/server/serverHelpers/NodeAdapterResponse.ts +19 -1
- package/server/serverHelpers/contentTypes.ts +8 -1
- package/server/serverHelpers/sharedMessageEncoding.ts +3 -2
- package/studio/web/assets/Chat-DjZNWwwA.js +2100 -0
- package/studio/web/assets/FloatingChat-BWfKK_RX.js +23 -0
- package/studio/web/assets/SearchDialog-CdlNxWef.js +1 -0
- package/studio/web/assets/{abnfDiagram-O67JEVCF-raXE027J.js → abnfDiagram-YUKMZFIV-DPR2lFRk.js} +1 -1
- package/studio/web/assets/{apiToken-DNrfij0E.js → apiToken-DyI8IwU3.js} +1 -1
- package/studio/web/assets/applications-DgGopKJq.js +296 -0
- package/studio/web/assets/architecture-WOLXFQ4H-DtJHIK0_.js +1 -0
- package/studio/web/assets/{architectureDiagram-NJMV4G6O-C8_Eet7w.js → architectureDiagram-47P4ROYG-MDymp2dh.js} +3 -3
- package/studio/web/assets/authStore-DE1HFvVP.js +3 -0
- package/studio/web/assets/{blockDiagram-BEXU5L5S-DR6S5-1D.js → blockDiagram-E7TT5TSB-O5qRgZmd.js} +3 -3
- package/studio/web/assets/button-C37w80Uk.js +21 -0
- package/studio/web/assets/{c4Diagram-YGBWAQC7-BAfZ1G01.js → c4Diagram-CRA5TL53-BdPORGHo.js} +2 -2
- package/studio/web/assets/channel-yCStoq0_.js +1 -0
- package/studio/web/assets/{chevron-up-CFxbMGv2.js → chevron-up-DozwrIz-.js} +1 -1
- package/studio/web/assets/chunk-2BW5OAIV-DJ-iXJAw.js +10 -0
- package/studio/web/assets/{chunk-ZIGJFQKS-Cs8fUn7y.js → chunk-3YJQHVM4-BOgacqS1.js} +2 -2
- package/studio/web/assets/{chunk-7PRAP22T-KyqX6x3f.js → chunk-4EA7E6EY-C-7xqUBl.js} +1 -1
- package/studio/web/assets/{chunk-GWA4HPMP-DsejB6gA.js → chunk-7TKQ45FW-C3XA1bIN.js} +1 -1
- package/studio/web/assets/{chunk-GTNCS2PH-DGHLXyow.js → chunk-AW2ZBBNX-BdvgD1UW.js} +3 -3
- package/studio/web/assets/{chunk-5DYCD2WN-BceBfIHT.js → chunk-DBDB3WZW-CKKxIc6F.js} +1 -1
- package/studio/web/assets/{chunk-MBY4JIJT-BwSGweQP.js → chunk-E2ZNV5FY-bVefNHvz.js} +3 -3
- package/studio/web/assets/chunk-EU5HNXII-BHv2kWp4.js +213 -0
- package/studio/web/assets/chunk-FVRAUYC3-BoL_NO6b.js +2 -0
- package/studio/web/assets/{chunk-742MDFTN-CIkt8ijt.js → chunk-HJ2JQQFS-N_98oMRS.js} +1 -1
- package/studio/web/assets/{chunk-NETBCI7D-C8UnOw_f.js → chunk-HTAEGDNF-BQ1fSjPs.js} +1 -1
- package/studio/web/assets/{chunk-WEXAMYUT-BAIhTwyh.js → chunk-KQW6MTUR-qP0QiQAg.js} +1 -1
- package/studio/web/assets/chunk-NGNAAXSQ-DqoPQk1a.js +136 -0
- package/studio/web/assets/{chunk-UA2S7LBM-BHpEdH8D.js → chunk-NTY3LDVX-CSBFjlJg.js} +1 -1
- package/studio/web/assets/chunk-VPRB5NB3-BVHOe9Q-.js +129 -0
- package/studio/web/assets/{chunk-7INBJB4K-BpeAXyOi.js → chunk-XC4XBNZT-46uQwtF4.js} +10 -10
- package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DmDPkt2u.js → classDiagram-v2-K4WV4PDN-Da0ps1Ui.js} +3 -3
- package/studio/web/assets/cn-DpF1PxBT.js +1 -0
- package/studio/web/assets/{createLucideIcon-fO5dL7D5.js → createLucideIcon-DLWRoWN-.js} +1 -1
- package/studio/web/assets/{cssMode-DBucrQdT.js → cssMode-4ddO86ey.js} +1 -1
- package/studio/web/assets/cynefin-EF2NZ3EQ-jQ2eCUaU.js +1 -0
- package/studio/web/assets/cynefinDiagram-3GCD6N5R-TNi5iS0E.js +62 -0
- package/studio/web/assets/dagre-W4DXFKR2-BnLHmA44.js +4 -0
- package/studio/web/assets/{diagram-22UHCM2B-DJCvcaWP.js → diagram-2UJZ2QOL-r_jehhZR.js} +7 -7
- package/studio/web/assets/{diagram-3UASUU5V-Du6VKWln.js → diagram-OVF4WLC6-BSUXGVh8.js} +11 -11
- package/studio/web/assets/{diagram-CDSNMT55-l5Ga0u-W.js → diagram-PFPMY2P6-BrVedycw.js} +3 -3
- package/studio/web/assets/{diagram-MLGK6HIB-CNQY47LA.js → diagram-UMYRVEAY-CBIaakBB.js} +1 -1
- package/studio/web/assets/diagram-YEKJPTXX-DFmmDXCl.js +41 -0
- package/studio/web/assets/diagram-ZIFT7M5P-CQfOWxOi.js +3 -0
- package/studio/web/assets/dist-9lpzt2Hn.js +11 -0
- package/studio/web/assets/{ebnfDiagram-ZINNZB2B-5OnkTpfi.js → ebnfDiagram-VR2GEFS7-FXA3Xe6e.js} +1 -1
- package/studio/web/assets/{editor-CHpQCuBq.js → editor-DATuu7_v.js} +1 -1
- package/studio/web/assets/elk-IJKZMXRS-CC4gfVRU.js +27 -0
- package/studio/web/assets/{erDiagram-OPXOYQCR-CQlCX84D.js → erDiagram-O2IAPWRE-C-BkDTPl.js} +2 -2
- package/studio/web/assets/errorText-8xV8WZBt.js +1 -0
- package/studio/web/assets/eventmodeling-K75KTNOO-D0dJ_PbR.js +1 -0
- package/studio/web/assets/flowDiagram-OXPTDLAJ-C1LEc_Hf.js +1 -0
- package/studio/web/assets/{ganttDiagram-FUAMR5RP-bloX3Voy.js → ganttDiagram-R7TSEDQI-85F44BIo.js} +4 -4
- package/studio/web/assets/{getAnalytics-C0FHjtdQ.js → getAnalytics-B1xpd4kG.js} +1 -1
- package/studio/web/assets/gitGraph-VSP46ZUC-BBmqrmjK.js +1 -0
- package/studio/web/assets/gitGraphDiagram-XJZIOB7I-CFY6iMwg.js +106 -0
- package/studio/web/assets/graphlib-DS17s2tU.js +1 -0
- package/studio/web/assets/{html-B1iA8q38.js → html-c4OKnEF1.js} +1 -1
- package/studio/web/assets/{htmlMode-CrL3YO5Y.js → htmlMode-yXjqUsW9.js} +1 -1
- package/studio/web/assets/index-CDnn0-PN.css +1 -0
- package/studio/web/assets/index-CIK9Nhgo.js +833 -0
- package/studio/web/assets/index.lazy-DQpKL_QQ.js +1 -0
- package/studio/web/assets/info-OHQRW6UA-Bs2aEco7.js +1 -0
- package/studio/web/assets/{infoDiagram-VRGFBTTK-B0J1132p.js → infoDiagram-5W2HQ5XZ-d6yG1yfZ.js} +1 -1
- package/studio/web/assets/invalidateEntityQueries-BkWXiqjz.js +1 -0
- package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-BNk0tuQx.js → ishikawaDiagram-K3B6WC7H-CSYz-WRA.js} +2 -2
- package/studio/web/assets/{javascript-CXgQMkoa.js → javascript-CP1WPkLu.js} +1 -1
- package/studio/web/assets/{journeyDiagram-ZHPQQLJL-Be79RMQM.js → journeyDiagram-COXZFDF6-BDj6Wqcj.js} +3 -3
- package/studio/web/assets/{jsonMode-DycqRKOv.js → jsonMode-BWsIHqbb.js} +1 -1
- package/studio/web/assets/{kanban-definition-PNTS6WVX-4uuoQP9W.js → kanban-definition-P3RFRI5V-C2Bjfbp-.js} +6 -6
- package/studio/web/assets/{languageServices-HYHV1wDJ.js → languageServices-Ca72n4Ql.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-CH_6SYiA.js → lspLanguageFeatures-BQdsSDeh.js} +1 -1
- package/studio/web/assets/{mermaid-parser.core-V1UYMeXV.js → mermaid-parser.core-Ch_T61iL.js} +6 -6
- package/studio/web/assets/mermaid.core-BOAgKzo5.js +44 -0
- package/studio/web/assets/{mindmap-definition-NLK3R4M7-C70Ctm0J.js → mindmap-definition-LZFPQGKD-DzOapzMM.js} +3 -3
- package/studio/web/assets/notifications-BYgO5-yk.js +1 -0
- package/studio/web/assets/notifications-DsGk-TnW.js +1 -0
- package/studio/web/assets/operations-Dy69AlTr.js +4 -0
- package/studio/web/assets/packet-JDAUHWVQ-CGHB_EIE.js +1 -0
- package/studio/web/assets/{pegDiagram-GJSIUBJH-d6z1cc7W.js → pegDiagram-BGZESJAR-ADzOIKrz.js} +1 -1
- package/studio/web/assets/pie-XZMESJXO-Bijz5Y2o.js +1 -0
- package/studio/web/assets/pieDiagram-CAPJLFHJ-VM7fvbwO.js +39 -0
- package/studio/web/assets/profile-DzQvoYBF.js +1 -0
- package/studio/web/assets/{quadrantDiagram-O4NWA36T-CIF5Qkou.js → quadrantDiagram-GDMTTRAM-Bmza84_j.js} +3 -3
- package/studio/web/assets/radar-ABXABTNO-IaC8pB63.js +1 -0
- package/studio/web/assets/railroad-I3PHUGI6-CCrSUqeq.js +1 -0
- package/studio/web/assets/railroad-abnf-LNEFI6M7-BhobVp-t.js +1 -0
- package/studio/web/assets/railroad-ebnf-DWYD2UWJ-OHhGUTEh.js +1 -0
- package/studio/web/assets/railroad-peg-JR7BVNR7-FupC3Jcn.js +1 -0
- package/studio/web/assets/{railroadDiagram-XR7U4H2S-y3Jv2U6N.js → railroadDiagram-SM67HX2B-CgFGOVI5.js} +1 -1
- package/studio/web/assets/regions-Sv_IN7XA.js +1 -0
- package/studio/web/assets/{register-DCN4QUii.js → register-AM2_79FO.js} +3 -3
- package/studio/web/assets/request-BJj8dbdU.js +2 -0
- package/studio/web/assets/{requirementDiagram-PLB6GJNP-ChINZEmu.js → requirementDiagram-X7JNWC4A-BHoQgkoT.js} +4 -4
- package/studio/web/assets/restartGate-D_e93lFE.js +1 -0
- package/studio/web/assets/restartUnconfirmed-BqcDZ__Y.js +1 -0
- package/studio/web/assets/sankeyDiagram-UM26HJRW-8tFu_oUe.js +40 -0
- package/studio/web/assets/sequenceDiagram-ZO4K6R2Y-jryb1rwT.js +169 -0
- package/studio/web/assets/setComponentFile-BCnAnxjR.js +1 -0
- package/studio/web/assets/setup-Ca4RpPzm.js +2 -0
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-Bs8RIb9K.js → stateDiagram-v2-TBUQTH76-CnWE5yFn.js} +2 -2
- package/studio/web/assets/status-D05eZ5pQ.js +61 -0
- package/studio/web/assets/{swimlanes-2SLR337P-B52xrf6t.js → swimlanes-N4OXWK64-CBlFzDJn.js} +1 -1
- package/studio/web/assets/swimlanesDiagram-K3J5GTZL-DvFLtUPi.js +8 -0
- package/studio/web/assets/{timeline-definition-EJHVYXUP-DJSITpQX.js → timeline-definition-YOQAKGHF-CFWqHrqI.js} +4 -4
- package/studio/web/assets/{toggleHighContrast-CfAp6Sxb.js → toggleHighContrast-JRkLfyfb.js} +1 -1
- package/studio/web/assets/treeView-D4JQ5SDB-CpV9zuVb.js +1 -0
- package/studio/web/assets/treemap-SAJKECNS-Bc94Tqiq.js +1 -0
- package/studio/web/assets/{tsMode-HZR0bfq4.js → tsMode-C_-eSRVR.js} +1 -1
- package/studio/web/assets/{typescript-UMJSfZyF.js → typescript-BYXigM_n.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-B379CS-h.js → useEntityRestURL-HHmjsRm9.js} +1 -1
- package/studio/web/assets/usecaseDiagram-VIAY4XPW-Brf2xP3S.js +328 -0
- package/studio/web/assets/{vendor-core-6LhfJlRC.js → vendor-core-PejxhUWQ.js} +1 -1
- package/studio/web/assets/vendor-datadog-GjET_Cbn.js +6 -0
- package/studio/web/assets/{vendor-react-BOliS0BL.js → vendor-react-DSIx92Ld.js} +1 -1
- package/studio/web/assets/vendor-tanstack-C5Y-nrd2.js +2 -0
- package/studio/web/assets/vendor-ui-BBWHOEoQ.js +41 -0
- package/studio/web/assets/{vennDiagram-UO4OBE2U-DZTsDA4k.js → vennDiagram-BLWOH2XV-XxOyJPq_.js} +3 -3
- package/studio/web/assets/wardley-7MLQ67FV-DRW0zuE4.js +1 -0
- package/studio/web/assets/{wardleyDiagram-VNRHLVJA-qu_f7Tvl.js → wardleyDiagram-YMQ3BMBF-B3WskJ9W.js} +3 -3
- package/studio/web/assets/wasAReleasedBeforeB-D5grAaGM.js +1 -0
- package/studio/web/assets/{workers-Bn_0_L1R.js → workers-DY0ka4Y7.js} +1 -1
- package/studio/web/assets/{xml-CG2N-1r1.js → xml-CdGINpmN.js} +1 -1
- package/studio/web/assets/xychartDiagram-TAQBALBS-D5NV4Env.js +7 -0
- package/studio/web/assets/{yaml-68pda6Av.js → yaml-DYeF07_w.js} +1 -1
- package/studio/web/index.html +17 -15
- package/utility/npmUtilities.ts +17 -7
- package/studio/web/assets/Chat-CrxPD4ph.js +0 -2294
- package/studio/web/assets/FloatingChat-EXzoUQ-9.js +0 -23
- package/studio/web/assets/SearchDialog-7qRL8Yqx.js +0 -1
- package/studio/web/assets/_basePickBy-QJXUw_pr.js +0 -1
- package/studio/web/assets/_baseUniq-DR4vVjyC.js +0 -1
- package/studio/web/assets/alertDialog-BbWD9Xxa.js +0 -1
- package/studio/web/assets/applications-CdqjiSQe.js +0 -296
- package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +0 -1
- package/studio/web/assets/arrow-up-D2g_57vT.js +0 -1
- package/studio/web/assets/authStore-CJCaADw9.js +0 -3
- package/studio/web/assets/button-DWwfgGWI.js +0 -21
- package/studio/web/assets/channel-D0AkPjJ8.js +0 -1
- package/studio/web/assets/chunk-3FUC2YCW-DuRp-j19.js +0 -2
- package/studio/web/assets/chunk-7M6MHVWA-h6WlaTzO.js +0 -213
- package/studio/web/assets/chunk-FOHPRMQF-DzxwRta7.js +0 -161
- package/studio/web/assets/chunk-O7XYJQB3-CBS3nZYV.js +0 -128
- package/studio/web/assets/chunk-Z7XXMR3K-DSHbImdf.js +0 -10
- package/studio/web/assets/cn-6T8VranJ.js +0 -1
- package/studio/web/assets/cynefin-OW5HDTMX-DIvbXLSj.js +0 -1
- package/studio/web/assets/cynefinDiagram-VND7K2PF-BdtaXiCE.js +0 -62
- package/studio/web/assets/dagre-6A5THRUB-C7RUWHyz.js +0 -4
- package/studio/web/assets/diagram-ATOU4E4O-dxRfeG4B.js +0 -3
- package/studio/web/assets/diagram-MPIPVDR6-7prveD1B.js +0 -41
- package/studio/web/assets/dist-BrRuMUzt.js +0 -11
- package/studio/web/assets/download-Cn6tPG4L.js +0 -1
- package/studio/web/assets/elk-276RUBZZ-DxDQPjQT.js +0 -27
- package/studio/web/assets/errorText-NpBmvk9I.js +0 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +0 -1
- package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +0 -1
- package/studio/web/assets/gitGraph-4MIJSDKK-BrXWmJt2.js +0 -1
- package/studio/web/assets/gitGraphDiagram-X574FWY7-C2KnnZeY.js +0 -106
- package/studio/web/assets/graphlib-C9Tl7QrK.js +0 -1
- package/studio/web/assets/index-QLZw6xrU.css +0 -1
- package/studio/web/assets/index-qCLyhyah.js +0 -828
- package/studio/web/assets/index.lazy-DCx-DxIV.js +0 -2
- package/studio/web/assets/info-A6RAGUB7-BhzBHe4U.js +0 -1
- package/studio/web/assets/mermaid.core-1PX6R49U.js +0 -44
- package/studio/web/assets/notifications-BSObVIY1.js +0 -1
- package/studio/web/assets/notifications-DFLXk68b.js +0 -1
- package/studio/web/assets/packet-AYTQ26CC-DWbXHx5g.js +0 -1
- package/studio/web/assets/pie-WAS4IAKB-DjoTm3yr.js +0 -1
- package/studio/web/assets/pieDiagram-5QR66LMP-Dz3RHpDA.js +0 -39
- package/studio/web/assets/profile-TBjzOMIy.js +0 -1
- package/studio/web/assets/radar-RG4KPBEZ-CVBgjCOj.js +0 -1
- package/studio/web/assets/railroad-74A4TZTK-BdhXw10Q.js +0 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +0 -1
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +0 -1
- package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +0 -1
- package/studio/web/assets/regions-C4an4H7e.js +0 -1
- package/studio/web/assets/restartGate-nHAFLDh-.js +0 -1
- package/studio/web/assets/sankeyDiagram-IPEJSGJF-D9AyLce2.js +0 -40
- package/studio/web/assets/sequenceDiagram-PO4LG4MO-hfk4aIEm.js +0 -169
- package/studio/web/assets/setComponentFile-BrqOM1Zm.js +0 -1
- package/studio/web/assets/setup-0v2MCm-E.js +0 -2
- package/studio/web/assets/status-DdezofUu.js +0 -61
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +0 -8
- package/studio/web/assets/tabs-BCTQzoQK.js +0 -1
- package/studio/web/assets/treeView-Q6P3EWNA-C-epdxRo.js +0 -1
- package/studio/web/assets/treemap-WGGIJYW6-DOVyp_zw.js +0 -1
- package/studio/web/assets/usecaseDiagram-POWQR4AR-B6252cbq.js +0 -328
- package/studio/web/assets/vendor-datadog-ozKf-pts.js +0 -6
- package/studio/web/assets/vendor-tanstack-Bntfka-M.js +0 -2
- package/studio/web/assets/vendor-ui-9EYYDgoN.js +0 -41
- package/studio/web/assets/wardley-WFR3VGLG-Cfqoe6z4.js +0 -1
- package/studio/web/assets/xychartDiagram-PMCCYNJV-UrOKRsmR.js +0 -7
|
@@ -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()`
|
|
41
|
-
timer
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
1070
|
-
> (the `hasLeaseProtectedWrite` fence in `DatabaseTransaction.performCommit()`)
|
|
1071
|
-
>
|
|
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
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
package/resources/recordLock.ts
CHANGED
|
@@ -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)`
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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:
|
|
242
|
-
// stalled holder does not keep a key every participant has
|
|
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
|
-
//
|
|
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
|
-
|
|
298
|
-
if (this.released) return;
|
|
299
|
-
this
|
|
300
|
-
|
|
301
|
-
|
|
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?.(
|
|
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
|
-
|
|
335
|
-
this
|
|
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.#
|
|
409
|
+
this.#unlockAndEmitRelease('record lock release failed (store may have been dropped)');
|
|
343
410
|
return true;
|
|
344
411
|
}
|
|
345
412
|
|
package/server/DESIGN.md
CHANGED
|
@@ -73,6 +73,11 @@ A request entering `http.ts` does **not** go through Fastify unless no Harper ha
|
|
|
73
73
|
> all. Only mutating an object _without_ changing its version can serve stale bytes, and that is
|
|
74
74
|
> indistinguishable from re-sending the same message.
|
|
75
75
|
|
|
76
|
+
> **Pass-through payloads are `Buffer | string`.** A message that carries its own `{ contentType, data }`
|
|
77
|
+
> is delivered as its `data`, but binary data leaves `serializeMessage` as a `Buffer`: on a worker other
|
|
78
|
+
> than the publisher's, the record is decoded from the transaction log and `data` is a plain `Uint8Array`,
|
|
79
|
+
> which mqtt-packet cannot encode. Pinned by `integrationTests/mqtt/opaque-payload-across-workers.test.ts`.
|
|
80
|
+
|
|
76
81
|
### Threads
|
|
77
82
|
|
|
78
83
|
| File | Purpose |
|
|
@@ -353,11 +358,13 @@ The `@table(cacheControl:)` value is persisted on the primary-key attribute (lik
|
|
|
353
358
|
|
|
354
359
|
A persisted QoS 1/2 entry is `{ qos, topic, startTime, databaseGeneration? }`, and the record carries the `incarnation` of the connection that owns it (harper#2448). A QoS 0 entry is `{ qos, topic }`: it is session state with no position, resumed live with retain handling 2 and no acknowledgement tracking.
|
|
355
360
|
|
|
356
|
-
- **Positions are certified, never clocks.** A new subscription starts at its registration watermark (`registeredThrough`), bound to the database generation when one exists, and otherwise at `getNextMonotonicTime()` unbound until the first certified position replaces it. A SUBSCRIBE that replaces a QoS 1/2 topic's subscription continues that topic's position through the checked replay, since replacing must lose nothing; a pruned-history refusal there resets the session as it would at reconnect. A topic's position is `min(subscription.progress(), previousKey of its oldest unacknowledged delivery)`; a transaction's messages share a key, so one unacked message holds the position before its whole transaction, and while a message is still queued the position stays before the last delivered transaction, which that message may share. Live events arrive in commit order, and a RocksDB transaction takes its key before it commits, so a transaction can arrive after a higher key. Its previousKey is then the position just below its own key, and while such a late delivery is unacknowledged the position takes the lowest previousKey of all of them rather than the oldest's (harper#3101). Scan deliveries (`fromScan`) stay out of this ordering: they are retained state, which a resumed session does not resend.
|
|
361
|
+
- **Positions are certified, never clocks.** A new subscription starts at its registration watermark (`registeredThrough`), bound to the database generation when one exists, and otherwise at `getNextMonotonicTime()` unbound until the first certified position replaces it. A SUBSCRIBE that replaces a QoS 1/2 topic's subscription continues that topic's position through the checked replay, since replacing must lose nothing; a pruned-history refusal there resets the session as it would at reconnect. A topic's position is `min(subscription.progress(), previousKey of its oldest unacknowledged delivery)`; a transaction's messages share a key, so one unacked message holds the position before its whole transaction, and while a message is still queued the position stays before the last delivered transaction, which that message may share. Live events arrive in commit order, and a RocksDB transaction takes its key before it commits, so a transaction can arrive after a higher key. Its previousKey is then the position just below its own key, and while such a late delivery is unacknowledged the position takes the lowest previousKey of all of them rather than the oldest's (harper#3101). Scan deliveries (`fromScan`) stay out of this ordering: they are retained state, which a resumed session does not resend: `resumeTopics` subscribes with retain handling 2 (`addSubscription` omits current values for that or an explicit `omitCurrent`), since a record topic whose saved position covers its current version would otherwise send that version again.
|
|
357
362
|
- **Checkpoints:** coalesced after acknowledgements, on an unref'd timer (`min(auditRetention / 10, 1h)`), and at disconnect, where positions are taken before subscriptions end because `close()` clears a queue and would read as idle. Nothing past a resumed position is saved until that subscription's `resumeVerified` resolves true. Out-of-order acks are not persisted: a partly acked transaction is redelivered. Legacy `acks` lists are ignored.
|
|
358
363
|
- **Before CONNACK,** a collection entry bound to its database's current generation is checked against the floor (a record entry's own walk decides after CONNACK); a failure deletes the record and reports the session as not present. The connection then writes its incarnation (`takeOver`), and every later write first checks the stored incarnation: a connection that finds another's stops writing and closes (0x8E), and one resumed from a record never creates one. Any newer authenticated connection for the client id on the same thread, a clean start included, closes the older durable connection the same way before it reads or deletes the record, once that connection has saved its positions, and waits for that save to settle; a connection that has already closed counts until its last save settles. A save that failed leaves the record at the last one that landed, so the newer connection resumes from there and may redeliver, never skip. Simultaneous CONNECTs for one client id take their turn through a per-thread claim, so each sees the one before it; one that gives up waiting keeps its place until the one before it finishes. The CONNECT's pending session is assigned before it waits, so packets sent right behind the CONNECT wait with it rather than being refused; the wait is bounded (10 s, then CONNACK 0x88, or 0x03 on v3.1.1), and a CONNECT whose client closes while it waits takes nothing over. A session that has ended, by a disconnect or by yielding to a takeover, saves nothing after its final save, so a SUBSCRIBE still in flight cannot write over the record a newer connection claimed.
|
|
359
364
|
- **Subscription changes are serial.** Packets are handled concurrently, so a durable session applies SUBSCRIBE, UNSUBSCRIBE and `resume()` one at a time: a topic's state belongs to one subscription. A SUBSCRIBE of any QoS is saved before SUBACK. A failed SUBSCRIBE leaves its topic unsubscribed and saves that. It ends the subscription it started, and drops the state of the one it replaced, which replacing ended. An UNSUBSCRIBE whose save fails is reported failed again on a retry until the removal is saved, and any failed save starts the checkpoint timer, so a session with only QoS 0 topics retries it too.
|
|
365
|
+
- **A delivery that cannot be sent ends the connection, not just the session.** The listener's `catch` in `mqtt.ts` aborts the transport (`destroy`/`terminate`, as for a parse error) and `onClose` ends the session, so the client sees the drop and reconnects. Ending only the session, as before, left the client connected and receiving nothing. An abort rather than a graceful end, because teardown on a graceful TCP end waits for the peer to close its side. A delivery that fails every time therefore repeats on each reconnect (a durable QoS≥1 session replays the unacknowledged message).
|
|
360
366
|
- **Wills are kept per connection,** keyed `[clientId, incarnation]`: a connection closing after a takeover publishes and deletes only its own will (MQTT 5 §3.1.4 publishes a taken-over client's will), never the will of the connection that replaced it.
|
|
367
|
+
- **Only a will's publish is authorized** (`settleWill`, harper#1874). The `hdb_session_will` row is the session's bookkeeping: it is read and deleted on contexts of its own, not the session's `authorize: true` context, which spends its one-shot check on the first call (roles are not granted system tables, so a non-super user's will never published and its row leaked). The publish then runs on a fresh session context, as the connection's user or a scoped will's embedded role, and the row is deleted after any outcome, a refused or failed publish included, so a will that fails for a transient reason is lost: the boot sweep, the only retry path, would replay a kept row only at the next restart, with no liveness check (harper#3034). The publish commits before the delete, so a crash between them leaves the row for that sweep and the will may be published twice (by design; untested). `unitTests/server/sessionWill.test.js` and `integrationTests/mqtt/will-authorization.test.ts` enforce that only the publish is authorized and that the row is deleted after a publish, a refusal and a clean DISCONNECT.
|
|
361
368
|
- **One terminal path** (`subscriptionFailed`): a refused resume subscribe, a final `Error` value (observed through the subscription's `close` as well as the consumer loop, which may be parked on socket back-pressure), or an iterator failure. Only a 410 (pruned history) discards the session — writers fenced, siblings ended, a v5 DISCONNECT (0x83, the client-error message when problem information is allowed) and a socket close that never waits on storage, then a deletion only if the record still carries this incarnation. Anything else, a 409 from a restored or copied database included, saves positions and closes the connection so the client resumes.
|
|
362
369
|
- **Only the current generation is checked.** A position bound to another generation, another node's (`hdb_durable_session` replicates, and each node mints its own generations) or this database's before a restore or copy, resumes best-effort: it is not checked before CONNACK, replays unchecked from where it is, and binds again at the first certified position. Until a live write gives it a watermark, that replay saves no position, so a reconnect on a quiet database replays it again.
|
|
363
370
|
- **Not covered:** concurrent connections for one client id on different threads, together with crash replay of a failed conditional write (harper#2940); late commits below a saved position, including a late transaction still queued behind the message being sent, which the session cannot see (harper#2928); a replay from another generation, which is unchecked.
|
|
@@ -647,6 +654,7 @@ The SQL and job paths are additive rather than exclusive: `verifyPermsAST` valid
|
|
|
647
654
|
- **Headers commit exactly once, through `this.writeHead`.** `write()`, `end()`, `flushHeaders()` and `_implicitHeader()` all reach `this.writeHead(this.statusCode)` by property lookup, so a `writeHead` that `on-headers` replaced on the instance runs its listeners (the ones that set `Content-Encoding` and remove `Content-Length`) before the promise resolves. After commit, `setHeader`/`appendHeader`/`removeHeader` and a second `writeHead` throw `ERR_HTTP_HEADERS_SENT` as Node's do; `_header` (which `compression` ≤ 1.7 tests instead of `headersSent`) and `finished` (which `on-finished` tests) derive from that state.
|
|
648
655
|
- **The adapter owns the `'error'` listener.** A `destroy(err)` right after `writeHead()` emits before the awaiting caller can attach one; the error stays in the stream's `errored` state for `pipeline()`, `finished()` or async iteration. Client disconnect (`Request.signal`) destroys the response without an error after headers (a plain premature close, which `pipeBodyToResponse` treats as routine) and rejects the promise with the abort reason before them; a handler that throws or rejects before ending the response destroys it, and one that fails after `end()` is logged at warn.
|
|
649
656
|
- **Header names are case-insensitive on removal too.** `Headers.delete` lowercases like `set`/`get`/`has`; the inherited `Map.delete` silently left `send`'s `Content-Length` on a gzip body (truncated transfers).
|
|
657
|
+
- **Every piped chunk reaches `write()` by property lookup.** Node 26.11's `Readable.pipe` fast path (nodejs/node#66182) calls `_write()` directly when pipe's listener is a byte-mode source's only `'data'` listener, so `compression`'s instance patch of `res.write` was skipped for most chunks of `send(...).pipe(res)`: raw bytes inside the gzip stream, or a stall once `compression` had routed pipe's `'drain'` to its idle zlib stream. On runtimes that have the fast path (`'writeKnownBuffer' in Writable`, a static it added) the response adds an inert `'data'` listener to each byte-mode core `Readable` on `'pipe'`, which fails the fast path's single-listener check, and removes it on `'unpipe'` through a listener prepended in the constructor: pipe's own cleanup resumes a source awaiting `'drain'` while any `'data'` listener remains, which would leave an unpiped source flowing into nothing. Object-mode and decoded sources, and legacy `Stream.prototype.pipe` (which never emits `'unpipe'`), are skipped. Remove it once every supported Node 26 release dispatches pipe through an overridden `write()`.
|
|
650
658
|
- **Express is not a target.** `express`'s `app.handle()` replaces the response's prototype with one rooted at `http.ServerResponse.prototype`, which no Writable-derived response survives, and would do the same to the request `Proxy`'s target, Harper's real `IncomingMessage`. Middleware that duck-types the response (Next.js, `compression`, `send`, `serve-static`, `finalhandler`, h3, fastify) is the supported surface.
|
|
651
659
|
|
|
652
660
|
## `manageThreads` has two different `workerCount`s (`server/threads/manageThreads.js`)
|
|
@@ -254,13 +254,13 @@ class SubscriptionsSession {
|
|
|
254
254
|
if (path.indexOf('.') > -1) throw new Error('Dots are not allowed in topic names');
|
|
255
255
|
// might be faster to somehow modify existing subscription and re-get the retained record, but this should work for now
|
|
256
256
|
const existingSubscription = this.subscriptions.find((subscription) => subscription.topic === topic);
|
|
257
|
-
let omitCurrent;
|
|
257
|
+
let omitCurrent = subscriptionRequest.omitCurrent === true;
|
|
258
258
|
if (existingSubscription) {
|
|
259
|
-
omitCurrent
|
|
259
|
+
omitCurrent ||= retainHandling > 0;
|
|
260
260
|
existingSubscription.end();
|
|
261
261
|
this.subscriptions.splice(this.subscriptions.indexOf(existingSubscription), 1);
|
|
262
262
|
} else {
|
|
263
|
-
omitCurrent
|
|
263
|
+
omitCurrent ||= retainHandling === 2;
|
|
264
264
|
}
|
|
265
265
|
if (startTime) trace('Resuming subscription from', topic, 'from', startTime);
|
|
266
266
|
const entry = resources.getMatch(path, 'mqtt');
|
|
@@ -484,22 +484,7 @@ class SubscriptionsSession {
|
|
|
484
484
|
const willId = this.willId;
|
|
485
485
|
this.willId = undefined;
|
|
486
486
|
if (willId) {
|
|
487
|
-
|
|
488
|
-
transaction(context, async () => {
|
|
489
|
-
try {
|
|
490
|
-
if (!clientTerminated) {
|
|
491
|
-
const will = await getLastWill().get(willId);
|
|
492
|
-
if (will && !isWillFromExpiredScopedToken(will)) {
|
|
493
|
-
// A scoped will authorizes under its own embedded role, never the disconnecting
|
|
494
|
-
// session's user (which may be a later same-clientId reconnect).
|
|
495
|
-
const willContext = will.user?._scopedToken ? { ...context, user: will.user } : context;
|
|
496
|
-
await publishMessage(will, will.data, willContext);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
} finally {
|
|
500
|
-
await getLastWill().delete(willId);
|
|
501
|
-
}
|
|
502
|
-
}).catch((error) => {
|
|
487
|
+
this.settleWill(willId, !clientTerminated).catch((error) => {
|
|
503
488
|
warn(`Error publishing MQTT will for ${this.sessionId}`, error);
|
|
504
489
|
});
|
|
505
490
|
}
|
|
@@ -509,6 +494,23 @@ class SubscriptionsSession {
|
|
|
509
494
|
}
|
|
510
495
|
this.subscriptions = [];
|
|
511
496
|
}
|
|
497
|
+
/** The will row is session bookkeeping on contexts of its own; only the publish runs as the session (server/DESIGN.md). */
|
|
498
|
+
async settleWill(willId: [string, string], publish: boolean) {
|
|
499
|
+
try {
|
|
500
|
+
if (publish) {
|
|
501
|
+
const will = await getLastWill().get(willId, {});
|
|
502
|
+
if (will && !isWillFromExpiredScopedToken(will)) {
|
|
503
|
+
const context = this.createContext();
|
|
504
|
+
// A scoped will authorizes under its own embedded role, never the disconnecting
|
|
505
|
+
// session's user (which may be a later same-clientId reconnect).
|
|
506
|
+
if (will.user?._scopedToken) context.user = will.user;
|
|
507
|
+
await publishMessage(will, will.data, context);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} finally {
|
|
511
|
+
await getLastWill().delete(willId, {});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
512
514
|
receivedPacket() {
|
|
513
515
|
if (this.keepalive) {
|
|
514
516
|
clearTimeout(this.keepaliveTimer);
|
|
@@ -670,11 +672,8 @@ export class DurableSubscriptionsSession extends SubscriptionsSession {
|
|
|
670
672
|
const { qos, topic, startTime, databaseGeneration } = state.entry;
|
|
671
673
|
const durable = qos > 0;
|
|
672
674
|
try {
|
|
673
|
-
// retain handling, not omitCurrent, decides whether a subscription starts with current values
|
|
674
675
|
await this.resumeSubscription(
|
|
675
|
-
durable
|
|
676
|
-
? { omitCurrent: true, topic, qos, startTime, databaseGeneration, reportProgress: true }
|
|
677
|
-
: { topic, qos, rh: 2 },
|
|
676
|
+
durable ? { topic, qos, rh: 2, startTime, databaseGeneration, reportProgress: true } : { topic, qos, rh: 2 },
|
|
678
677
|
durable
|
|
679
678
|
);
|
|
680
679
|
} catch (error) {
|
package/server/mqtt.ts
CHANGED
|
@@ -206,6 +206,19 @@ const SERVER_SHUTTING_DOWN = 0x8b; // MQTT v5 DISCONNECT reason code
|
|
|
206
206
|
const REASON_STRING_LIMIT = 256; // bytes
|
|
207
207
|
/** Fixed header, message id and reason code, plus the property identifier and its length prefix. */
|
|
208
208
|
const ACK_PACKET_OVERHEAD = 16;
|
|
209
|
+
/** `AccessViolation` is 401 for a request with no user and 403 for a user lacking permission; an MQTT client sees both as "Not authorized". */
|
|
210
|
+
const isNotAuthorized = (error: any) => error?.statusCode === 401 || error?.statusCode === 403;
|
|
211
|
+
/** Only failures this layer maps to a code of their own are safe to describe: any other carries an internal message. */
|
|
212
|
+
const hasClientSafeMessage = (error: any) => isNotAuthorized(error) || error?.statusCode === 404;
|
|
213
|
+
/** REASON_STRING_LIMIT counts encoded bytes, and a character must not be cut in half. */
|
|
214
|
+
function boundedReasonString(message: unknown): string {
|
|
215
|
+
const text = String(message);
|
|
216
|
+
if (Buffer.byteLength(text) <= REASON_STRING_LIMIT) return text;
|
|
217
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
218
|
+
let end = REASON_STRING_LIMIT;
|
|
219
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
220
|
+
return bytes.subarray(0, end).toString('utf8');
|
|
221
|
+
}
|
|
209
222
|
|
|
210
223
|
/**
|
|
211
224
|
* Every live MQTT connection on this worker, so a shutdown can announce itself: a bare socket close
|
|
@@ -316,6 +329,8 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
316
329
|
}
|
|
317
330
|
}
|
|
318
331
|
let disconnected;
|
|
332
|
+
// set by a failed delivery, so sibling subscriptions stop writing before the aborted socket's 'close' arrives
|
|
333
|
+
let aborted = false;
|
|
319
334
|
numberOfConnections++;
|
|
320
335
|
const connection = {
|
|
321
336
|
protocolVersion: () => mqttOptions.protocolVersion,
|
|
@@ -327,17 +342,14 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
327
342
|
function closeConnection(error?: Error) {
|
|
328
343
|
try {
|
|
329
344
|
if (mqttOptions.protocolVersion >= 5) {
|
|
330
|
-
const packet: any = { cmd: 'disconnect', reasonCode: error ? 0x83 : 0x8e };
|
|
345
|
+
const packet: any = { cmd: 'disconnect', reasonCode: error ? (isNotAuthorized(error) ? 0x87 : 0x83) : 0x8e };
|
|
331
346
|
// as for PUBACK: only failures this layer describes itself are safe to hand a client
|
|
332
347
|
const describable =
|
|
333
348
|
error instanceof DatabaseGenerationChangedError ||
|
|
334
349
|
error instanceof ResumeHistoryUnavailableError ||
|
|
335
|
-
(error
|
|
336
|
-
(error as any)?.statusCode === 404;
|
|
350
|
+
hasClientSafeMessage(error);
|
|
337
351
|
const reasonString =
|
|
338
|
-
describable && error.message && sendProblemInformation
|
|
339
|
-
? Buffer.from(String(error.message), 'utf8').subarray(0, REASON_STRING_LIMIT).toString('utf8')
|
|
340
|
-
: undefined;
|
|
352
|
+
describable && error.message && sendProblemInformation ? boundedReasonString(error.message) : undefined;
|
|
341
353
|
if (
|
|
342
354
|
reasonString &&
|
|
343
355
|
(!maximumPacketSize || maximumPacketSize >= Buffer.byteLength(reasonString) + ACK_PACKET_OVERHEAD)
|
|
@@ -547,7 +559,14 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
547
559
|
});
|
|
548
560
|
const listener = async (topic, message, messageId, subscription, version) => {
|
|
549
561
|
try {
|
|
550
|
-
if (disconnected
|
|
562
|
+
if (disconnected || aborted) {
|
|
563
|
+
// a delivery already past the subscription when the client left or a failed delivery aborted
|
|
564
|
+
// the connection; onClose ends the session
|
|
565
|
+
mqttLog.debug?.(
|
|
566
|
+
`Session ${session?.sessionId} ${aborted ? 'aborted' : 'disconnected'} while trying to send message to ${topic}`
|
|
567
|
+
);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
551
570
|
const slashIndex = topic.indexOf('/', 1);
|
|
552
571
|
const generalTopic = slashIndex > 0 ? topic.slice(0, slashIndex) : topic;
|
|
553
572
|
const qos = subscription.qos || 0;
|
|
@@ -555,11 +574,12 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
555
574
|
// payload is encoded once per (message, content type) and reused across all of them.
|
|
556
575
|
const encoding = getSharedMessageEncoding(message, request, version);
|
|
557
576
|
const encoded = encoding.payload;
|
|
577
|
+
let payload: Buffer | string;
|
|
558
578
|
// only pay for a microtask when the serialization is genuinely still pending
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
579
|
+
if (typeof (encoded as any)?.then === 'function') {
|
|
580
|
+
payload = await resolveSharedPayload(encoding, message, request, version);
|
|
581
|
+
if (disconnected || aborted) return false;
|
|
582
|
+
} else payload = encoded as Buffer | string;
|
|
563
583
|
if (qos > 0) {
|
|
564
584
|
// mqtt-packet requires a numeric message identifier once qos is non-zero
|
|
565
585
|
const packetData: any = {
|
|
@@ -595,9 +615,11 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
595
615
|
return !rawSocket.closed;
|
|
596
616
|
} catch (error) {
|
|
597
617
|
mqttLog.error?.(error);
|
|
598
|
-
session
|
|
599
|
-
|
|
600
|
-
|
|
618
|
+
// Ending only the session would leave the client connected and receiving nothing. An abort,
|
|
619
|
+
// unlike a graceful end, closes without waiting on the peer, and onClose ends the session.
|
|
620
|
+
aborted = true;
|
|
621
|
+
if (socket.destroy) socket.destroy();
|
|
622
|
+
else socket.terminate();
|
|
601
623
|
return false;
|
|
602
624
|
}
|
|
603
625
|
};
|
|
@@ -606,6 +628,8 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
606
628
|
break;
|
|
607
629
|
case 'subscribe':
|
|
608
630
|
const granted = [];
|
|
631
|
+
// a SUBACK carries one reason string for the whole packet, so the first refusal's is the one sent
|
|
632
|
+
let refusalMessage: string | undefined;
|
|
609
633
|
for (const subscription of packet.subscriptions) {
|
|
610
634
|
let grantedQos;
|
|
611
635
|
try {
|
|
@@ -624,21 +648,27 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
624
648
|
grantedQos =
|
|
625
649
|
mqttOptions.protocolVersion < 5
|
|
626
650
|
? 0x80 // the only error code in v3.1.1
|
|
627
|
-
: error
|
|
651
|
+
: isNotAuthorized(error)
|
|
628
652
|
? 0x87 // unauthorized
|
|
629
653
|
: error.statusCode === 404
|
|
630
654
|
? 0x8f // invalid topic
|
|
631
655
|
: 0x80; // generic failure
|
|
656
|
+
if (mqttOptions.protocolVersion >= 5 && error.message && hasClientSafeMessage(error))
|
|
657
|
+
refusalMessage ??= error.message;
|
|
632
658
|
}
|
|
633
659
|
granted.push(grantedQos);
|
|
634
660
|
}
|
|
635
661
|
await session.committed;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
662
|
+
const subackPacket: any = { cmd: 'suback', granted, messageId: packet.messageId };
|
|
663
|
+
if (refusalMessage !== undefined && sendProblemInformation)
|
|
664
|
+
subackPacket.properties = { reasonString: boundedReasonString(refusalMessage) };
|
|
665
|
+
let suback = generate(subackPacket, mqttOptions);
|
|
666
|
+
// the result vector grows with the filter count, so only the encoded packet says whether the string fits
|
|
667
|
+
if (subackPacket.properties && maximumPacketSize && suback.length > maximumPacketSize) {
|
|
668
|
+
delete subackPacket.properties;
|
|
669
|
+
suback = generate(subackPacket, mqttOptions);
|
|
670
|
+
}
|
|
671
|
+
sendPacket(suback, 'suback');
|
|
642
672
|
break;
|
|
643
673
|
case 'unsubscribe': {
|
|
644
674
|
const granted = [];
|
|
@@ -691,23 +721,18 @@ function onSocket(socket, send, request, user, mqttSettings) {
|
|
|
691
721
|
reasonCode:
|
|
692
722
|
mqttOptions.protocolVersion < 5
|
|
693
723
|
? 0x80 // the only error code in v3.1.1
|
|
694
|
-
: error
|
|
724
|
+
: isNotAuthorized(error)
|
|
695
725
|
? 0x87 // not authorized
|
|
696
726
|
: error?.statusCode === 404
|
|
697
727
|
? 0x90 // topic name invalid
|
|
698
728
|
: 0x80, // unspecified error
|
|
699
729
|
};
|
|
700
|
-
// Only errors this layer maps to a code of their own are safe to describe: any other
|
|
701
|
-
// failure carries an internal message that a client must not be handed. Both limits
|
|
702
|
-
// are in encoded bytes, so measure and trim the string in bytes too.
|
|
703
730
|
const describable =
|
|
704
731
|
mqttOptions.protocolVersion >= 5 &&
|
|
705
732
|
sendProblemInformation &&
|
|
706
733
|
error?.message &&
|
|
707
|
-
(error
|
|
708
|
-
const reasonString = describable
|
|
709
|
-
? Buffer.from(String(error.message), 'utf8').subarray(0, REASON_STRING_LIMIT).toString('utf8')
|
|
710
|
-
: undefined;
|
|
734
|
+
hasClientSafeMessage(error);
|
|
735
|
+
const reasonString = describable ? boundedReasonString(error.message) : undefined;
|
|
711
736
|
if (
|
|
712
737
|
reasonString &&
|
|
713
738
|
(!maximumPacketSize || maximumPacketSize >= Buffer.byteLength(reasonString) + ACK_PACKET_OVERHEAD)
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
ServerResponse as NodeServerResponse,
|
|
7
7
|
} from 'node:http';
|
|
8
8
|
import type { Socket } from 'node:net';
|
|
9
|
-
import { PassThrough } from 'node:stream';
|
|
9
|
+
import { PassThrough, Readable, Writable } from 'node:stream';
|
|
10
10
|
import { Headers as ResponseHeaders, applyWriteHeadHeaders } from './Headers.ts';
|
|
11
11
|
|
|
12
12
|
export interface AdaptedResponse {
|
|
@@ -29,6 +29,20 @@ class UnsupportedResponseMethodError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const ignoreError = () => {};
|
|
32
|
+
const ignoreChunk = () => {};
|
|
33
|
+
|
|
34
|
+
// Node 26.11's pipe fast path (nodejs/node#66182) skips write() overrides such as compression's while pipe's listener
|
|
35
|
+
// is a byte-mode source's only 'data' listener; see server/DESIGN.md
|
|
36
|
+
const pipeSkipsWrite = 'writeKnownBuffer' in Writable;
|
|
37
|
+
function guardPipeSource(source: Readable) {
|
|
38
|
+
// object mode, a decoder, and legacy or userland pipe() never take the fast path; legacy pipe() never emits 'unpipe'
|
|
39
|
+
if (source instanceof Readable && !source.readableObjectMode && !source.readableEncoding)
|
|
40
|
+
source.on('data', ignoreChunk);
|
|
41
|
+
}
|
|
42
|
+
// must run before pipe's own unpipe cleanup, which resumes a source awaiting 'drain' while any 'data' listener remains
|
|
43
|
+
function releasePipeSource(source: unknown) {
|
|
44
|
+
if (source instanceof Readable) source.removeListener('data', ignoreChunk);
|
|
45
|
+
}
|
|
32
46
|
|
|
33
47
|
export class NodeAdapterResponse extends PassThrough implements NodeServerResponse {
|
|
34
48
|
statusCode = 200;
|
|
@@ -60,6 +74,10 @@ export class NodeAdapterResponse extends PassThrough implements NodeServerRespon
|
|
|
60
74
|
this.#resolve = resolve;
|
|
61
75
|
this.#reject = reject;
|
|
62
76
|
this.on('error', ignoreError);
|
|
77
|
+
if (pipeSkipsWrite) {
|
|
78
|
+
this.on('pipe', guardPipeSource);
|
|
79
|
+
this.prependListener('unpipe', releasePipeSource);
|
|
80
|
+
}
|
|
63
81
|
if (typeof nodeResponse?.on === 'function') {
|
|
64
82
|
const forward = (...args: any[]) => this.emit('timeout', ...args);
|
|
65
83
|
let forwarding = false;
|
|
@@ -536,7 +536,14 @@ export function serializeMessage(
|
|
|
536
536
|
request?: Request,
|
|
537
537
|
inAsyncContinuation?: boolean
|
|
538
538
|
): Buffer | string | Promise<Buffer | string> {
|
|
539
|
-
if (message?.contentType != null && message.data != null)
|
|
539
|
+
if (message?.contentType != null && message.data != null) {
|
|
540
|
+
const data = message.data;
|
|
541
|
+
// a record read on another thread decodes binary as a plain Uint8Array view, which protocol
|
|
542
|
+
// encoders (mqtt-packet) reject; wrap the same bytes rather than copy them
|
|
543
|
+
return ArrayBuffer.isView(data) && !Buffer.isBuffer(data)
|
|
544
|
+
? Buffer.from(data.buffer, data.byteOffset, data.byteLength)
|
|
545
|
+
: data;
|
|
546
|
+
}
|
|
540
547
|
asyncSerializations = inAsyncContinuation ? undefined : [];
|
|
541
548
|
try {
|
|
542
549
|
const serialized: Buffer | string = getMessageSerializer(request)(message);
|
|
@@ -162,8 +162,9 @@ function fill(encoding: SharedMessageEncoding, message: any, request: any) {
|
|
|
162
162
|
const payload = serializeMessage(message, request);
|
|
163
163
|
encoding.payload = payload;
|
|
164
164
|
encoding.failed = false;
|
|
165
|
-
// A pass-through payload IS the message's own data
|
|
166
|
-
// caching it retains nothing extra and must not be billed
|
|
165
|
+
// A pass-through payload IS the message's own data (or a Buffer over the same memory), already
|
|
166
|
+
// reachable from the WeakMap key, so caching it retains nothing extra and must not be billed
|
|
167
|
+
// against the budget.
|
|
167
168
|
if (encoding.serializer === PASS_THROUGH) return;
|
|
168
169
|
if (typeof (payload as any)?.then === 'function')
|
|
169
170
|
// collapse an async serialization in place so later subscribers, and frame sharing, see the
|