@harperfast/harper 5.3.0-beta.4 → 5.3.0
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/backup.ts +6 -1
- package/components/Application.ts +486 -115
- package/components/DESIGN.md +109 -21
- package/components/deploymentProvenance.ts +28 -0
- package/components/operations.js +14 -7
- package/components/packageComponent.ts +5 -1
- package/config-root.schema.json +1 -1
- package/dataLayer/DESIGN.md +35 -7
- package/dataLayer/blobBackup.ts +71 -30
- package/dataLayer/harperBridge/ResourceBridge.ts +16 -2
- package/dataLayer/rocksdbBackup.ts +88 -34
- package/dataLayer/schemaDescribe.ts +45 -1
- package/dist/bin/backup.js +1 -1
- package/dist/bin/backup.js.map +1 -1
- package/dist/components/Application.d.ts +11 -21
- package/dist/components/Application.js +436 -105
- package/dist/components/Application.js.map +1 -1
- package/dist/components/deploymentProvenance.d.ts +4 -0
- package/dist/components/deploymentProvenance.js +32 -0
- package/dist/components/deploymentProvenance.js.map +1 -0
- package/dist/components/operations.js +14 -7
- package/dist/components/operations.js.map +1 -1
- package/dist/components/packageComponent.js +6 -1
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/dataLayer/blobBackup.d.ts +30 -3
- package/dist/dataLayer/blobBackup.js +73 -36
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +15 -3
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +4 -1
- package/dist/dataLayer/rocksdbBackup.js +74 -36
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +45 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +1 -0
- package/dist/resources/DatabaseTransaction.js +33 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +7 -3
- package/dist/resources/ResourceInterface.js +2 -0
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Table.d.ts +151 -445
- package/dist/resources/Table.js +209 -8
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.d.ts +11 -0
- package/dist/resources/analytics/write.js +3 -0
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +30 -0
- package/dist/resources/blob.js +189 -1
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +1 -0
- package/dist/resources/databases.js +80 -15
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +13 -0
- package/dist/resources/derivedIndexRuntime.js +57 -5
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +1 -0
- package/dist/resources/derivedIndexes.js +116 -15
- package/dist/resources/derivedIndexes.js.map +1 -1
- package/dist/resources/fullTextSchema.d.ts +5 -3
- package/dist/resources/fullTextSchema.js +48 -5
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +2 -0
- package/dist/resources/fullTextSchemaLifecycle.js +33 -0
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +30 -12
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +21 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +114 -5
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/fullTextDerivedIndex.d.ts +11 -2
- package/dist/resources/indexes/fullTextDerivedIndex.js +187 -24
- package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +99 -1
- package/dist/resources/indexes/fullTextNativeBinding.js +35 -1
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
- package/dist/resources/indexes/fullTextQueryIndex.d.ts +58 -0
- package/dist/resources/indexes/fullTextQueryIndex.js +943 -0
- package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -0
- package/dist/resources/indexes/fullTextQueryProtocol.d.ts +6 -0
- package/dist/resources/indexes/fullTextQueryProtocol.js +32 -0
- package/dist/resources/indexes/fullTextQueryProtocol.js.map +1 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +4 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +30 -7
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/search.d.ts +41 -0
- package/dist/resources/search.js +468 -26
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transactionBroadcast.js +101 -50
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/resources/typeParity.d.ts +9 -0
- package/dist/resources/typeParity.js +3 -0
- package/dist/resources/typeParity.js.map +1 -0
- package/dist/server/REST.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +48 -8
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +7 -0
- package/dist/server/serverHelpers/contentTypes.js +36 -23
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +28 -5
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/threads/itc.js +19 -0
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +3 -0
- package/dist/server/threads/manageThreads.js +90 -23
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/utility/operation_authorization.js +25 -7
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/signalling.js +3 -0
- package/dist/utility/signalling.js.map +1 -1
- package/dist/validation/searchValidator.js +22 -2
- package/dist/validation/searchValidator.js.map +1 -1
- package/npm-shrinkwrap.json +22 -22
- package/package.json +2 -4
- package/resources/DESIGN.md +19 -2
- package/resources/DatabaseTransaction.ts +40 -10
- package/resources/FULL_TEXT.md +97 -0
- package/resources/ResourceInterface.ts +8 -2
- package/resources/Table.ts +767 -17
- package/resources/analytics/write.ts +3 -3
- package/resources/blob.ts +183 -1
- package/resources/databases.ts +126 -14
- package/resources/derivedIndexRuntime.ts +79 -6
- package/resources/derivedIndexes.ts +142 -18
- package/resources/fullTextSchema.ts +55 -9
- package/resources/fullTextSchemaLifecycle.ts +45 -0
- package/resources/graphql.ts +33 -12
- package/resources/indexes/DESIGN.md +167 -15
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +125 -5
- package/resources/indexes/fullTextDerivedIndex.ts +216 -32
- package/resources/indexes/fullTextNativeBinding.ts +133 -3
- package/resources/indexes/fullTextQueryIndex.ts +1120 -0
- package/resources/indexes/fullTextQueryProtocol.ts +35 -0
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +28 -8
- package/resources/search.ts +513 -26
- package/resources/transactionBroadcast.ts +86 -47
- package/resources/typeParity.ts +20 -0
- package/schema.graphql +18 -5
- package/server/DESIGN.md +18 -0
- package/server/REST.ts +1 -1
- package/server/itc/serverHandlers.js +61 -8
- package/server/serverHelpers/contentTypes.ts +31 -26
- package/server/serverHelpers/uwsServer.ts +35 -7
- package/server/threads/itc.js +21 -0
- package/server/threads/manageThreads.js +95 -21
- package/studio/web/assets/{Chat-Bq60AO-g.js → Chat-CNEKMllJ.js} +13 -13
- package/studio/web/assets/FloatingChat-CEnWhHeR.js +23 -0
- package/studio/web/assets/SearchDialog-CwhJdEdY.js +1 -0
- package/studio/web/assets/{abnfDiagram-O67JEVCF-DpPZWMOf.js → abnfDiagram-O67JEVCF-raXE027J.js} +1 -1
- package/studio/web/assets/alertDialog-BbWD9Xxa.js +1 -0
- package/studio/web/assets/apiToken-DC5vsaaO.js +1 -0
- package/studio/web/assets/applications-DQZxDy0x.js +296 -0
- package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +1 -0
- package/studio/web/assets/{architectureDiagram-NJMV4G6O-OmgLraID.js → architectureDiagram-NJMV4G6O-C8_Eet7w.js} +1 -1
- package/studio/web/assets/arrow-up-D2g_57vT.js +1 -0
- package/studio/web/assets/authStore-CJCaADw9.js +3 -0
- package/studio/web/assets/{blockDiagram-BEXU5L5S-Cv7CD3Ai.js → blockDiagram-BEXU5L5S-DR6S5-1D.js} +1 -1
- package/studio/web/assets/{button-MFP9n8Ck.js → button-DWwfgGWI.js} +2 -2
- package/studio/web/assets/{c4Diagram-YGBWAQC7-Ds1vKYnN.js → c4Diagram-YGBWAQC7-BAfZ1G01.js} +1 -1
- package/studio/web/assets/channel-D0AkPjJ8.js +1 -0
- package/studio/web/assets/{chevron-up-CFe8x5Bp.js → chevron-up-CFxbMGv2.js} +1 -1
- package/studio/web/assets/{chunk-3FUC2YCW-DpnBPH8B.js → chunk-3FUC2YCW-DuRp-j19.js} +1 -1
- package/studio/web/assets/{chunk-5DYCD2WN-LLLCQSzg.js → chunk-5DYCD2WN-BceBfIHT.js} +1 -1
- package/studio/web/assets/{chunk-742MDFTN-BL8J_pIO.js → chunk-742MDFTN-CIkt8ijt.js} +1 -1
- package/studio/web/assets/{chunk-7INBJB4K-C-hJHiiK.js → chunk-7INBJB4K-BpeAXyOi.js} +1 -1
- package/studio/web/assets/{chunk-7M6MHVWA-C6ZqkFbA.js → chunk-7M6MHVWA-h6WlaTzO.js} +1 -1
- package/studio/web/assets/{chunk-7PRAP22T-CUXM8BG6.js → chunk-7PRAP22T-KyqX6x3f.js} +1 -1
- package/studio/web/assets/{chunk-GTNCS2PH-DJ2BFvS1.js → chunk-GTNCS2PH-DGHLXyow.js} +1 -1
- package/studio/web/assets/{chunk-GWA4HPMP-C42bSqLL.js → chunk-GWA4HPMP-DsejB6gA.js} +1 -1
- package/studio/web/assets/{chunk-MBY4JIJT-C-5jz6Lo.js → chunk-MBY4JIJT-BwSGweQP.js} +1 -1
- package/studio/web/assets/{chunk-NETBCI7D-D-bzlN4r.js → chunk-NETBCI7D-C8UnOw_f.js} +1 -1
- package/studio/web/assets/{chunk-O7XYJQB3-ep14zA23.js → chunk-O7XYJQB3-CBS3nZYV.js} +1 -1
- package/studio/web/assets/{chunk-UA2S7LBM-BMRWRHED.js → chunk-UA2S7LBM-BHpEdH8D.js} +1 -1
- package/studio/web/assets/{chunk-WEXAMYUT-DdLYVdX1.js → chunk-WEXAMYUT-BAIhTwyh.js} +1 -1
- package/studio/web/assets/{chunk-Z7XXMR3K-BmKEDjcP.js → chunk-Z7XXMR3K-DSHbImdf.js} +1 -1
- package/studio/web/assets/{chunk-ZIGJFQKS-DLZgdNxA.js → chunk-ZIGJFQKS-Cs8fUn7y.js} +1 -1
- package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DOI5vgKT.js → classDiagram-v2-NBCMYWYE-DmDPkt2u.js} +1 -1
- package/studio/web/assets/cn-6T8VranJ.js +1 -0
- package/studio/web/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
- package/studio/web/assets/{createLucideIcon-bziYxJli.js → createLucideIcon-fO5dL7D5.js} +1 -1
- package/studio/web/assets/{css.worker-l1psR_wG.js → css.worker-DoT-OOzx.js} +36 -36
- package/studio/web/assets/{cssMode-DJyc9mHj.js → cssMode-Dz2DxF4e.js} +1 -1
- package/studio/web/assets/{cynefin-OW5HDTMX-BSW-DB7S.js → cynefin-OW5HDTMX-DIvbXLSj.js} +1 -1
- package/studio/web/assets/{cynefinDiagram-VND7K2PF-CbAHEwxt.js → cynefinDiagram-VND7K2PF-BdtaXiCE.js} +1 -1
- package/studio/web/assets/{dagre-6A5THRUB-Csxewgs4.js → dagre-6A5THRUB-C7RUWHyz.js} +1 -1
- package/studio/web/assets/{diagram-22UHCM2B-a0rt8tY4.js → diagram-22UHCM2B-DJCvcaWP.js} +1 -1
- package/studio/web/assets/{diagram-3UASUU5V-lEj2SVNh.js → diagram-3UASUU5V-Du6VKWln.js} +1 -1
- package/studio/web/assets/{diagram-ATOU4E4O-HcrAeB-q.js → diagram-ATOU4E4O-dxRfeG4B.js} +1 -1
- package/studio/web/assets/{diagram-CDSNMT55-CYxiuyEx.js → diagram-CDSNMT55-l5Ga0u-W.js} +1 -1
- package/studio/web/assets/{diagram-MLGK6HIB-ChbCJTgJ.js → diagram-MLGK6HIB-CNQY47LA.js} +1 -1
- package/studio/web/assets/{diagram-MPIPVDR6-DaX9Y-N3.js → diagram-MPIPVDR6-7prveD1B.js} +1 -1
- package/studio/web/assets/{dist-Cz2Y6-6G.js → dist-BrRuMUzt.js} +1 -1
- package/studio/web/assets/{download-CtostjYn.js → download-Cn6tPG4L.js} +1 -1
- package/studio/web/assets/{ebnfDiagram-ZINNZB2B-CwAsENB1.js → ebnfDiagram-ZINNZB2B-5OnkTpfi.js} +1 -1
- package/studio/web/assets/{editor-DDIsQfj6.js → editor-B3mjd3tz.js} +1 -1
- package/studio/web/assets/editor.worker-Yd1NR0QT.js +33 -0
- package/studio/web/assets/{elk-276RUBZZ-Hexg9E4D.js → elk-276RUBZZ-DxDQPjQT.js} +1 -1
- package/studio/web/assets/{erDiagram-OPXOYQCR-CeC-wwO1.js → erDiagram-OPXOYQCR-CQlCX84D.js} +1 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +1 -0
- package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +1 -0
- package/studio/web/assets/{ganttDiagram-FUAMR5RP-C2_Hhl1-.js → ganttDiagram-FUAMR5RP-bloX3Voy.js} +1 -1
- package/studio/web/assets/{getAnalytics-CjthMQqF.js → getAnalytics-C0FHjtdQ.js} +1 -1
- package/studio/web/assets/{gitGraph-4MIJSDKK-xIk5Vuew.js → gitGraph-4MIJSDKK-BrXWmJt2.js} +1 -1
- package/studio/web/assets/{gitGraphDiagram-X574FWY7-CMxJ34up.js → gitGraphDiagram-X574FWY7-C2KnnZeY.js} +1 -1
- package/studio/web/assets/{html-g1BJHxLy.js → html-BsRGM7tp.js} +1 -1
- package/studio/web/assets/{html.worker-DdwM4IrA.js → html.worker-B1dwY0Ay.js} +27 -27
- package/studio/web/assets/{htmlMode-Vh5IglCQ.js → htmlMode-tUKJaA3o.js} +1 -1
- package/studio/web/assets/index-72cEH474.css +1 -0
- package/studio/web/assets/index-BuqFjN5K.js +828 -0
- package/studio/web/assets/index.lazy-CcpfQFY_.js +2 -0
- package/studio/web/assets/{info-A6RAGUB7-o49TEbj-.js → info-A6RAGUB7-BhzBHe4U.js} +1 -1
- package/studio/web/assets/{infoDiagram-VRGFBTTK-Cjhh7yzZ.js → infoDiagram-VRGFBTTK-B0J1132p.js} +1 -1
- package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-C63aXRmh.js → ishikawaDiagram-OU5B5YK6-BNk0tuQx.js} +1 -1
- package/studio/web/assets/{javascript-5VIolqf7.js → javascript-BZ_uRGHl.js} +1 -1
- package/studio/web/assets/{journeyDiagram-ZHPQQLJL-D_o0P5Mu.js → journeyDiagram-ZHPQQLJL-Be79RMQM.js} +1 -1
- package/studio/web/assets/json.worker-BQqXGI3X.js +65 -0
- package/studio/web/assets/{jsonMode-DG4zQraI.js → jsonMode-DgB-aFMj.js} +1 -1
- package/studio/web/assets/{kanban-definition-PNTS6WVX-CTI9SSxY.js → kanban-definition-PNTS6WVX-4uuoQP9W.js} +1 -1
- package/studio/web/assets/{languageServices-t5FBpX-i.js → languageServices-BleuBw17.js} +1 -1
- package/studio/web/assets/localStorageKeys-etzFY_xV.js +1 -0
- package/studio/web/assets/{lspLanguageFeatures-DgVnNPh-.js → lspLanguageFeatures-ByVYLSsQ.js} +1 -1
- package/studio/web/assets/{mermaid-parser.core-C2lcH_jP.js → mermaid-parser.core-V1UYMeXV.js} +3 -3
- package/studio/web/assets/{mermaid.core-BG5bjfIG.js → mermaid.core-1PX6R49U.js} +4 -4
- package/studio/web/assets/{mindmap-definition-NLK3R4M7-BK7ho6JD.js → mindmap-definition-NLK3R4M7-C70Ctm0J.js} +1 -1
- package/studio/web/assets/notifications-D1Nf-Edn.js +1 -0
- package/studio/web/assets/notifications-TuzN0IMi.js +1 -0
- package/studio/web/assets/{packet-AYTQ26CC-BHPy6KBV.js → packet-AYTQ26CC-DWbXHx5g.js} +1 -1
- package/studio/web/assets/{pegDiagram-GJSIUBJH-DzjwG3jO.js → pegDiagram-GJSIUBJH-d6z1cc7W.js} +1 -1
- package/studio/web/assets/{pie-WAS4IAKB-D0cP-DTU.js → pie-WAS4IAKB-DjoTm3yr.js} +1 -1
- package/studio/web/assets/{pieDiagram-5QR66LMP-C_hOP9Lv.js → pieDiagram-5QR66LMP-Dz3RHpDA.js} +1 -1
- package/studio/web/assets/profile-D7C7Lupz.js +1 -0
- package/studio/web/assets/{quadrantDiagram-O4NWA36T-CZW8PbV1.js → quadrantDiagram-O4NWA36T-CIF5Qkou.js} +1 -1
- package/studio/web/assets/{radar-RG4KPBEZ-C8fk0U81.js → radar-RG4KPBEZ-CVBgjCOj.js} +1 -1
- package/studio/web/assets/{railroad-74A4TZTK-B60FWm17.js → railroad-74A4TZTK-BdhXw10Q.js} +1 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +1 -0
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +1 -0
- package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +1 -0
- package/studio/web/assets/{railroadDiagram-XR7U4H2S-DyyNmgbU.js → railroadDiagram-XR7U4H2S-y3Jv2U6N.js} +1 -1
- package/studio/web/assets/regions-nGQCm-v8.js +1 -0
- package/studio/web/assets/register-C3x9Ze6P.js +909 -0
- package/studio/web/assets/register-DjpsE8to.css +1 -0
- package/studio/web/assets/{requirementDiagram-PLB6GJNP-C4H-mLKu.js → requirementDiagram-PLB6GJNP-ChINZEmu.js} +1 -1
- package/studio/web/assets/restartGate-nHAFLDh-.js +1 -0
- package/studio/web/assets/{sankeyDiagram-IPEJSGJF-DHDyga4c.js → sankeyDiagram-IPEJSGJF-D9AyLce2.js} +1 -1
- package/studio/web/assets/{sequenceDiagram-PO4LG4MO-CdNcHXRI.js → sequenceDiagram-PO4LG4MO-hfk4aIEm.js} +1 -1
- package/studio/web/assets/{setComponentFile-z4-qJCbA.js → setComponentFile-DpjrRqeG.js} +1 -1
- package/studio/web/assets/{setup-CS50uJ4x.js → setup-CGiKLZ9e.js} +2 -2
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-CpKuXq15.js → stateDiagram-v2-GCMORJYK-Bs8RIb9K.js} +1 -1
- package/studio/web/assets/status-CGJwrNPH.js +61 -0
- package/studio/web/assets/{swimlanes-2SLR337P-NRHa2D8I.js → swimlanes-2SLR337P-B52xrf6t.js} +1 -1
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +8 -0
- package/studio/web/assets/tabs-BCTQzoQK.js +1 -0
- package/studio/web/assets/{timeline-definition-EJHVYXUP-D39Y-xpq.js → timeline-definition-EJHVYXUP-DJSITpQX.js} +1 -1
- package/studio/web/assets/toggleHighContrast-C-Ne6P7T.js +51 -0
- package/studio/web/assets/toggleHighContrast-n7clI0si.css +1 -0
- package/studio/web/assets/{treeView-Q6P3EWNA-CRWR9OqD.js → treeView-Q6P3EWNA-C-epdxRo.js} +1 -1
- package/studio/web/assets/{treemap-WGGIJYW6-ZfXyJgLO.js → treemap-WGGIJYW6-DOVyp_zw.js} +1 -1
- package/studio/web/assets/{ts.worker-DDMUrpqh.js → ts.worker-Clm12uY6.js} +131 -131
- package/studio/web/assets/{tsMode-D83Gg3cq.js → tsMode-CSreYTys.js} +1 -1
- package/studio/web/assets/{typescript-Dfp-GrNq.js → typescript-MnxBk89W.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BKetlINf.js → useEntityRestURL-TNrh77-s.js} +1 -1
- package/studio/web/assets/{usecaseDiagram-POWQR4AR-jf-zIDZ_.js → usecaseDiagram-POWQR4AR-B6252cbq.js} +1 -1
- package/studio/web/assets/{vendor-core-BdkdbUek.js → vendor-core-6LhfJlRC.js} +1 -1
- package/studio/web/assets/vendor-datadog-ozKf-pts.js +6 -0
- package/studio/web/assets/{vendor-react-izWchTRL.js → vendor-react-BOliS0BL.js} +1 -1
- package/studio/web/assets/vendor-tanstack-Bntfka-M.js +2 -0
- package/studio/web/assets/vendor-ui-9EYYDgoN.js +41 -0
- package/studio/web/assets/{vennDiagram-UO4OBE2U-BKYAZUh_.js → vennDiagram-UO4OBE2U-DZTsDA4k.js} +1 -1
- package/studio/web/assets/{wardley-WFR3VGLG-BO7f36ci.js → wardley-WFR3VGLG-Cfqoe6z4.js} +1 -1
- package/studio/web/assets/{wardleyDiagram-VNRHLVJA-Djb0JDQc.js → wardleyDiagram-VNRHLVJA-qu_f7Tvl.js} +1 -1
- package/studio/web/assets/{workers-CCvmDLH3.js → workers-BfZlUNVI.js} +3 -3
- package/studio/web/assets/{xml-06pZp00U.js → xml-BHd6qGH7.js} +1 -1
- package/studio/web/assets/{xychartDiagram-PMCCYNJV-BQh9ubvb.js → xychartDiagram-PMCCYNJV-UrOKRsmR.js} +1 -1
- package/studio/web/assets/{yaml-iSnWMVlV.js → yaml-C53R8OQq.js} +1 -1
- package/studio/web/assets/{yaml.worker-DAK2o_H1.js → yaml.worker-D9JekGdp.js} +183 -183
- package/studio/web/index.html +15 -15
- package/utility/operation_authorization.ts +27 -7
- package/utility/signalling.ts +3 -0
- package/validation/searchValidator.ts +22 -13
- package/studio/web/assets/FloatingChat-D4560T0p.js +0 -23
- package/studio/web/assets/alertDialog-UrbiMgQt.js +0 -1
- package/studio/web/assets/apiToken-BEvbFPMc.js +0 -1
- package/studio/web/assets/applications-CSUVTH4E.js +0 -296
- package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +0 -1
- package/studio/web/assets/authStore-5jSS5pTW.js +0 -3
- package/studio/web/assets/channel-BDtouPd6.js +0 -1
- package/studio/web/assets/dialog-DTepachC.js +0 -1
- package/studio/web/assets/editor.worker-CkkvdXvO.js +0 -33
- package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +0 -1
- package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +0 -1
- package/studio/web/assets/index-CPoDzQTe.css +0 -1
- package/studio/web/assets/index-DGRAzvO4.js +0 -824
- package/studio/web/assets/index.lazy-vgSic7et.js +0 -2
- package/studio/web/assets/json.worker-CSi2hF-a.js +0 -65
- package/studio/web/assets/notifications-BeWcX04m.js +0 -1
- package/studio/web/assets/notifications-DUGWrWoO.js +0 -1
- package/studio/web/assets/profile-BvUEDBzW.js +0 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +0 -1
- package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +0 -1
- package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +0 -1
- package/studio/web/assets/regions-CB-_2m7M.js +0 -1
- package/studio/web/assets/register-1w1Zr3b2.js +0 -848
- package/studio/web/assets/register-jjEx9u7D.css +0 -1
- package/studio/web/assets/setLocalStorage-D_kflv4U.js +0 -1
- package/studio/web/assets/status-CI0wOdOE.js +0 -61
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +0 -8
- package/studio/web/assets/tabs-CnQ_zenf.js +0 -1
- package/studio/web/assets/toggleHighContrast--ixms01u.css +0 -1
- package/studio/web/assets/toggleHighContrast-BsAE4jPe.js +0 -49
- package/studio/web/assets/useLocalStorage-BYoZF60G.js +0 -1
- package/studio/web/assets/vendor-datadog-fCQhBHGk.js +0 -6
- package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +0 -2
- package/studio/web/assets/vendor-ui-aE1tHcW7.js +0 -41
- package/studio/web/assets/x-vYntCxm4.js +0 -1
|
@@ -95,6 +95,7 @@ const manageThreads_js_1 = require("../server/threads/manageThreads.js");
|
|
|
95
95
|
const gitCredentialServer_ts_1 = require("./gitCredentialServer.js");
|
|
96
96
|
const secretDecryptor_ts_1 = require("../resources/secretDecryptor.js");
|
|
97
97
|
const envFile_ts_1 = require("../utility/envFile.js");
|
|
98
|
+
const deploymentProvenance_ts_1 = require("./deploymentProvenance.js");
|
|
98
99
|
const node_path_1 = require("node:path");
|
|
99
100
|
const promises_1 = require("node:fs/promises");
|
|
100
101
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -951,6 +952,11 @@ const ACTIVATION_JOURNAL_VERSION = 2;
|
|
|
951
952
|
const LEGACY_ACTIVATION_JOURNAL_VERSION = 1;
|
|
952
953
|
const ARTIFACT_DESCRIPTOR_VERSION = 1;
|
|
953
954
|
const DEFAULT_STAGING_RETENTION_MAX_COUNT = 5;
|
|
955
|
+
// A digest of the component, not its name, follows the uuid in a claim's name: a long name would pass the filename
|
|
956
|
+
// limit.
|
|
957
|
+
const CLAIMING_PREFIX = '.claiming-';
|
|
958
|
+
const CLAIMING_UUID_LENGTH = 36;
|
|
959
|
+
const CLAIM_OWNER_DIGEST_LENGTH = 16;
|
|
954
960
|
/** `deployment_stagingRetention_maxCount`; 0 keeps none. Only a number or numeric string counts, so `true`/`[]`/blank cannot become "keep nothing". */
|
|
955
961
|
function getStagingRetentionMaxCount() {
|
|
956
962
|
const configured = (0, configUtils_ts_1.getConfigValue)(hdbTerms_ts_1.CONFIG_PARAMS.DEPLOYMENT_STAGINGRETENTION_MAXCOUNT);
|
|
@@ -961,6 +967,9 @@ function getStagingRetentionMaxCount() {
|
|
|
961
967
|
const parsed = Number(configured);
|
|
962
968
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : DEFAULT_STAGING_RETENTION_MAX_COUNT;
|
|
963
969
|
}
|
|
970
|
+
function isDeploymentDirectoryEntry(entry) {
|
|
971
|
+
return entry.isDirectory() && !entry.name.startsWith('.');
|
|
972
|
+
}
|
|
964
973
|
async function presentOrAbsent(path) {
|
|
965
974
|
return (0, promises_1.lstat)(path).catch((error) => {
|
|
966
975
|
if (error?.code === 'ENOENT')
|
|
@@ -991,15 +1000,20 @@ async function dormantBuildAt(deploymentDirPath, owner) {
|
|
|
991
1000
|
* cannot hold or miss a slot. Never throws: a failure must neither fail a component closed nor replace a
|
|
992
1001
|
* deploy's own error.
|
|
993
1002
|
*
|
|
994
|
-
* `
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
* the eviction tail leaves
|
|
998
|
-
* not pin it
|
|
1003
|
+
* A `pinned` id is never evicted. A delayed activation runs this preamble under the same lock it is about to
|
|
1004
|
+
* activate under, so without the pin retention would delete the artifact the request named — immediately, when the
|
|
1005
|
+
* knob is `0` — and a settlement's pins keep the releases it just put back. Pins are applied after the kept set is
|
|
1006
|
+
* chosen, so each pinned build in the eviction tail leaves one more than `maxCount` on disk until a pass that does
|
|
1007
|
+
* not pin it.
|
|
999
1008
|
*/
|
|
1000
|
-
async function pruneDormantBuilds(componentName, builds, maxCount,
|
|
1009
|
+
async function pruneDormantBuilds(componentName, builds, maxCount, pinned) {
|
|
1010
|
+
const pins = new Set(typeof pinned === 'string' ? [pinned] : (pinned ?? []));
|
|
1001
1011
|
const current = [];
|
|
1012
|
+
const seen = new Set();
|
|
1002
1013
|
for (const build of builds) {
|
|
1014
|
+
if (seen.has(build.deploymentDirPath))
|
|
1015
|
+
continue;
|
|
1016
|
+
seen.add(build.deploymentDirPath);
|
|
1003
1017
|
try {
|
|
1004
1018
|
const fresh = await dormantBuildAt(build.deploymentDirPath, componentName);
|
|
1005
1019
|
if (fresh && !(await presentOrAbsent((0, node_path_1.join)(build.deploymentDirPath, ACTIVATION_JOURNAL))))
|
|
@@ -1013,7 +1027,7 @@ async function pruneDormantBuilds(componentName, builds, maxCount, pinnedDeploym
|
|
|
1013
1027
|
.sort((left, right) => right.completedAt - left.completedAt ||
|
|
1014
1028
|
(left.deploymentId < right.deploymentId ? -1 : left.deploymentId > right.deploymentId ? 1 : 0))
|
|
1015
1029
|
.slice(Math.max(0, maxCount))
|
|
1016
|
-
.filter((build) => build.deploymentId
|
|
1030
|
+
.filter((build) => !pins.has(build.deploymentId));
|
|
1017
1031
|
for (const build of evictions) {
|
|
1018
1032
|
try {
|
|
1019
1033
|
await (0, promises_1.rm)(build.deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
@@ -1039,7 +1053,7 @@ async function dormantBuildsOf(componentsRootDirPath, componentName) {
|
|
|
1039
1053
|
}
|
|
1040
1054
|
const builds = [];
|
|
1041
1055
|
for (const deployment of deployments) {
|
|
1042
|
-
if (!deployment
|
|
1056
|
+
if (!isDeploymentDirectoryEntry(deployment))
|
|
1043
1057
|
continue;
|
|
1044
1058
|
const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
|
|
1045
1059
|
try {
|
|
@@ -1327,6 +1341,142 @@ async function writeArtifactDescriptor(componentDirPath, deploymentId, descripto
|
|
|
1327
1341
|
throw error;
|
|
1328
1342
|
}
|
|
1329
1343
|
}
|
|
1344
|
+
async function writeDeploymentProvenance(candidateDirPath, componentName, deploymentId) {
|
|
1345
|
+
const markerPath = (0, node_path_1.join)(candidateDirPath, deploymentProvenance_ts_1.DEPLOYMENT_PROVENANCE_FILE);
|
|
1346
|
+
await (0, promises_1.rm)(markerPath, { recursive: true, force: true });
|
|
1347
|
+
await writeControlFileDurably(markerPath, (0, deploymentProvenance_ts_1.formatDeploymentProvenance)(componentName, deploymentId));
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* A link carries no provenance, whatever its target holds. A read that fails propagates rather than reading as
|
|
1351
|
+
* absent.
|
|
1352
|
+
*/
|
|
1353
|
+
async function readDeploymentProvenance(treePath, componentName) {
|
|
1354
|
+
const tree = await presentOrAbsent(treePath);
|
|
1355
|
+
if (!tree?.isDirectory())
|
|
1356
|
+
return undefined;
|
|
1357
|
+
const raw = await (0, promises_1.readFile)((0, node_path_1.join)(treePath, deploymentProvenance_ts_1.DEPLOYMENT_PROVENANCE_FILE), 'utf8').catch((error) => {
|
|
1358
|
+
if (error?.code === 'ENOENT' || error?.code === 'EISDIR')
|
|
1359
|
+
return undefined;
|
|
1360
|
+
throw error;
|
|
1361
|
+
});
|
|
1362
|
+
return raw === undefined ? undefined : (0, deploymentProvenance_ts_1.parseDeploymentProvenance)(raw, componentName);
|
|
1363
|
+
}
|
|
1364
|
+
async function isDeploymentRecord(deploymentDirPath, componentName) {
|
|
1365
|
+
if ((await candidateComponentName(deploymentDirPath)) !== componentName)
|
|
1366
|
+
return false;
|
|
1367
|
+
if (!(await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER))))
|
|
1368
|
+
return false;
|
|
1369
|
+
for (const unexpected of [ACTIVATION_JOURNAL, UNSETTLED_MARKER, componentName]) {
|
|
1370
|
+
if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, unexpected)))
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
return (await readArtifactDescriptor(deploymentDirPath, componentName)) !== undefined;
|
|
1375
|
+
}
|
|
1376
|
+
catch (error) {
|
|
1377
|
+
if (error instanceof hdbError_ts_1.ClientError)
|
|
1378
|
+
return false;
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* "Not live" does not make a record stale: a crash between the commit and the re-home leaves the displaced tree in
|
|
1384
|
+
* `.deploy-aside`, retired or not, and a scan can reach the record before the journal that re-homes into it.
|
|
1385
|
+
*/
|
|
1386
|
+
async function deploymentRecordIsReferenced(componentsRootDirPath, componentName, deploymentId) {
|
|
1387
|
+
const liveDirPath = (0, node_path_1.join)(componentsRootDirPath, componentName);
|
|
1388
|
+
if ((await readDeploymentProvenance(liveDirPath, componentName)) === deploymentId)
|
|
1389
|
+
return true;
|
|
1390
|
+
const asideStagingDir = extractionStagingDirectory(liveDirPath);
|
|
1391
|
+
const entries = await (0, promises_1.readdir)(asideStagingDir, { withFileTypes: true }).catch((error) => {
|
|
1392
|
+
if (error?.code === 'ENOENT')
|
|
1393
|
+
return [];
|
|
1394
|
+
throw error;
|
|
1395
|
+
});
|
|
1396
|
+
for (const entry of entries) {
|
|
1397
|
+
if (!entry.isDirectory() || !entry.name.startsWith(IN_PROGRESS_ASIDE_PREFIX))
|
|
1398
|
+
continue;
|
|
1399
|
+
if ((await readDeploymentProvenance((0, node_path_1.join)(asideStagingDir, entry.name), componentName)) === deploymentId) {
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
async function isReferencedDeploymentRecord(componentsRootDirPath, deploymentDirPath, owner) {
|
|
1406
|
+
for (const expected of [CANDIDATE_COMPLETE_MARKER, CANDIDATE_ARTIFACT_FILE]) {
|
|
1407
|
+
if (!(await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, expected))))
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, owner)))
|
|
1411
|
+
return false;
|
|
1412
|
+
return deploymentRecordIsReferenced(componentsRootDirPath, owner, (0, node_path_1.basename)(deploymentDirPath));
|
|
1413
|
+
}
|
|
1414
|
+
/** A failed read keeps the directory: a leftover record costs a few bytes, a lost one costs the release. */
|
|
1415
|
+
async function keepsDeploymentRecord(deploymentDirPath) {
|
|
1416
|
+
return presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_ARTIFACT_FILE)).then(Boolean, () => true);
|
|
1417
|
+
}
|
|
1418
|
+
/** Retention orders by `.complete`'s mtime, and a kept release has to outrank stages nobody activated. */
|
|
1419
|
+
async function refreshCompletedAt(deploymentDirPath, componentName, deploymentId) {
|
|
1420
|
+
for (let attempt = 1;; attempt++) {
|
|
1421
|
+
const now = new Date();
|
|
1422
|
+
try {
|
|
1423
|
+
await (0, promises_1.utimes)((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER), now, now);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
if (attempt >= 3) {
|
|
1428
|
+
harper_logger_ts_1.default.warn(`Kept deployment ${deploymentId} of ${componentName}, but retention may evict it ahead of older ` +
|
|
1429
|
+
`builds: ${errorMessage(error)}`);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
await (0, promises_3.setTimeout)(100 * attempt);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Only an explicit retention decision deletes a displaced tree: `failed` leaves it for the next preparation to retry.
|
|
1438
|
+
* A failure after the rename propagates, as the aside syncs around it do.
|
|
1439
|
+
*/
|
|
1440
|
+
async function retainDisplacedRelease(componentsRootDirPath, componentName, displacedTreePath) {
|
|
1441
|
+
const keepNone = getStagingRetentionMaxCount() === 0;
|
|
1442
|
+
let record;
|
|
1443
|
+
try {
|
|
1444
|
+
const deploymentId = await readDeploymentProvenance(displacedTreePath, componentName);
|
|
1445
|
+
if (deploymentId !== undefined) {
|
|
1446
|
+
const recordDirPath = (0, node_path_1.join)(componentsRootDirPath, exports.DEPLOY_STAGING_DIR, deploymentId);
|
|
1447
|
+
if (await isDeploymentRecord(recordDirPath, componentName))
|
|
1448
|
+
record = { deploymentId, recordDirPath };
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
catch (error) {
|
|
1452
|
+
if (keepNone)
|
|
1453
|
+
return { verdict: 'ineligible' };
|
|
1454
|
+
harper_logger_ts_1.default.warn(`Could not tell whether the previous release of ${componentName} at ${displacedTreePath} can be kept; ` +
|
|
1455
|
+
`leaving it for its next deploy:`, (0, harper_logger_ts_1.errorForLog)(error));
|
|
1456
|
+
return { verdict: 'failed' };
|
|
1457
|
+
}
|
|
1458
|
+
if (!record)
|
|
1459
|
+
return { verdict: 'ineligible' };
|
|
1460
|
+
const { deploymentId, recordDirPath } = record;
|
|
1461
|
+
if (keepNone) {
|
|
1462
|
+
// Nothing will ever put the tree back into its record, so the record goes with it.
|
|
1463
|
+
await (0, promises_1.rm)(recordDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => harper_logger_ts_1.default.warn(`Could not remove the record of deployment ${deploymentId} of ${componentName}:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
1464
|
+
return { verdict: 'ineligible' };
|
|
1465
|
+
}
|
|
1466
|
+
const keptTreePath = (0, node_path_1.join)(recordDirPath, componentName);
|
|
1467
|
+
try {
|
|
1468
|
+
await renameThroughTransientHolder(displacedTreePath, keptTreePath);
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
harper_logger_ts_1.default.warn(`Could not keep the previous release of ${componentName} as deployment ${deploymentId}; leaving it for ` +
|
|
1472
|
+
`its next deploy:`, (0, harper_logger_ts_1.errorForLog)(error));
|
|
1473
|
+
return { verdict: 'failed' };
|
|
1474
|
+
}
|
|
1475
|
+
await syncRenameParents(displacedTreePath, keptTreePath);
|
|
1476
|
+
await refreshCompletedAt(recordDirPath, componentName, deploymentId);
|
|
1477
|
+
harper_logger_ts_1.default.trace?.(`Kept the previous release of ${componentName} as dormant deployment ${deploymentId}`);
|
|
1478
|
+
return { verdict: 'kept', deploymentId };
|
|
1479
|
+
}
|
|
1330
1480
|
/**
|
|
1331
1481
|
* A component name safe to join onto the components root: no separator, no traversal, not dot-prefixed.
|
|
1332
1482
|
* Applied to EVERY source of the name — the journal and the sidecar — because validating one and trusting
|
|
@@ -1491,88 +1641,107 @@ async function ensureSecureStagingDirectory(stagingDir) {
|
|
|
1491
1641
|
}
|
|
1492
1642
|
}
|
|
1493
1643
|
/**
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
* directory would let a replayed stage rewrite the bytes under an existing `.complete` and descriptor —
|
|
1497
|
-
* and a crash mid-rebuild would leave a partial tree that still reads as certified.
|
|
1498
|
-
*
|
|
1499
|
-
* The caller holds the component's preparation lock, which is what makes the EEXIST verdicts sound: no
|
|
1500
|
-
* other preparation of THIS component is running, and a directory belonging to another component is not
|
|
1501
|
-
* this lock's to touch.
|
|
1644
|
+
* Exclusive, because an operator can repeat a public id; built aside and renamed onto it, so it never appears
|
|
1645
|
+
* unattributed. The caller's preparation lock is what makes the verdicts on an existing directory sound.
|
|
1502
1646
|
*/
|
|
1503
1647
|
async function claimDeploymentDirectory(deploymentDirPath, componentName) {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
const taken = (message) => new hdbError_ts_1.ClientError(message, 409);
|
|
1648
|
+
const claimDirPath = (0, node_path_1.join)((0, node_path_1.dirname)(deploymentDirPath), `${CLAIMING_PREFIX}${(0, node_crypto_1.randomUUID)()}-${claimOwnerDigest(componentName)}`);
|
|
1649
|
+
await (0, promises_1.mkdir)(claimDirPath, { mode: 0o700 });
|
|
1507
1650
|
try {
|
|
1508
|
-
await (0, promises_1.
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
if (error.code !== 'EEXIST')
|
|
1512
|
-
throw error;
|
|
1513
|
-
const owner = await candidateComponentName(deploymentDirPath);
|
|
1514
|
-
if (owner !== undefined && owner !== componentName) {
|
|
1515
|
-
throw taken(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} already holds a build of '${owner}'; a deployment id ` +
|
|
1516
|
-
`names one artifact for its lifetime`);
|
|
1517
|
-
}
|
|
1518
|
-
// Ownership has to be POSITIVE to reclaim, and EMPTINESS IS NOT A VERDICT. The sidecar below is
|
|
1519
|
-
// written as part of the claim, so a directory naming nobody is another component between its own
|
|
1520
|
-
// mkdir and that write, and a deploy can hold a nearly empty one for minutes while it resolves and
|
|
1521
|
-
// packs. Nothing on disk separates that from a claim that got no further, and the lock that
|
|
1522
|
-
// serializes it is not this one, so only the exclusive create may conclude the id is free: the
|
|
1523
|
-
// directory can also have been discarded by a failed build between the first mkdir and this read,
|
|
1524
|
-
// and another component can claim it in that same gap.
|
|
1525
|
-
if (owner === undefined) {
|
|
1526
|
-
await (0, promises_1.mkdir)(deploymentDirPath, { mode: 0o700 }).catch((retry) => {
|
|
1527
|
-
if (retry?.code !== 'EEXIST')
|
|
1528
|
-
throw retry;
|
|
1529
|
-
throw taken(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} is already claimed by a build that has not named its ` +
|
|
1530
|
-
`component yet. If no deploy of any component is in flight, that directory is abandoned and has ` +
|
|
1531
|
-
`to be removed by hand; deploying again without a deployment_id mints a fresh id`);
|
|
1532
|
-
});
|
|
1651
|
+
const claimed = await (0, promises_1.lstat)(claimDirPath);
|
|
1652
|
+
if (!claimed.isDirectory() || claimed.isSymbolicLink()) {
|
|
1653
|
+
throw new Error(`Component deploy staging path is not a directory: ${claimDirPath}`);
|
|
1533
1654
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1655
|
+
if (process.platform !== 'win32' && (claimed.mode & 0o777) !== 0o700) {
|
|
1656
|
+
await (0, promises_1.chmod)(claimDirPath, 0o700).catch((error) => harper_logger_ts_1.default.warn(`Could not restrict component deploy staging permissions for ${claimDirPath}:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
1657
|
+
}
|
|
1658
|
+
await publishClaimOwnership(claimDirPath, componentName);
|
|
1659
|
+
for (let attempt = 1;; attempt++) {
|
|
1660
|
+
// Checked first: Windows refuses a rename onto an existing directory with a transient holder's EPERM.
|
|
1661
|
+
if (!(await presentOrAbsent(deploymentDirPath))) {
|
|
1662
|
+
try {
|
|
1663
|
+
await renameThroughTransientHolder(claimDirPath, deploymentDirPath);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
// Only this component's lock can remove the claim, so a vanished source means the rename landed.
|
|
1668
|
+
if (!(await presentOrAbsent(claimDirPath)))
|
|
1669
|
+
return;
|
|
1670
|
+
if (!(await presentOrAbsent(deploymentDirPath)))
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1538
1673
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1674
|
+
if (attempt >= MAX_CLAIM_ATTEMPTS) {
|
|
1675
|
+
throw new hdbError_ts_1.ClientError(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} was claimed again each time it was freed; deploying ` +
|
|
1676
|
+
`again without a deployment_id mints a fresh id`, 409);
|
|
1677
|
+
}
|
|
1678
|
+
await makeRoomForClaim(deploymentDirPath, componentName);
|
|
1543
1679
|
}
|
|
1544
1680
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1681
|
+
finally {
|
|
1682
|
+
// A claim that did not land names no id, so it is nobody else's.
|
|
1683
|
+
await (0, promises_1.rm)(claimDirPath, { recursive: true, force: true }).catch(() => { });
|
|
1548
1684
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1685
|
+
}
|
|
1686
|
+
const MAX_CLAIM_ATTEMPTS = 3;
|
|
1687
|
+
/** Every refusal is a conflict the caller resolves by naming another id: a 409, not a server fault. */
|
|
1688
|
+
async function makeRoomForClaim(deploymentDirPath, componentName) {
|
|
1689
|
+
const taken = (message) => new hdbError_ts_1.ClientError(message, 409);
|
|
1690
|
+
const deploymentId = (0, node_path_1.basename)(deploymentDirPath);
|
|
1691
|
+
const owner = await candidateComponentName(deploymentDirPath);
|
|
1692
|
+
if (owner !== undefined && owner !== componentName) {
|
|
1693
|
+
throw taken(`Deployment id ${deploymentId} already holds a build of '${owner}'; a deployment id names one artifact for ` +
|
|
1694
|
+
`its lifetime`);
|
|
1695
|
+
}
|
|
1696
|
+
if (owner === undefined) {
|
|
1697
|
+
// Claims appear already attributed, so an EMPTY directory is only ever one a build created before naming it,
|
|
1698
|
+
// abandoned; `rmdir` removes nothing else.
|
|
1699
|
+
try {
|
|
1700
|
+
await (0, promises_1.rmdir)(deploymentDirPath);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
catch (error) {
|
|
1704
|
+
const code = error.code;
|
|
1705
|
+
if (code === 'ENOENT')
|
|
1706
|
+
return;
|
|
1707
|
+
if (code !== 'ENOTEMPTY' && code !== 'EEXIST')
|
|
1708
|
+
throw error;
|
|
1709
|
+
}
|
|
1710
|
+
throw taken(`Deployment id ${deploymentId} is held by a directory that names no component. Remove it once you have ` +
|
|
1711
|
+
`determined it holds nothing you need; deploying again without a deployment_id mints a fresh id`);
|
|
1712
|
+
}
|
|
1713
|
+
if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER))) {
|
|
1714
|
+
throw taken(`Deployment id ${deploymentId} already holds a completed build of '${componentName}'; deploy it with ` +
|
|
1715
|
+
`deployment_id, or deploy again to build a new one`);
|
|
1716
|
+
}
|
|
1717
|
+
// Nothing certified it, and this component's lock serializes its claims.
|
|
1718
|
+
await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
1719
|
+
}
|
|
1720
|
+
function claimOwnerDigest(componentName) {
|
|
1721
|
+
return (0, node_crypto_1.createHash)('sha256').update(componentName).digest('hex').slice(0, CLAIM_OWNER_DIGEST_LENGTH);
|
|
1722
|
+
}
|
|
1723
|
+
function isClaimOf(entryName, componentName) {
|
|
1724
|
+
return (entryName.startsWith(CLAIMING_PREFIX) &&
|
|
1725
|
+
entryName.slice(CLAIMING_PREFIX.length + CLAIMING_UUID_LENGTH + 1) === claimOwnerDigest(componentName));
|
|
1726
|
+
}
|
|
1727
|
+
/** The caller holds the component's preparation lock, so none of its claims is in flight. */
|
|
1728
|
+
async function sweepAbandonedClaims(stagingRoot, entries, componentName) {
|
|
1729
|
+
for (const entry of entries) {
|
|
1730
|
+
if (!entry.isDirectory() || !isClaimOf(entry.name, componentName))
|
|
1731
|
+
continue;
|
|
1732
|
+
await (0, promises_1.rm)((0, node_path_1.join)(stagingRoot, entry.name), { recursive: true, force: true }).catch((error) => harper_logger_ts_1.default.warn(`Could not remove the abandoned deploy claim ${entry.name}:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
1551
1733
|
}
|
|
1552
|
-
await publishClaimOwnership(deploymentDirPath, componentName);
|
|
1553
1734
|
}
|
|
1554
1735
|
/**
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
* Ownership is published as part of the claim rather than at certification, because resolving and packing can
|
|
1559
|
-
* take minutes and until a tree exists to infer an owner from, a directory answering to nobody is one another
|
|
1560
|
-
* component will take for abandoned. Unattributed is a permanent refusal, though, so a failure here — a full
|
|
1561
|
-
* disk, an EIO on the temp write or its sync — would burn this deployment id for good, every retry refused by
|
|
1562
|
-
* its own wreckage. The removal is scoped to the directory this invocation made, and deliberately not
|
|
1563
|
-
* broadened to one found by EEXIST: that directory may be another claimant's, and removing it is the race the
|
|
1564
|
-
* refusal exists to prevent. Best-effort, because the claim failure is what the caller needs to see.
|
|
1565
|
-
*
|
|
1566
|
-
* `write` is a parameter so the failure is testable without a filesystem that can be made to fail on exactly
|
|
1567
|
-
* this write and nothing else.
|
|
1736
|
+
* The claim names no id yet, so a failure burns nothing. `write` is a parameter so the failure is testable without a
|
|
1737
|
+
* filesystem that can be made to fail on exactly this write.
|
|
1568
1738
|
*/
|
|
1569
|
-
async function publishClaimOwnership(
|
|
1739
|
+
async function publishClaimOwnership(claimDirPath, componentName, write = writeControlFileDurably) {
|
|
1570
1740
|
try {
|
|
1571
|
-
await write((0, node_path_1.join)(
|
|
1741
|
+
await write((0, node_path_1.join)(claimDirPath, CANDIDATE_COMPONENT_FILE), componentName);
|
|
1572
1742
|
}
|
|
1573
1743
|
catch (error) {
|
|
1574
|
-
await (0, promises_1.rm)(
|
|
1575
|
-
`ownership; the id stays unusable until it is removed:`, (0, harper_logger_ts_1.errorForLog)(cleanupError)));
|
|
1744
|
+
await (0, promises_1.rm)(claimDirPath, { recursive: true, force: true }).catch((cleanupError) => harper_logger_ts_1.default.warn(`Could not remove the unfinished deploy claim ${claimDirPath}:`, (0, harper_logger_ts_1.errorForLog)(cleanupError)));
|
|
1576
1745
|
throw error;
|
|
1577
1746
|
}
|
|
1578
1747
|
}
|
|
@@ -1632,7 +1801,7 @@ async function journaledDeploymentForComponent(componentsRootDirPath, componentN
|
|
|
1632
1801
|
throw error;
|
|
1633
1802
|
}
|
|
1634
1803
|
for (const deployment of deployments) {
|
|
1635
|
-
if (!deployment
|
|
1804
|
+
if (!isDeploymentDirectoryEntry(deployment))
|
|
1636
1805
|
continue;
|
|
1637
1806
|
const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
|
|
1638
1807
|
const journalPath = (0, node_path_1.join)(deploymentDirPath, ACTIVATION_JOURNAL);
|
|
@@ -1741,9 +1910,11 @@ async function settleStagingForComponent(componentsRootDirPath, componentName, p
|
|
|
1741
1910
|
return;
|
|
1742
1911
|
throw error;
|
|
1743
1912
|
}
|
|
1913
|
+
await sweepAbandonedClaims(stagingRoot, deployments, componentName);
|
|
1744
1914
|
const dormant = [];
|
|
1915
|
+
const kept = [];
|
|
1745
1916
|
for (const deployment of deployments) {
|
|
1746
|
-
if (!deployment
|
|
1917
|
+
if (!isDeploymentDirectoryEntry(deployment))
|
|
1747
1918
|
continue;
|
|
1748
1919
|
const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
|
|
1749
1920
|
// Ownership BEFORE parsing. Reading every journal first meant a truncated journal belonging to another
|
|
@@ -1790,11 +1961,27 @@ async function settleStagingForComponent(componentsRootDirPath, componentName, p
|
|
|
1790
1961
|
// activation interrupted before B1 has no rollback record for the restore gate to catch.
|
|
1791
1962
|
if (journal.component !== componentName)
|
|
1792
1963
|
continue;
|
|
1793
|
-
await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, journal);
|
|
1964
|
+
kept.push(...(await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, journal)));
|
|
1794
1965
|
}
|
|
1966
|
+
await catalogueKeptReleases(stagingRoot, componentName, kept, dormant);
|
|
1795
1967
|
const maxCount = getStagingRetentionMaxCount();
|
|
1796
|
-
if (dormant.length > maxCount)
|
|
1797
|
-
await pruneDormantBuilds(componentName, dormant, maxCount,
|
|
1968
|
+
if (dormant.length > maxCount) {
|
|
1969
|
+
await pruneDormantBuilds(componentName, dormant, maxCount, [
|
|
1970
|
+
...kept,
|
|
1971
|
+
...(pinnedDeploymentId ? [pinnedDeploymentId] : []),
|
|
1972
|
+
]);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/** Counted and pinned, or the pass a settlement ran inside can miss what it kept, or evict it after a failed refresh. */
|
|
1976
|
+
async function catalogueKeptReleases(stagingRoot, componentName, kept, into) {
|
|
1977
|
+
const present = new Set(into.map((build) => build.deploymentDirPath));
|
|
1978
|
+
for (const deploymentId of kept) {
|
|
1979
|
+
const build = await dormantBuildAt((0, node_path_1.join)(stagingRoot, deploymentId), componentName).catch(() => undefined);
|
|
1980
|
+
if (build && !present.has(build.deploymentDirPath)) {
|
|
1981
|
+
present.add(build.deploymentDirPath);
|
|
1982
|
+
into.push(build);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1798
1985
|
}
|
|
1799
1986
|
/**
|
|
1800
1987
|
* Components that on-disk evidence says were left in a state nobody settled — determined READ-ONLY, so any
|
|
@@ -1822,7 +2009,7 @@ async function unsettleableComponentsFromDisk(componentsRootDirPath) {
|
|
|
1822
2009
|
throw error;
|
|
1823
2010
|
}
|
|
1824
2011
|
for (const deployment of deployments) {
|
|
1825
|
-
if (!deployment
|
|
2012
|
+
if (!isDeploymentDirectoryEntry(deployment))
|
|
1826
2013
|
continue;
|
|
1827
2014
|
const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
|
|
1828
2015
|
// Per deployment. `candidateComponentName` propagates every non-ENOENT error now, and this pass runs
|
|
@@ -1906,13 +2093,23 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
|
|
|
1906
2093
|
const dormant = new Map();
|
|
1907
2094
|
const catalogue = (owner, build) => {
|
|
1908
2095
|
const builds = dormant.get(owner);
|
|
1909
|
-
if (builds)
|
|
1910
|
-
builds.push(build);
|
|
1911
|
-
else
|
|
2096
|
+
if (!builds)
|
|
1912
2097
|
dormant.set(owner, [build]);
|
|
2098
|
+
else if (!builds.some((existing) => existing.deploymentDirPath === build.deploymentDirPath))
|
|
2099
|
+
builds.push(build);
|
|
2100
|
+
};
|
|
2101
|
+
const keptBy = new Map();
|
|
2102
|
+
const keptDuring = async (owner, kept) => {
|
|
2103
|
+
if (kept.length === 0)
|
|
2104
|
+
return;
|
|
2105
|
+
keptBy.set(owner, [...(keptBy.get(owner) ?? []), ...kept]);
|
|
2106
|
+
const builds = [];
|
|
2107
|
+
await catalogueKeptReleases(stagingRoot, owner, kept, builds);
|
|
2108
|
+
for (const build of builds)
|
|
2109
|
+
catalogue(owner, build);
|
|
1913
2110
|
};
|
|
1914
2111
|
for (const deployment of deployments) {
|
|
1915
|
-
if (!deployment
|
|
2112
|
+
if (!isDeploymentDirectoryEntry(deployment))
|
|
1916
2113
|
continue;
|
|
1917
2114
|
const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
|
|
1918
2115
|
const journalPath = (0, node_path_1.join)(deploymentDirPath, ACTIVATION_JOURNAL);
|
|
@@ -1971,7 +2168,7 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
|
|
|
1971
2168
|
throw splitAttributionError(deploymentDirPath, appeared.component, splitNames[0]);
|
|
1972
2169
|
}
|
|
1973
2170
|
activationToFail = appeared.component;
|
|
1974
|
-
await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, appeared);
|
|
2171
|
+
await keptDuring(appeared.component, await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, appeared));
|
|
1975
2172
|
// Settled. Anything that fails after this — releasing the lock, say — is not this
|
|
1976
2173
|
// activation's, and attributing it here would fail a correctly settled component closed.
|
|
1977
2174
|
activationToFail = undefined;
|
|
@@ -2014,6 +2211,9 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
|
|
|
2014
2211
|
// Cleared, and still not a retainable build — an incomplete or treeless staged directory. That
|
|
2015
2212
|
// is residue like any other, so it falls through to the removal below.
|
|
2016
2213
|
}
|
|
2214
|
+
// Re-derived under the lock too: the release may have been re-homed, or displaced, since the scan.
|
|
2215
|
+
if (await isReferencedDeploymentRecord(componentsRootDirPath, deploymentDirPath, owner))
|
|
2216
|
+
return;
|
|
2017
2217
|
// Cleanup, not settlement. There was no activation here — this is most often the residue a
|
|
2018
2218
|
// SUCCESSFUL settlement leaves when its own sweep failed — so a sweep that fails again cannot
|
|
2019
2219
|
// make anything unsettled, and recording it would refuse a live component on every worker
|
|
@@ -2050,6 +2250,18 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
|
|
|
2050
2250
|
catalogue(owner, build);
|
|
2051
2251
|
continue;
|
|
2052
2252
|
}
|
|
2253
|
+
// The record of a release that is live, or displaced and not yet put back, is left alone without the
|
|
2254
|
+
// lock for the same reason. A read that fails leaves it in place: deleting it is the irreversible side.
|
|
2255
|
+
try {
|
|
2256
|
+
if (!(await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, UNSETTLED_MARKER))) &&
|
|
2257
|
+
(await isReferencedDeploymentRecord(componentsRootDirPath, deploymentDirPath, owner))) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch (error) {
|
|
2262
|
+
harper_logger_ts_1.default.warn(`Leaving deploy staging ${deploymentDirPath} in place; it could not be read:`, (0, harper_logger_ts_1.errorForLog)(error));
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2053
2265
|
}
|
|
2054
2266
|
// Scoped to THIS deployment, like the journaled branch below: a lock timeout or an EIO here used to
|
|
2055
2267
|
// abort the entire scan, leaving every later deployment unsettled and unmarked.
|
|
@@ -2107,11 +2319,12 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
|
|
|
2107
2319
|
continue;
|
|
2108
2320
|
}
|
|
2109
2321
|
const settling = journal;
|
|
2110
|
-
await (0, componentPreparationLock_ts_1.withComponentPreparationLock)((0, node_path_1.join)(componentsRootDirPath, settling.component), () => settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, settling), {
|
|
2322
|
+
const kept = await (0, componentPreparationLock_ts_1.withComponentPreparationLock)((0, node_path_1.join)(componentsRootDirPath, settling.component), () => settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, settling), {
|
|
2111
2323
|
purpose: 'activation-recovery',
|
|
2112
2324
|
...RECOVERY_LOCK_WAIT,
|
|
2113
2325
|
isOwnerAlive: (lockOwner) => lockOwner.pid !== process.pid || (0, manageThreads_js_1.isThreadRunning)(lockOwner.threadId),
|
|
2114
2326
|
});
|
|
2327
|
+
await keptDuring(settling.component, kept);
|
|
2115
2328
|
}
|
|
2116
2329
|
catch (error) {
|
|
2117
2330
|
// The journal named its component, so attribution is exact however the settle failed.
|
|
@@ -2120,7 +2333,8 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
|
|
|
2120
2333
|
}
|
|
2121
2334
|
const maxCount = getStagingRetentionMaxCount();
|
|
2122
2335
|
for (const [owner, builds] of dormant) {
|
|
2123
|
-
|
|
2336
|
+
const pinned = keptBy.get(owner) ?? [];
|
|
2337
|
+
for (const [component, error] of await reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount, pinned)) {
|
|
2124
2338
|
if (!failures.has(component))
|
|
2125
2339
|
failures.set(component, error);
|
|
2126
2340
|
}
|
|
@@ -2150,7 +2364,7 @@ async function recordUnsettled(failures, component, error, deploymentDirPath) {
|
|
|
2150
2364
|
* dormant. The lock is taken only when there is something to do; a lock a live deploy holds is the same
|
|
2151
2365
|
* deferral the residue branch records.
|
|
2152
2366
|
*/
|
|
2153
|
-
async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount) {
|
|
2367
|
+
async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount, pinned = []) {
|
|
2154
2368
|
const failures = new Map();
|
|
2155
2369
|
let journaled;
|
|
2156
2370
|
for (const build of builds) {
|
|
@@ -2166,6 +2380,7 @@ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxC
|
|
|
2166
2380
|
try {
|
|
2167
2381
|
await (0, componentPreparationLock_ts_1.withComponentPreparationLock)((0, node_path_1.join)(componentsRootDirPath, owner), async () => {
|
|
2168
2382
|
const stillDormant = [];
|
|
2383
|
+
const keptHere = [];
|
|
2169
2384
|
for (const build of builds) {
|
|
2170
2385
|
let journal;
|
|
2171
2386
|
try {
|
|
@@ -2189,14 +2404,17 @@ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxC
|
|
|
2189
2404
|
continue;
|
|
2190
2405
|
}
|
|
2191
2406
|
try {
|
|
2192
|
-
await settleInterruptedActivation(componentsRootDirPath, build.deploymentDirPath, journal);
|
|
2407
|
+
const kept = await settleInterruptedActivation(componentsRootDirPath, build.deploymentDirPath, journal);
|
|
2408
|
+
keptHere.push(...kept);
|
|
2409
|
+
await catalogueKeptReleases((0, node_path_1.join)(componentsRootDirPath, exports.DEPLOY_STAGING_DIR), owner, kept, stillDormant);
|
|
2193
2410
|
}
|
|
2194
2411
|
catch (error) {
|
|
2195
2412
|
await recordUnsettled(failures, journal.component, error, build.deploymentDirPath);
|
|
2196
2413
|
}
|
|
2197
2414
|
}
|
|
2198
|
-
if (stillDormant.length > maxCount)
|
|
2199
|
-
await pruneDormantBuilds(owner, stillDormant, maxCount);
|
|
2415
|
+
if (stillDormant.length > maxCount) {
|
|
2416
|
+
await pruneDormantBuilds(owner, stillDormant, maxCount, [...pinned, ...keptHere]);
|
|
2417
|
+
}
|
|
2200
2418
|
}, {
|
|
2201
2419
|
purpose: 'activation-recovery',
|
|
2202
2420
|
...RECOVERY_LOCK_WAIT,
|
|
@@ -2229,6 +2447,7 @@ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxC
|
|
|
2229
2447
|
* journal prevents. Sweeping the displaced tree only costs disk, so it is logged.
|
|
2230
2448
|
*/
|
|
2231
2449
|
async function sweepAsideRecords(records, componentName, liveDirPath, asideStagingDir) {
|
|
2450
|
+
const kept = [];
|
|
2232
2451
|
for (const record of records) {
|
|
2233
2452
|
// RETIRING IS CORRECTNESS, not hygiene: the retired marker is what stops the legacy pass treating this
|
|
2234
2453
|
// record as authoritative and restoring the displaced tree over the candidate that was just rolled
|
|
@@ -2236,10 +2455,18 @@ async function sweepAsideRecords(records, componentName, liveDirPath, asideStagi
|
|
|
2236
2455
|
// the inversion this protocol exists to prevent, so a failure here PROPAGATES — the caller keeps the
|
|
2237
2456
|
// journal and the next start retries.
|
|
2238
2457
|
const retiredMarkerPath = await retireExtractionAside(record);
|
|
2458
|
+
// No prune here: settlement can run inside another request's preamble, whose pin it cannot see. A tree that
|
|
2459
|
+
// could not be kept stays retired in the aside for the legacy pass to retry.
|
|
2460
|
+
const retention = await retainDisplacedRelease((0, node_path_1.dirname)(liveDirPath), componentName, record);
|
|
2461
|
+
if (retention.verdict === 'kept')
|
|
2462
|
+
kept.push(retention.deploymentId);
|
|
2463
|
+
if (retention.verdict === 'failed')
|
|
2464
|
+
continue;
|
|
2239
2465
|
// Sweeping the displaced tree is hygiene: it bounds disk, and a failure costs space rather than
|
|
2240
2466
|
// correctness, so it is logged. The retired marker above already makes the record non-authoritative.
|
|
2241
2467
|
await cleanupExtractionPaths({ name: componentName, dirPath: liveDirPath, logger: harper_logger_ts_1.default }, asideStagingDir, new Set([record, retiredMarkerPath])).catch((error) => harper_logger_ts_1.default.warn(`Settled ${componentName} but could not sweep ${record}:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
2242
2468
|
}
|
|
2469
|
+
return kept;
|
|
2243
2470
|
}
|
|
2244
2471
|
/**
|
|
2245
2472
|
* Clear an earlier failed recovery's verdict once this settlement has decided. Treated as CORRECTNESS, not
|
|
@@ -2280,7 +2507,10 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
|
|
|
2280
2507
|
const candidateExists = await exists(candidateDirPath);
|
|
2281
2508
|
const candidateComplete = await exists((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER));
|
|
2282
2509
|
const asideRecords = await inProgressAsideRecords(asideStagingDir);
|
|
2510
|
+
let rolledForward = false;
|
|
2511
|
+
let kept = [];
|
|
2283
2512
|
const rollForward = async () => {
|
|
2513
|
+
rolledForward = true;
|
|
2284
2514
|
if (!liveExists)
|
|
2285
2515
|
await renameThroughTransientHolder(candidateDirPath, liveDirPath);
|
|
2286
2516
|
// Unconditional, not only when THIS pass performed the rename: a crash after normal activation
|
|
@@ -2296,7 +2526,7 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
|
|
|
2296
2526
|
// this roll-forward just displaced. Failing the component closed and retrying at the next start is
|
|
2297
2527
|
// the cheaper mistake — the journal survives, so the verdict is re-derivable. Only the disk sweep
|
|
2298
2528
|
// inside is best-effort.
|
|
2299
|
-
await sweepAsideRecords(asideRecords, journal.component, liveDirPath, asideStagingDir);
|
|
2529
|
+
kept = await sweepAsideRecords(asideRecords, journal.component, liveDirPath, asideStagingDir);
|
|
2300
2530
|
};
|
|
2301
2531
|
const rollBack = async (restoreFrom) => {
|
|
2302
2532
|
if (restoreFrom) {
|
|
@@ -2376,7 +2606,7 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
|
|
|
2376
2606
|
`could not flush that to storage; a power loss could resurrect its activation journal:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
2377
2607
|
harper_logger_ts_1.default.info?.(`Returned the staged build ${(0, node_path_1.basename)(deploymentDirPath)} of ${journal.component} to dormant after an ` +
|
|
2378
2608
|
`activation that never moved its live tree aside`);
|
|
2379
|
-
return;
|
|
2609
|
+
return kept;
|
|
2380
2610
|
}
|
|
2381
2611
|
await rollBack();
|
|
2382
2612
|
}
|
|
@@ -2397,7 +2627,7 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
|
|
|
2397
2627
|
catch (error) {
|
|
2398
2628
|
harper_logger_ts_1.default.warn(`Settled the interrupted activation of ${journal.component} but could not flush its rollback record; ` +
|
|
2399
2629
|
`leaving the journal for the next start: ${errorMessage(error)}`);
|
|
2400
|
-
return;
|
|
2630
|
+
return kept;
|
|
2401
2631
|
}
|
|
2402
2632
|
// Best-effort, matching the activation path: the activation is settled by this point, so a transient
|
|
2403
2633
|
// EBUSY removing staging must not throw out of the recovery pass and take the other components with it.
|
|
@@ -2406,8 +2636,11 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
|
|
|
2406
2636
|
// the stale marker and failed it closed.
|
|
2407
2637
|
await clearUnsettledVerdict(deploymentDirPath, journal.component);
|
|
2408
2638
|
await (0, promises_1.rm)(journalPath, { force: true }).catch((error) => harper_logger_ts_1.default.warn(`Settled ${journal.component} but could not remove its activation journal:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
2639
|
+
if (rolledForward && (await keepsDeploymentRecord(deploymentDirPath)))
|
|
2640
|
+
return kept;
|
|
2409
2641
|
await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => harper_logger_ts_1.default.warn(`Settled ${journal.component} but could not clean up its staging directory:`, (0, harper_logger_ts_1.errorForLog)(error)));
|
|
2410
2642
|
await (0, promises_1.rmdir)((0, node_path_1.dirname)(deploymentDirPath)).catch(() => { });
|
|
2643
|
+
return kept;
|
|
2411
2644
|
}
|
|
2412
2645
|
/** Mark a candidate build+validation complete. Idempotent, so a retried activation is not a failure. */
|
|
2413
2646
|
/**
|
|
@@ -2725,14 +2958,21 @@ async function activateCandidateApplication(application, deploymentId, options =
|
|
|
2725
2958
|
}
|
|
2726
2959
|
const settledRecord = asidePath ?? priorAbsentRecordPath;
|
|
2727
2960
|
let retired = false;
|
|
2961
|
+
let keptDeploymentId;
|
|
2728
2962
|
// Skipped entirely when the swap is not known to be on storage, so the journal below survives.
|
|
2729
2963
|
if (swapDurable) {
|
|
2730
2964
|
try {
|
|
2731
2965
|
const retiredMarkerPath = await retireExtractionAside(settledRecord);
|
|
2732
|
-
//
|
|
2733
|
-
//
|
|
2734
|
-
|
|
2735
|
-
|
|
2966
|
+
// A tree nothing can keep is swept, or the components root grows by a component version per deploy; one
|
|
2967
|
+
// whose keep FAILED stays retired here for a retry.
|
|
2968
|
+
const retention = asidePath
|
|
2969
|
+
? await retainDisplacedRelease((0, node_path_1.dirname)(liveDirPath), application.name, asidePath)
|
|
2970
|
+
: { verdict: 'ineligible' };
|
|
2971
|
+
if (retention.verdict === 'kept')
|
|
2972
|
+
keptDeploymentId = retention.deploymentId;
|
|
2973
|
+
if (retention.verdict !== 'failed') {
|
|
2974
|
+
await cleanupExtractionPaths(application, asideStagingDir, new Set([settledRecord, retiredMarkerPath]));
|
|
2975
|
+
}
|
|
2736
2976
|
// Before the journal goes: if the journal's removal persists but the record's does not, startup sees
|
|
2737
2977
|
// an in-progress aside with no journal and the legacy pass restores the old tree over the new one.
|
|
2738
2978
|
await syncDirectory(asideStagingDir);
|
|
@@ -2747,8 +2987,23 @@ async function activateCandidateApplication(application, deploymentId, options =
|
|
|
2747
2987
|
// `.in-progress-*` record still names the displaced tree lets the legacy pass restore the old tree.
|
|
2748
2988
|
if (retired) {
|
|
2749
2989
|
await (0, promises_1.rm)(journalPath, { force: true }).catch((error) => application.logger.warn(`Deployed ${application.name} but could not remove its activation journal:`, error));
|
|
2750
|
-
|
|
2751
|
-
|
|
2990
|
+
if (!(await keepsDeploymentRecord(deploymentDirPath))) {
|
|
2991
|
+
await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => application.logger.warn(`Deployed ${application.name} but could not clean up its staging directory:`, error));
|
|
2992
|
+
await (0, promises_1.rmdir)((0, node_path_1.dirname)(deploymentDirPath)).catch(() => { });
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
// Only here, where the request's own artifact is already live: a prune inside settlement could evict the
|
|
2996
|
+
// artifact a request's preamble pinned. Best-effort, like every prune.
|
|
2997
|
+
if (keptDeploymentId !== undefined) {
|
|
2998
|
+
const maxCount = getStagingRetentionMaxCount();
|
|
2999
|
+
try {
|
|
3000
|
+
const builds = await dormantBuildsOf((0, node_path_1.dirname)(liveDirPath), application.name);
|
|
3001
|
+
if (builds.length > maxCount)
|
|
3002
|
+
await pruneDormantBuilds(application.name, builds, maxCount, keptDeploymentId);
|
|
3003
|
+
}
|
|
3004
|
+
catch (error) {
|
|
3005
|
+
application.logger.warn(`Deployed ${application.name} but could not bound its kept releases:`, error);
|
|
3006
|
+
}
|
|
2752
3007
|
}
|
|
2753
3008
|
}
|
|
2754
3009
|
/**
|
|
@@ -2944,6 +3199,10 @@ async function buildCandidateApplication(application, deploymentId, options = {}
|
|
|
2944
3199
|
// deployer's git token. `prepareApplication`'s finally still calls this; it is idempotent.
|
|
2945
3200
|
await application.cleanupGitCredentialSession();
|
|
2946
3201
|
await installApplication(application, candidateDirPath);
|
|
3202
|
+
// After the install, which can rewrite the tree; never through a link, whose target is not the deploy's to write.
|
|
3203
|
+
if (resolved.kind !== 'link') {
|
|
3204
|
+
await writeDeploymentProvenance(candidateDirPath, application.name, deploymentId);
|
|
3205
|
+
}
|
|
2947
3206
|
return candidateDirPath;
|
|
2948
3207
|
}
|
|
2949
3208
|
catch (error) {
|
|
@@ -2996,6 +3255,21 @@ async function recoverOrCleanupStaleExtractionPaths(application, asideStagingDir
|
|
|
2996
3255
|
(recoveryRecords.length > 1 ? `; discarded ${recoveryRecords.length - 1} older recovery candidates` : ''));
|
|
2997
3256
|
return;
|
|
2998
3257
|
}
|
|
3258
|
+
// Each retired tree left here is a displaced release: kept if it can be — the retry of a keep that failed — and
|
|
3259
|
+
// swept only when it cannot.
|
|
3260
|
+
for (const entry of entries) {
|
|
3261
|
+
if (!entry.isDirectory() || !entry.name.startsWith(IN_PROGRESS_ASIDE_PREFIX))
|
|
3262
|
+
continue;
|
|
3263
|
+
const treePath = (0, node_path_1.join)(asideStagingDir, entry.name);
|
|
3264
|
+
const retention = await retainDisplacedRelease((0, node_path_1.dirname)(application.dirPath), application.name, treePath).catch((error) => {
|
|
3265
|
+
application.logger.warn(`Could not keep the previous release at ${treePath}; leaving it in place:`, error);
|
|
3266
|
+
return { verdict: 'failed' };
|
|
3267
|
+
});
|
|
3268
|
+
if (retention.verdict === 'failed') {
|
|
3269
|
+
paths.delete(treePath);
|
|
3270
|
+
paths.delete(retiredMarkerForAside(treePath));
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
2999
3273
|
await cleanupExtractionPaths(application, asideStagingDir, paths);
|
|
3000
3274
|
}
|
|
3001
3275
|
function isPriorAbsentRecoveryRecord(entry) {
|
|
@@ -3163,9 +3437,11 @@ async function retireComponentDirectory(componentDirPath, componentName = (0, no
|
|
|
3163
3437
|
},
|
|
3164
3438
|
async discard() {
|
|
3165
3439
|
await cleanupExtractionPaths({ name: componentName, dirPath: componentDirPath, logger: componentLogger }, asideStagingDir, new Set([droppedPath]));
|
|
3166
|
-
// A dropped component has no next deploy to bound its dormant builds
|
|
3440
|
+
// A dropped component has no next deploy to bound its dormant builds, and nothing left for its records to
|
|
3441
|
+
// describe.
|
|
3167
3442
|
try {
|
|
3168
3443
|
await pruneDormantBuilds(componentName, await dormantBuildsOf((0, node_path_1.dirname)(componentDirPath), componentName), 0);
|
|
3444
|
+
await reclaimDeploymentRecordsOf((0, node_path_1.dirname)(componentDirPath), componentName);
|
|
3169
3445
|
}
|
|
3170
3446
|
catch (error) {
|
|
3171
3447
|
componentLogger.warn(`Dropped ${componentName} but could not reclaim its dormant staged builds:`, (0, harper_logger_ts_1.errorForLog)(error));
|
|
@@ -3173,6 +3449,33 @@ async function retireComponentDirectory(componentDirPath, componentName = (0, no
|
|
|
3173
3449
|
},
|
|
3174
3450
|
};
|
|
3175
3451
|
}
|
|
3452
|
+
/** Journaled directories stay: an activation nobody settled is evidence, not litter. */
|
|
3453
|
+
async function reclaimDeploymentRecordsOf(componentsRootDirPath, componentName) {
|
|
3454
|
+
const stagingRoot = (0, node_path_1.join)(componentsRootDirPath, exports.DEPLOY_STAGING_DIR);
|
|
3455
|
+
const entries = await (0, promises_1.readdir)(stagingRoot, { withFileTypes: true }).catch((error) => {
|
|
3456
|
+
if (error?.code === 'ENOENT')
|
|
3457
|
+
return [];
|
|
3458
|
+
throw error;
|
|
3459
|
+
});
|
|
3460
|
+
await sweepAbandonedClaims(stagingRoot, entries, componentName);
|
|
3461
|
+
for (const entry of entries) {
|
|
3462
|
+
if (!isDeploymentDirectoryEntry(entry))
|
|
3463
|
+
continue;
|
|
3464
|
+
const deploymentDirPath = (0, node_path_1.join)(stagingRoot, entry.name);
|
|
3465
|
+
try {
|
|
3466
|
+
if ((await candidateComponentName(deploymentDirPath)) !== componentName)
|
|
3467
|
+
continue;
|
|
3468
|
+
if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, ACTIVATION_JOURNAL)))
|
|
3469
|
+
continue;
|
|
3470
|
+
if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, componentName)))
|
|
3471
|
+
continue;
|
|
3472
|
+
await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
3473
|
+
}
|
|
3474
|
+
catch (error) {
|
|
3475
|
+
harper_logger_ts_1.default.warn(`Could not reclaim the deployment record ${deploymentDirPath} of ${componentName}:`, (0, harper_logger_ts_1.errorForLog)(error));
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3176
3479
|
async function cleanupExtractionPaths(application, asideStagingDir, paths) {
|
|
3177
3480
|
const retiredMarkers = [];
|
|
3178
3481
|
for (const path of paths) {
|
|
@@ -3672,6 +3975,7 @@ class Application {
|
|
|
3672
3975
|
isNewComponent = true;
|
|
3673
3976
|
packageMetadataChanged = false;
|
|
3674
3977
|
installationIsOpaque = false;
|
|
3978
|
+
alreadyActive = false;
|
|
3675
3979
|
constructor({ name, payload, packageIdentifier, install, onInstallLine, credentials }) {
|
|
3676
3980
|
this.name = name;
|
|
3677
3981
|
this.payload = payload;
|
|
@@ -3852,6 +4156,15 @@ async function prepareApplication(application, options = {}) {
|
|
|
3852
4156
|
throw error;
|
|
3853
4157
|
}));
|
|
3854
4158
|
if (mode === 'activate') {
|
|
4159
|
+
// A retry of an activation that swapped here and failed later or elsewhere: answered without a swap so
|
|
4160
|
+
// it can go on to the nodes still holding the artifact. Nothing can tell whether the running workers
|
|
4161
|
+
// loaded it, hence the restart request.
|
|
4162
|
+
if ((await readDeploymentProvenance(application.dirPath, application.name)) === artifactId) {
|
|
4163
|
+
application.alreadyActive = true;
|
|
4164
|
+
application.packageMetadataChanged = true;
|
|
4165
|
+
application.logger.debug?.(`Deployment ${artifactId} is already live; nothing to swap`);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
3855
4168
|
await activateStagedArtifact(application, artifactId, previousPackageMetadata, options);
|
|
3856
4169
|
return;
|
|
3857
4170
|
}
|
|
@@ -3896,8 +4209,23 @@ async function prepareApplication(application, options = {}) {
|
|
|
3896
4209
|
await syncArtifactAncestors(candidateDeploymentDirPath(application.dirPath, artifactId));
|
|
3897
4210
|
return;
|
|
3898
4211
|
}
|
|
3899
|
-
await markCandidateComplete(application.dirPath, artifactId, application.name);
|
|
3900
4212
|
const declared = options.describeArtifact?.();
|
|
4213
|
+
// The directory outlives the swap as the record a later displacement puts this release back into;
|
|
4214
|
+
// a link has no bytes of its own to put back.
|
|
4215
|
+
const described = declared && !(await (0, promises_1.lstat)(candidateDirPath)).isSymbolicLink() ? declared : undefined;
|
|
4216
|
+
if (described) {
|
|
4217
|
+
await writeArtifactDescriptor(application.dirPath, artifactId, {
|
|
4218
|
+
v: ARTIFACT_DESCRIPTOR_VERSION,
|
|
4219
|
+
component: application.name,
|
|
4220
|
+
rootConfig: described.rootConfig,
|
|
4221
|
+
installationIsOpaque: application.installationIsOpaque,
|
|
4222
|
+
isolated: described.isolated,
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
await markCandidateComplete(application.dirPath, artifactId, application.name);
|
|
4226
|
+
// The record is meant to outlive the swap, so its own entry has to be on storage, as a stage's is.
|
|
4227
|
+
if (described)
|
|
4228
|
+
await syncArtifactAncestors(candidateDeploymentDirPath(application.dirPath, artifactId));
|
|
3901
4229
|
await activateCandidateApplication(application, artifactId, {
|
|
3902
4230
|
rootConfig: declared ? (0, rootConfigPublication_ts_1.rootConfigEffectFromDeclaration)(declared.rootConfig) : { kind: 'keep' },
|
|
3903
4231
|
});
|
|
@@ -3969,11 +4297,14 @@ async function activateStagedArtifact(application, artifactId, previousPackageMe
|
|
|
3969
4297
|
throw unusable('an activation of it is unsettled');
|
|
3970
4298
|
}
|
|
3971
4299
|
const candidateStat = await presentOrAbsent(candidateDirPath);
|
|
3972
|
-
|
|
4300
|
+
// The record of a release that is neither live nor kept here: nothing on this node answers to the id.
|
|
4301
|
+
if (!candidateStat)
|
|
4302
|
+
throw missing('that release is no longer on this node');
|
|
4303
|
+
if (!candidateStat.isDirectory()) {
|
|
3973
4304
|
// Unlike an immediate deploy, a symlink is refused: a `file:` directory is linked rather than
|
|
3974
4305
|
// copied, so what it points at now is not what was certified. Staging rejects the source for the
|
|
3975
4306
|
// same reason; this is the other end of the same rule, for an artifact staged by an older build.
|
|
3976
|
-
throw unusable('its build tree is
|
|
4307
|
+
throw unusable('its build tree is a link rather than a copy');
|
|
3977
4308
|
}
|
|
3978
4309
|
const descriptor = await readArtifactDescriptor(deploymentDirPath, application.name);
|
|
3979
4310
|
if (!descriptor)
|