@harperfast/harper 5.3.2 → 5.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +75 -10
- package/dist/resources/DatabaseTransaction.js +1080 -277
- package/dist/resources/DatabaseTransaction.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 +16 -2
- 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 +20 -17
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/mqtt.js +41 -23
- 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/utility/npmUtilities.js +18 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/package.json +1 -1
- package/resources/DESIGN.md +42 -10
- package/resources/DatabaseTransaction.ts +1244 -275
- package/resources/Resource.ts +39 -14
- package/resources/RocksTransactionLogStore.ts +1 -0
- package/resources/Table.ts +24 -2
- 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 +2 -0
- package/server/DurableSubscriptionsSession.ts +18 -16
- package/server/mqtt.ts +39 -22
- package/server/serverHelpers/NodeAdapterResponse.ts +19 -1
- package/studio/web/assets/Chat-B-Rd-ft9.js +2100 -0
- package/studio/web/assets/FloatingChat-CFypO7kh.js +23 -0
- package/studio/web/assets/SearchDialog-BRIyPiBl.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-BraYnzvC.js} +1 -1
- package/studio/web/assets/applications-XCds8-ZP.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-BY1w91l8.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-D5mpVs1b.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-DiXKiahz.js} +1 -1
- package/studio/web/assets/{htmlMode-CrL3YO5Y.js → htmlMode-DitVJ707.js} +1 -1
- package/studio/web/assets/index-CDnn0-PN.css +1 -0
- package/studio/web/assets/index-D8kyF6dt.js +833 -0
- package/studio/web/assets/index.lazy-DgbjUxLo.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-BBVXeMDm.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-D85_j2oD.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-U3mWhpRL.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-CH_6SYiA.js → lspLanguageFeatures-CoymyM3c.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-CPgEC6ZP.js +1 -0
- package/studio/web/assets/notifications-D_VqhHnx.js +1 -0
- package/studio/web/assets/operations-D8aaKYty.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-DM5E6Xls.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-WFvXqryO.js +1 -0
- package/studio/web/assets/{register-DCN4QUii.js → register-B7DvfAGj.js} +3 -3
- package/studio/web/assets/request-ChoMERRk.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-BHk22Rh6.js +1 -0
- package/studio/web/assets/setup-wNdpTrVH.js +2 -0
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-Bs8RIb9K.js → stateDiagram-v2-TBUQTH76-CnWE5yFn.js} +2 -2
- package/studio/web/assets/status-CYoonm9V.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-CKEm-P4K.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-Dv35qHmn.js} +1 -1
- package/studio/web/assets/{typescript-UMJSfZyF.js → typescript-Bly1EW8o.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-B379CS-h.js → useEntityRestURL-Cg3P0MOb.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-DVtjrvUa.js} +1 -1
- package/studio/web/assets/{xml-CG2N-1r1.js → xml-DjAPbgLN.js} +1 -1
- package/studio/web/assets/xychartDiagram-TAQBALBS-D5NV4Env.js +7 -0
- package/studio/web/assets/{yaml-68pda6Av.js → yaml-oTUyoJy1.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
package/resources/Resource.ts
CHANGED
|
@@ -1099,7 +1099,7 @@ export function allowedMethods(resource: any) {
|
|
|
1099
1099
|
*/
|
|
1100
1100
|
function selectFromObject(object, propertyResolvers, context) {
|
|
1101
1101
|
// TODO: eventually we will do aggregate functions here
|
|
1102
|
-
const record = object.getRecord
|
|
1102
|
+
const record = typeof object.getRecord === 'function' ? object.getRecord() : undefined;
|
|
1103
1103
|
if (record) {
|
|
1104
1104
|
const ownData = object.getChanges?.();
|
|
1105
1105
|
return (property) => {
|
|
@@ -1109,25 +1109,36 @@ function selectFromObject(object, propertyResolvers, context) {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
if (ownData && property in ownData) {
|
|
1111
1111
|
return ownData[property];
|
|
1112
|
-
} else if (
|
|
1112
|
+
} else if (
|
|
1113
|
+
propertyResolvers &&
|
|
1114
|
+
Object.hasOwn(propertyResolvers, property) &&
|
|
1115
|
+
(resolver = propertyResolvers[property])
|
|
1116
|
+
) {
|
|
1113
1117
|
return resolver(object, context);
|
|
1114
1118
|
} else return record[property];
|
|
1115
1119
|
};
|
|
1116
1120
|
} else if (propertyResolvers) {
|
|
1117
1121
|
return (property) => {
|
|
1118
|
-
const resolver = propertyResolvers[property];
|
|
1119
|
-
|
|
1122
|
+
const resolver = Object.hasOwn(propertyResolvers, property) ? propertyResolvers[property] : undefined;
|
|
1123
|
+
if (resolver) return resolver(object, context);
|
|
1124
|
+
const value = object[property];
|
|
1125
|
+
return typeof value === 'function' ? undefined : value;
|
|
1126
|
+
};
|
|
1127
|
+
} else
|
|
1128
|
+
return (property) => {
|
|
1129
|
+
const value = object[property];
|
|
1130
|
+
return typeof value === 'function' ? undefined : value;
|
|
1120
1131
|
};
|
|
1121
|
-
} else return (property) => object[property];
|
|
1122
1132
|
}
|
|
1123
1133
|
export function transformForSelect(select, resource) {
|
|
1124
|
-
const propertyResolvers = resource
|
|
1125
|
-
const context = resource
|
|
1134
|
+
const propertyResolvers = resource?.propertyResolvers;
|
|
1135
|
+
const context = resource?.getContext?.();
|
|
1126
1136
|
let subTransforms;
|
|
1127
1137
|
if (typeof select === 'string')
|
|
1128
1138
|
// if select is a single string then return property value
|
|
1129
1139
|
return function transform(object) {
|
|
1130
|
-
if (object
|
|
1140
|
+
if (object == null || (!resource && typeof object !== 'object')) return object;
|
|
1141
|
+
if (typeof object.then === 'function') return object.then(transform);
|
|
1131
1142
|
if (Array.isArray(object)) return object.map(transform);
|
|
1132
1143
|
return selectFromObject(object, propertyResolvers, context)(select);
|
|
1133
1144
|
};
|
|
@@ -1135,7 +1146,8 @@ export function transformForSelect(select, resource) {
|
|
|
1135
1146
|
// if it is an array, return an array
|
|
1136
1147
|
if (select.asArray)
|
|
1137
1148
|
return function transform(object) {
|
|
1138
|
-
if (object
|
|
1149
|
+
if (object == null || (!resource && typeof object !== 'object')) return object;
|
|
1150
|
+
if (typeof object.then === 'function') return object.then(transform);
|
|
1139
1151
|
if (Array.isArray(object)) return object.map(transform);
|
|
1140
1152
|
const results = [];
|
|
1141
1153
|
const getProperty = handleProperty(selectFromObject(object, propertyResolvers, context));
|
|
@@ -1145,18 +1157,27 @@ export function transformForSelect(select, resource) {
|
|
|
1145
1157
|
return results;
|
|
1146
1158
|
};
|
|
1147
1159
|
const forceNulls = select.forceNulls;
|
|
1160
|
+
const selectsPrototype = select.some((property) => (property.name || property) === '__proto__');
|
|
1148
1161
|
return function transform(object) {
|
|
1149
|
-
if (object
|
|
1162
|
+
if (object == null || (!resource && typeof object !== 'object')) return object;
|
|
1163
|
+
if (typeof object.then === 'function') return object.then(transform);
|
|
1150
1164
|
if (Array.isArray(object))
|
|
1151
1165
|
return object.map((value) => (value && typeof value === 'object' ? transform(value) : value));
|
|
1152
1166
|
// finally the case of returning objects
|
|
1153
1167
|
const selectedData = {};
|
|
1154
1168
|
const getProperty = handleProperty(selectFromObject(object, propertyResolvers, context));
|
|
1169
|
+
if (selectsPrototype)
|
|
1170
|
+
Object.defineProperty(selectedData, '__proto__', {
|
|
1171
|
+
value: undefined,
|
|
1172
|
+
writable: true,
|
|
1173
|
+
enumerable: true,
|
|
1174
|
+
configurable: true,
|
|
1175
|
+
});
|
|
1155
1176
|
let promises;
|
|
1156
1177
|
for (const property of select) {
|
|
1157
1178
|
let value = getProperty(property);
|
|
1158
1179
|
if (value === undefined && forceNulls) value = null;
|
|
1159
|
-
if (value?.then) {
|
|
1180
|
+
if (typeof value?.then === 'function') {
|
|
1160
1181
|
if (!promises) promises = [];
|
|
1161
1182
|
promises.push(value.then((value) => (selectedData[property.name || property] = value)));
|
|
1162
1183
|
} else selectedData[property.name || property] = value;
|
|
@@ -1172,14 +1193,18 @@ export function transformForSelect(select, resource) {
|
|
|
1172
1193
|
} else if (typeof property === 'object') {
|
|
1173
1194
|
// TODO: Handle aggregate functions
|
|
1174
1195
|
if (property.name) {
|
|
1175
|
-
|
|
1196
|
+
const value = getProperty(property.name);
|
|
1197
|
+
if (!value || typeof value !== 'object' || value instanceof Date) return value;
|
|
1198
|
+
if (!subTransforms) subTransforms = Object.create(null);
|
|
1176
1199
|
// TODO: Get the resource, cache this transform, and apply above
|
|
1177
1200
|
let transform = subTransforms[property.name];
|
|
1178
1201
|
if (!transform) {
|
|
1179
|
-
const resource =
|
|
1202
|
+
const resource =
|
|
1203
|
+
propertyResolvers && Object.hasOwn(propertyResolvers, property.name)
|
|
1204
|
+
? propertyResolvers[property.name]?.definition?.tableClass
|
|
1205
|
+
: undefined;
|
|
1180
1206
|
transform = subTransforms[property.name] = transformForSelect(property.select || property, resource);
|
|
1181
1207
|
}
|
|
1182
|
-
const value = getProperty(property.name);
|
|
1183
1208
|
return transform(value);
|
|
1184
1209
|
} else return getProperty(property);
|
|
1185
1210
|
} else return property;
|
|
@@ -167,6 +167,7 @@ export class RocksTransactionLogStore extends EventEmitter {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
log.addEntry(entryBinary, options.transaction.id);
|
|
170
|
+
options.transaction.hasStagedLog = true;
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
/** A log holds one origin, which keeps `txnLogKey` unique within it; see resources/DESIGN.md. */
|
package/resources/Table.ts
CHANGED
|
@@ -4331,7 +4331,9 @@ export function makeTable(options): TableResourceClass {
|
|
|
4331
4331
|
return this;
|
|
4332
4332
|
}
|
|
4333
4333
|
/**
|
|
4334
|
-
* Release the lock this instance holds. Resolves true when this call cleared the native key lock
|
|
4334
|
+
* Release the lock this instance holds. Resolves true when this call cleared the native key lock;
|
|
4335
|
+
* false when it was already released, or when a commit of this lock's writes is still in flight
|
|
4336
|
+
* and the native key is returned once that commit settles.
|
|
4335
4337
|
* Works for both held (`{ hold: true }`) and transaction-scoped locks. After unlock() the
|
|
4336
4338
|
* instance is no longer lock-writable; writes through it require a fresh lock.
|
|
4337
4339
|
*/
|
|
@@ -4639,7 +4641,13 @@ export function makeTable(options): TableResourceClass {
|
|
|
4639
4641
|
}
|
|
4640
4642
|
},
|
|
4641
4643
|
before: writeToSource(),
|
|
4642
|
-
commit: (
|
|
4644
|
+
commit: (
|
|
4645
|
+
txnTime: number,
|
|
4646
|
+
existingEntry: Entry,
|
|
4647
|
+
retry: boolean,
|
|
4648
|
+
transaction: any,
|
|
4649
|
+
committedBy?: DatabaseTransaction
|
|
4650
|
+
) => {
|
|
4643
4651
|
// Whether a prior attempt of THIS write appended its own audit entry (sticky, set in
|
|
4644
4652
|
// save(); log entries are not part of the aborted rocks transaction, so they survive).
|
|
4645
4653
|
// Only such a write can find its own orphaned entry in the dedup lookups below and must
|
|
@@ -4730,6 +4738,19 @@ export function makeTable(options): TableResourceClass {
|
|
|
4730
4738
|
let belowAuditFloor = false;
|
|
4731
4739
|
let dedupVersionCouldBeRetained: (version: number) => boolean;
|
|
4732
4740
|
if (audit) {
|
|
4741
|
+
if (
|
|
4742
|
+
stagedOwnAuditEntry &&
|
|
4743
|
+
committedBy?.sourceApply &&
|
|
4744
|
+
isRocksDB &&
|
|
4745
|
+
!replaying &&
|
|
4746
|
+
precedesExisting === 0 &&
|
|
4747
|
+
txnLogKey === txnTime &&
|
|
4748
|
+
(existingEntry.nodeId ?? 0) === (options?.nodeId ?? getThisNodeId(auditStore)) &&
|
|
4749
|
+
!existingEntry.additionalAuditRefs?.length
|
|
4750
|
+
) {
|
|
4751
|
+
write.skipped = true;
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4733
4754
|
// A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
|
|
4734
4755
|
// its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
|
|
4735
4756
|
// out-of-order versions, read with read-your-writes consistency, so this skips the duplicate up
|
|
@@ -7932,6 +7953,7 @@ export function makeTable(options): TableResourceClass {
|
|
|
7932
7953
|
},
|
|
7933
7954
|
};
|
|
7934
7955
|
propertyResolvers.$highlights.directReturn = true;
|
|
7956
|
+
primaryKeyAttribute = undefined;
|
|
7935
7957
|
for (const attribute of this.attributes) {
|
|
7936
7958
|
if (attribute.isPrimaryKey) primaryKeyAttribute = attribute;
|
|
7937
7959
|
attribute.resolve = null; // reset this
|
|
@@ -36,3 +36,20 @@ Still open: `findLastAggregationTime` seeds both markers after a restart from th
|
|
|
36
36
|
analytics record, whose `time` is an end-of-cycle stamp, so a restart mid-drain can skip the rest of
|
|
37
37
|
the backlog; and `storeMetric` discards `table.put()`'s result, so the cursor advances past raw
|
|
38
38
|
records whose aggregate rows failed to commit. Both need a cursor persisted with the rows.
|
|
39
|
+
|
|
40
|
+
## Aggregation folds each measure over only the samples that carry it (`resources/analytics/write.ts`)
|
|
41
|
+
|
|
42
|
+
A key's raw samples need not carry the same measures: callback metrics (`recordAction(fn)`) and
|
|
43
|
+
analytics listeners report whatever fields they have, and a raw backlog can come from another
|
|
44
|
+
version. `aggregation()` keeps, per fold target (an action, or one thread's record for `byThread`
|
|
45
|
+
metrics), the count of samples that carried each measure in the cycle-local `measureCounts` map —
|
|
46
|
+
not on the action, which `storeMetric` persists and aggregate listeners receive. A measure's first
|
|
47
|
+
sample seeds it, `max[A-Z]`-named measures fold with `Math.max`, and the rest are a running mean
|
|
48
|
+
weighted by that count. A measure in every sample has a count equal to the target's `count`, so it
|
|
49
|
+
folds exactly as before; keep the running mean, since sum-then-divide rounds differently.
|
|
50
|
+
|
|
51
|
+
The `byThread` combination sums every thread record's numeric fields into the entry, so a measure
|
|
52
|
+
the key's first sample lacked, or that only some threads carry, is kept. That holds only while a
|
|
53
|
+
thread record carries nothing but measures and its `count`, which the thread count then replaces:
|
|
54
|
+
the fold keeps `total`/`ratio` on non-thread actions only, so a `ratio` on a thread record is
|
|
55
|
+
always a caller's measure.
|
|
@@ -967,6 +967,8 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
967
967
|
const aggregateActions = new Map();
|
|
968
968
|
const distributions = new Map();
|
|
969
969
|
const threadsToAverage = [];
|
|
970
|
+
// A measure's weight is the count of only the samples that carried it, per action or thread record.
|
|
971
|
+
const measureCounts = new Map<object, Record<string, number>>();
|
|
970
972
|
let lastTime: number | undefined;
|
|
971
973
|
let stoppedAtWindowEdge = false;
|
|
972
974
|
for (const { key, value } of rawAnalyticsTable.primaryStore.getRange({
|
|
@@ -992,7 +994,8 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
992
994
|
if (type !== undefined) key += '-' + type;
|
|
993
995
|
let action = aggregateActions.get(key);
|
|
994
996
|
if (action) {
|
|
995
|
-
|
|
997
|
+
const byThread = Boolean(action.byThread);
|
|
998
|
+
if (byThread) {
|
|
996
999
|
const actionForThread = action.threads[threadId];
|
|
997
1000
|
if (actionForThread) action = actionForThread;
|
|
998
1001
|
else {
|
|
@@ -1001,17 +1004,27 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
1001
1004
|
}
|
|
1002
1005
|
}
|
|
1003
1006
|
if (!action.count) action.count = 1;
|
|
1004
|
-
|
|
1007
|
+
let counts = measureCounts.get(action);
|
|
1008
|
+
if (!counts) {
|
|
1009
|
+
// Until its first fold, a target holds only its first sample.
|
|
1010
|
+
counts = Object.create(null);
|
|
1011
|
+
for (const measureName in action) {
|
|
1012
|
+
if (typeof action[measureName] === 'number') counts[measureName] = action.count;
|
|
1013
|
+
}
|
|
1014
|
+
measureCounts.set(action, counts);
|
|
1015
|
+
}
|
|
1005
1016
|
for (const measureName in measures) {
|
|
1006
1017
|
const value = measures[measureName];
|
|
1007
|
-
if (typeof value
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1018
|
+
if (typeof value !== 'number') continue;
|
|
1019
|
+
const measureCount = counts[measureName];
|
|
1020
|
+
if (measureCount === undefined) action[measureName] = value;
|
|
1021
|
+
else if (MAX_MEASURE_NAME.test(measureName)) action[measureName] = Math.max(action[measureName], value);
|
|
1022
|
+
else action[measureName] = (action[measureName] * measureCount + value * count) / (measureCount + count);
|
|
1023
|
+
counts[measureName] = (measureCount ?? 0) + count;
|
|
1012
1024
|
}
|
|
1013
1025
|
action.count += count;
|
|
1014
|
-
|
|
1026
|
+
// A thread's record is built from measures alone, so it has no total to add to.
|
|
1027
|
+
if (total >= 0 && !byThread) {
|
|
1015
1028
|
action.total += total;
|
|
1016
1029
|
action.ratio = action.total / action.count;
|
|
1017
1030
|
}
|
|
@@ -1043,15 +1056,15 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
1043
1056
|
let { path, method, type, metric, count, total, distribution, threads, ...measures } = entry;
|
|
1044
1057
|
threads = threads.filter((thread) => thread);
|
|
1045
1058
|
for (const measureName in measures) {
|
|
1046
|
-
if (typeof entry[measureName]
|
|
1047
|
-
|
|
1048
|
-
|
|
1059
|
+
if (typeof entry[measureName] === 'number') entry[measureName] = 0;
|
|
1060
|
+
}
|
|
1061
|
+
// A thread can carry measures the entry's first sample lacked.
|
|
1062
|
+
for (const thread of threads) {
|
|
1063
|
+
for (const measureName in thread) {
|
|
1049
1064
|
const value = thread[measureName];
|
|
1050
|
-
if (typeof value === 'number')
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1065
|
+
if (typeof value === 'number')
|
|
1066
|
+
entry[measureName] = (typeof entry[measureName] === 'number' ? entry[measureName] : 0) + value;
|
|
1053
1067
|
}
|
|
1054
|
-
entry[measureName] = total;
|
|
1055
1068
|
}
|
|
1056
1069
|
entry.count = threads.length;
|
|
1057
1070
|
delete entry.threads;
|
package/resources/databases.ts
CHANGED
|
@@ -654,6 +654,21 @@ const DROPPED_ROW_PREFIX = '/dropped/';
|
|
|
654
654
|
function droppedRowKey(tableName: string): string {
|
|
655
655
|
return DROPPED_ROW_PREFIX + tableName;
|
|
656
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Whether a catalog row names its table's primary store: one flagged `isPrimaryKey`, or the nameless bare
|
|
659
|
+
* `<table>/` row of a keyless table, which can lose the flag while its store still holds primary records.
|
|
660
|
+
*/
|
|
661
|
+
export function isPrimaryCatalogRow(key: unknown, value: any): boolean {
|
|
662
|
+
if (value?.isPrimaryKey) return true;
|
|
663
|
+
// exactly one slash, at the end: initStores reads `<table>/<attribute>` by splitting on the first slash
|
|
664
|
+
return (
|
|
665
|
+
value != null &&
|
|
666
|
+
value.name == null &&
|
|
667
|
+
typeof key === 'string' &&
|
|
668
|
+
key.length > 1 &&
|
|
669
|
+
key.indexOf('/') === key.length - 1
|
|
670
|
+
);
|
|
671
|
+
}
|
|
657
672
|
/**
|
|
658
673
|
* Once a name has been dropped, later creates of it take a generation even after the retirement journal is
|
|
659
674
|
* reclaimed. Keep the row untimed and shaped `{ table, tableId }`: later versions read it as an untimed drop
|
|
@@ -1325,7 +1340,7 @@ function initStores(
|
|
|
1325
1340
|
definedTables?.add(tableName);
|
|
1326
1341
|
let tableDef = tablesToLoad.get(tableName);
|
|
1327
1342
|
if (!tableDef) tablesToLoad.set(tableName, (tableDef = { attributes: [] }));
|
|
1328
|
-
if (
|
|
1343
|
+
if (isPrimaryCatalogRow(key, value)) tableDef.primary = value;
|
|
1329
1344
|
if (value.dropping) tableDef.tombstone = value;
|
|
1330
1345
|
if (attribute_name != null) tableDef.attributes.push(value);
|
|
1331
1346
|
Object.defineProperty(value, 'key', { value: key, configurable: true });
|
|
@@ -1505,7 +1520,8 @@ function initStores(
|
|
|
1505
1520
|
(attributesDbi as any).putSync(NEXT_TABLE_ID, tableId + 1);
|
|
1506
1521
|
(attributesDbi as any).putSync(primaryAttribute.key, primaryAttribute);
|
|
1507
1522
|
}
|
|
1508
|
-
|
|
1523
|
+
// primary by selection, not by its flag (isPrimaryCatalogRow)
|
|
1524
|
+
const dbiInit = createOpenDBIObject(false, true);
|
|
1509
1525
|
dbiInit.compression = primaryAttribute.compression;
|
|
1510
1526
|
if (dbiInit.compression) {
|
|
1511
1527
|
const compressionThreshold =
|
|
@@ -3671,6 +3687,25 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3671
3687
|
if (Table.primaryStore.rootStore.status === 'closed') {
|
|
3672
3688
|
throw new Error(`Can not use a closed data store from ${tableName} class`);
|
|
3673
3689
|
}
|
|
3690
|
+
if (Table.primaryKey == null) {
|
|
3691
|
+
// a keyless table cannot adopt a key without re-keying its records
|
|
3692
|
+
for (const attribute of attributes) {
|
|
3693
|
+
if (!attribute.isPrimaryKey) continue;
|
|
3694
|
+
if (origin === 'cluster')
|
|
3695
|
+
logger.warn(
|
|
3696
|
+
`Ignoring peer redefinition of ${databaseName}.${tableName}.${attribute.name} ` +
|
|
3697
|
+
`(isPrimaryKey: local table has no primary key, peer true)`
|
|
3698
|
+
);
|
|
3699
|
+
else if (schemaDefinedExplicit)
|
|
3700
|
+
logger.error(
|
|
3701
|
+
`Table '${databaseName}.${tableName}' has no primary key, so its declared primary key ` +
|
|
3702
|
+
`'${attribute.name}' is ignored and the table stays keyless. Recreate the table with that ` +
|
|
3703
|
+
`primary key, or migrate its records, to use it.`
|
|
3704
|
+
);
|
|
3705
|
+
delete attribute.isPrimaryKey;
|
|
3706
|
+
if (properties?.[attribute.name]) delete properties[attribute.name].primaryKey;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3674
3709
|
// Reject moving the primary key to a different attribute on a table that already has records.
|
|
3675
3710
|
// The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
|
|
3676
3711
|
// leave describe reporting the new attribute while every record — old and newly inserted — stays
|
|
@@ -5736,7 +5771,8 @@ function completeInterruptedDrop(
|
|
|
5736
5771
|
}>;
|
|
5737
5772
|
const bareEntry = catalogRows.find(({ key }) => key === tableName + '/');
|
|
5738
5773
|
const primaryEntry =
|
|
5739
|
-
|
|
5774
|
+
catalogRows.find(({ value }) => value?.isPrimaryKey) ??
|
|
5775
|
+
(bareEntry && isPrimaryCatalogRow(bareEntry.key, bareEntry.value) ? bareEntry : undefined);
|
|
5740
5776
|
const tombstoneEntry = bareEntry ?? primaryEntry;
|
|
5741
5777
|
if (rootStore instanceof RocksDatabase && !fullTextRetired) {
|
|
5742
5778
|
const names = [
|
|
@@ -5795,7 +5831,7 @@ function completeInterruptedDrop(
|
|
|
5795
5831
|
// recreate silently inherit the previous table's records.
|
|
5796
5832
|
for (const { key, value } of catalogRows) {
|
|
5797
5833
|
const objectStorage =
|
|
5798
|
-
value
|
|
5834
|
+
isPrimaryCatalogRow(key, value) || (value?.indexed?.type && CUSTOM_INDEXES[value.indexed.type]?.useObjectStore);
|
|
5799
5835
|
const store = (rootStore as any).openDB(key, createOpenDBIObject(!objectStorage, objectStorage) as any);
|
|
5800
5836
|
try {
|
|
5801
5837
|
// dropSync (not drop): this function is synchronous, and its callers rely on
|
|
@@ -37,14 +37,24 @@ Consequences that shape the code:
|
|
|
37
37
|
no-op. The key is `lockAttemptKey(tableId, id)` = `[LOCK_KEY_PREFIX, tableId, ...id]`, distinct from
|
|
38
38
|
`getFromSource`'s bare-id single-flight lock.
|
|
39
39
|
- **`RecordLockHandle`** (`recordLock.ts`) carries `store`, `key`, `keyId`, `acquiredAt`, `expiresAt`,
|
|
40
|
-
`hold`, `released`, and `expired`. `release()`
|
|
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
|
|