@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
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { fromBufferKey } from 'ordered-binary';
|
|
3
|
+
import { ClientError, DerivedIndexLagError, IndexRebuildingError, ServerError } from '../../utility/errors/hdbError.ts';
|
|
4
|
+
import {
|
|
5
|
+
derivedIndexTime,
|
|
6
|
+
readDerivedIndexCoverage,
|
|
7
|
+
readDerivedIndexReadiness,
|
|
8
|
+
subscribeDerivedIndexPublications,
|
|
9
|
+
type DerivedIndexCoverage,
|
|
10
|
+
type DerivedIndexPublicationSubscription,
|
|
11
|
+
type DerivedIndexReadiness,
|
|
12
|
+
} from '../derivedIndexRuntime.ts';
|
|
13
|
+
import type { FullTextDefinition } from '../fullTextSchema.ts';
|
|
14
|
+
import type { RocksTransactionLogStore } from '../RocksTransactionLogStore.ts';
|
|
15
|
+
import {
|
|
16
|
+
loadFullTextNativeBinding,
|
|
17
|
+
nativeFullTextSearchThreads,
|
|
18
|
+
validateFullTextQueryRuntimeInfo,
|
|
19
|
+
type NativeFullTextIndexConfiguration,
|
|
20
|
+
type NativeFullTextModule,
|
|
21
|
+
type NativeFullTextReader,
|
|
22
|
+
type NativeFullTextSearchExpression,
|
|
23
|
+
type NativeFullTextSearchMode,
|
|
24
|
+
} from './fullTextNativeBinding.ts';
|
|
25
|
+
import { nativeFullTextIndexPath } from './nativeFullTextDerivedIndexLifecycle.ts';
|
|
26
|
+
import { decodeFullTextPublication, type FullTextPublication } from './fullTextDerivedIndex.ts';
|
|
27
|
+
import { fullTextComparatorMode } from './fullTextQueryProtocol.ts';
|
|
28
|
+
export {
|
|
29
|
+
FULL_TEXT_QUERY_PAUSE_OPERATION,
|
|
30
|
+
FULL_TEXT_QUERY_RESUME_OPERATION,
|
|
31
|
+
fullTextComparatorMode,
|
|
32
|
+
} from './fullTextQueryProtocol.ts';
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_MAX_INDEX_LAG_MILLISECONDS,
|
|
35
|
+
MAX_WAIT_FOR_INDEX_MILLISECONDS,
|
|
36
|
+
type DerivedNativeIndexHost,
|
|
37
|
+
} from './hnswDerivedIndex.ts';
|
|
38
|
+
import { loggerWithTag } from '../../utility/logging/logger.ts';
|
|
39
|
+
|
|
40
|
+
const RAW_PAGE_SIZE = 256;
|
|
41
|
+
const MIN_RAW_PAGE_SIZE = 32;
|
|
42
|
+
const RAW_PAGE_OVERFETCH_FACTOR = 2;
|
|
43
|
+
const MAX_RELOAD_FAILURES_BEFORE_REOPEN = 3;
|
|
44
|
+
const HIGHLIGHT_BLOB_READ_TIMEOUT_MILLISECONDS = 5_000;
|
|
45
|
+
const READER_DRAIN_GRACE_MILLISECONDS = 1_000;
|
|
46
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
47
|
+
const logger = loggerWithTag('fulltext-query-index');
|
|
48
|
+
|
|
49
|
+
export type FullTextCondition = {
|
|
50
|
+
attribute?: string;
|
|
51
|
+
value?: string;
|
|
52
|
+
comparator?: string;
|
|
53
|
+
fields?: string[];
|
|
54
|
+
fullTextQuery?: NativeFullTextSearchExpression;
|
|
55
|
+
fullTextLeaves?: Array<{ text: string; mode: NativeFullTextSearchMode; fields?: string[] }>;
|
|
56
|
+
includeHighlights?: boolean;
|
|
57
|
+
maxIndexLagMilliseconds?: number;
|
|
58
|
+
waitForIndexMilliseconds?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ReaderSlot = {
|
|
62
|
+
reader: NativeFullTextReader;
|
|
63
|
+
definition: FullTextDefinition;
|
|
64
|
+
ownerEpoch: bigint;
|
|
65
|
+
publicationGeneration: bigint;
|
|
66
|
+
publication: FullTextPublication;
|
|
67
|
+
configurationRevision: number;
|
|
68
|
+
active: number;
|
|
69
|
+
retired: boolean;
|
|
70
|
+
closeOperation?: Promise<void>;
|
|
71
|
+
idleWaiters?: Array<() => void>;
|
|
72
|
+
reloadFailures: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type FullTextQueryHost = Pick<DerivedNativeIndexHost, 'readiness' | 'waitForCoverage' | 'requestRebuild'>;
|
|
76
|
+
|
|
77
|
+
class FullTextReaderPublicationError extends Error {}
|
|
78
|
+
|
|
79
|
+
const queryIndexesByPath = new Map<string, Set<FullTextQueryIndex>>();
|
|
80
|
+
type QueryPauses = Map<string, bigint>;
|
|
81
|
+
const pausedQueryPaths = new Map<string, QueryPauses>();
|
|
82
|
+
const retiredReadinessIdsByPath = new Map<string, Set<string>>();
|
|
83
|
+
const MAX_RETIRED_READINESS_IDS_PER_PATH = 64;
|
|
84
|
+
|
|
85
|
+
export type FullTextQueryIndexOptions = {
|
|
86
|
+
Table: any;
|
|
87
|
+
definition: FullTextDefinition;
|
|
88
|
+
auditStore: RocksTransactionLogStore;
|
|
89
|
+
readinessId: string;
|
|
90
|
+
indexId: string;
|
|
91
|
+
storePath: string;
|
|
92
|
+
storeName: string;
|
|
93
|
+
sourceGeneration: string;
|
|
94
|
+
limits: NativeFullTextIndexConfiguration['limits'];
|
|
95
|
+
binding?: NativeFullTextModule | (() => Promise<NativeFullTextModule>);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export class FullTextQueryIndex {
|
|
99
|
+
readonly filteredSearch = true;
|
|
100
|
+
readonly filePrimary = true;
|
|
101
|
+
readonly #options: FullTextQueryIndexOptions;
|
|
102
|
+
readonly #nativeOptions: NativeFullTextIndexConfiguration & {
|
|
103
|
+
path: string;
|
|
104
|
+
indexId: string;
|
|
105
|
+
generation: string;
|
|
106
|
+
};
|
|
107
|
+
#definition: FullTextDefinition;
|
|
108
|
+
#definitionSnapshot: string;
|
|
109
|
+
#configurationRevision = 0;
|
|
110
|
+
#binding?: NativeFullTextModule;
|
|
111
|
+
#readerSlot?: ReaderSlot;
|
|
112
|
+
#readerOperation?: Promise<ReaderSlot>;
|
|
113
|
+
#retiredReaderSlots = new Set<ReaderSlot>();
|
|
114
|
+
#maxSearchWindow?: number;
|
|
115
|
+
#maxAutocompleteResults?: number;
|
|
116
|
+
#maxSearchBudgetMilliseconds?: number;
|
|
117
|
+
#maxTraceRecords?: number;
|
|
118
|
+
#maxTraceSourceBytes?: number;
|
|
119
|
+
#closed = false;
|
|
120
|
+
#pauses: QueryPauses = new Map();
|
|
121
|
+
#foreignPausesObservedBeforeReady: QueryPauses = new Map();
|
|
122
|
+
#derivedHost?: FullTextQueryHost;
|
|
123
|
+
readonly #derivedHostReady = Promise.withResolvers<FullTextQueryHost>();
|
|
124
|
+
#publicationSubscription?: DerivedIndexPublicationSubscription;
|
|
125
|
+
#publicationRebuildRequested = false;
|
|
126
|
+
#refreshFailureWarned = false;
|
|
127
|
+
#unexpectedSearchFailureWarned = false;
|
|
128
|
+
|
|
129
|
+
constructor(options: FullTextQueryIndexOptions) {
|
|
130
|
+
this.#options = options;
|
|
131
|
+
this.#definition = options.definition;
|
|
132
|
+
this.#definitionSnapshot = queryDefinitionSnapshot(options.definition);
|
|
133
|
+
this.#nativeOptions = {
|
|
134
|
+
path: nativeFullTextIndexPath(options.storePath, options.storeName),
|
|
135
|
+
indexId: options.indexId,
|
|
136
|
+
generation: digestGeneration(options.sourceGeneration),
|
|
137
|
+
fields: options.definition.fields.map(({ name, weight }) => ({ name, weight })),
|
|
138
|
+
analyzer: options.definition.analyzer,
|
|
139
|
+
stopWords: options.definition.stopWords,
|
|
140
|
+
positions: options.definition.positions,
|
|
141
|
+
surfaceTerms: options.definition.surfaceTerms,
|
|
142
|
+
synonyms: options.definition.synonyms.map(({ source, replacements }) => ({
|
|
143
|
+
source,
|
|
144
|
+
replacements: [...replacements],
|
|
145
|
+
})),
|
|
146
|
+
limits: { ...options.limits },
|
|
147
|
+
};
|
|
148
|
+
let indexes = queryIndexesByPath.get(this.#nativeOptions.path);
|
|
149
|
+
if (!indexes) queryIndexesByPath.set(this.#nativeOptions.path, (indexes = new Set()));
|
|
150
|
+
indexes.add(this);
|
|
151
|
+
const retiredReadinessIds = retiredReadinessIdsByPath.get(this.#nativeOptions.path);
|
|
152
|
+
if (retiredReadinessIds?.delete(options.readinessId) && retiredReadinessIds.size === 0)
|
|
153
|
+
retiredReadinessIdsByPath.delete(this.#nativeOptions.path);
|
|
154
|
+
const pauses = pausedQueryPaths.get(this.#nativeOptions.path);
|
|
155
|
+
if (pauses) {
|
|
156
|
+
this.#pauses = new Map(pauses);
|
|
157
|
+
if (readDerivedIndexReadiness(options.auditStore, options.readinessId).state !== 'ready')
|
|
158
|
+
for (const [readinessId, ownerEpoch] of pauses)
|
|
159
|
+
if (readinessId !== options.readinessId) this.#foreignPausesObservedBeforeReady.set(readinessId, ownerEpoch);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
updateDefinition(definition: FullTextDefinition): void {
|
|
164
|
+
const snapshot = queryDefinitionSnapshot(definition);
|
|
165
|
+
if (snapshot === this.#definitionSnapshot) return;
|
|
166
|
+
this.#definition = definition;
|
|
167
|
+
this.#definitionSnapshot = snapshot;
|
|
168
|
+
this.#nativeOptions.fields = definition.fields.map(({ name, weight }) => ({ name, weight }));
|
|
169
|
+
this.#configurationRevision++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
estimateCount(): number {
|
|
173
|
+
const store = this.#options.Table.primaryStore;
|
|
174
|
+
const records =
|
|
175
|
+
typeof store.getEstimatedKeyCount === 'function' ? store.getEstimatedKeyCount() : store.getStats?.().entryCount;
|
|
176
|
+
return Math.max(1, Math.round((Number.isFinite(records) ? records : 20) * 0.05));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
search(
|
|
180
|
+
condition: FullTextCondition,
|
|
181
|
+
context: any,
|
|
182
|
+
options: {
|
|
183
|
+
filter?: (id: unknown, entry?: any) => boolean;
|
|
184
|
+
minResults?: number;
|
|
185
|
+
resultOffset?: number;
|
|
186
|
+
assertTransactionActive?: () => void;
|
|
187
|
+
} = {}
|
|
188
|
+
): Promise<Array<{ key: unknown; $score: number; $highlights?: Record<string, unknown>; loadedEntry: any }>> {
|
|
189
|
+
const maxIndexLagMilliseconds = condition.maxIndexLagMilliseconds ?? DEFAULT_MAX_INDEX_LAG_MILLISECONDS;
|
|
190
|
+
const waitForIndexMilliseconds = condition.waitForIndexMilliseconds ?? 0;
|
|
191
|
+
assertFreshnessOptions(maxIndexLagMilliseconds, waitForIndexMilliseconds);
|
|
192
|
+
const waiting = waitForIndexMilliseconds > 0;
|
|
193
|
+
let coverage: DerivedIndexCoverage | undefined;
|
|
194
|
+
const execute = async () => {
|
|
195
|
+
assertReady(
|
|
196
|
+
readDerivedIndexReadiness(this.#options.auditStore, this.#options.readinessId),
|
|
197
|
+
this.#definition.name
|
|
198
|
+
);
|
|
199
|
+
if (waiting) {
|
|
200
|
+
await context?.indexSearchStart;
|
|
201
|
+
context?.signal?.throwIfAborted();
|
|
202
|
+
const waitDeadline = performance.now() + waitForIndexMilliseconds;
|
|
203
|
+
const started = derivedIndexTime(this.#options.Table.primaryStore.rootStore);
|
|
204
|
+
let host = this.#derivedHost;
|
|
205
|
+
if (!host) {
|
|
206
|
+
const remaining = waitDeadline - performance.now();
|
|
207
|
+
if (remaining <= 0)
|
|
208
|
+
throw new DerivedIndexLagError('Timed out waiting for the local full-text query host; retry this query');
|
|
209
|
+
host = await this.#waitForDerivedHost(remaining, context?.signal);
|
|
210
|
+
}
|
|
211
|
+
const state = host?.readiness().state;
|
|
212
|
+
if (state !== 'ready')
|
|
213
|
+
throw new ServerError(
|
|
214
|
+
`Full-text index '${this.#definition.name}' is ${state === 'unavailable' ? 'unavailable' : 'rebuilding'}`,
|
|
215
|
+
503
|
|
216
|
+
);
|
|
217
|
+
context?.signal?.throwIfAborted();
|
|
218
|
+
if (options.minResults === 0) {
|
|
219
|
+
this.#unexpectedSearchFailureWarned = false;
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
if (this.#readCoverage(0).state !== 'current') {
|
|
223
|
+
await host.waitForCoverage(started, Math.max(0, waitDeadline - performance.now()), context?.signal);
|
|
224
|
+
}
|
|
225
|
+
context?.signal?.throwIfAborted();
|
|
226
|
+
const result = await this.#search(condition, context, options);
|
|
227
|
+
this.#unexpectedSearchFailureWarned = false;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
coverage = this.#queryCoverage(maxIndexLagMilliseconds);
|
|
231
|
+
const result = await this.#search(condition, context, options);
|
|
232
|
+
this.#unexpectedSearchFailureWarned = false;
|
|
233
|
+
return result;
|
|
234
|
+
};
|
|
235
|
+
const operation = execute().catch((error) => {
|
|
236
|
+
if (error === context?.signal?.reason) throw error;
|
|
237
|
+
if (nativeErrorNeedsRebuild(error)) {
|
|
238
|
+
this.#requestRebuild();
|
|
239
|
+
void this.#retireAllReaders().catch(() => undefined);
|
|
240
|
+
}
|
|
241
|
+
const publicError = publicSearchError(error, this.#definition.name);
|
|
242
|
+
if (
|
|
243
|
+
publicError !== error &&
|
|
244
|
+
publicError instanceof ServerError &&
|
|
245
|
+
publicError.statusCode === 500 &&
|
|
246
|
+
!this.#unexpectedSearchFailureWarned
|
|
247
|
+
) {
|
|
248
|
+
this.#unexpectedSearchFailureWarned = true;
|
|
249
|
+
logger.error?.(`Unexpected full-text search failure on '${this.#definition.name}'`, error);
|
|
250
|
+
}
|
|
251
|
+
throw publicError;
|
|
252
|
+
});
|
|
253
|
+
if (!waiting && coverage) Object.defineProperty(operation, 'indexCoverage', { value: coverage });
|
|
254
|
+
operation.catch(() => {});
|
|
255
|
+
return operation;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
attachDerivedHost(host: FullTextQueryHost): void {
|
|
259
|
+
this.#derivedHost = host;
|
|
260
|
+
this.#derivedHostReady.resolve(host);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async #waitForDerivedHost(timeout: number, signal?: AbortSignal): Promise<FullTextQueryHost> {
|
|
264
|
+
signal?.throwIfAborted();
|
|
265
|
+
let timer: NodeJS.Timeout;
|
|
266
|
+
let abort: (() => void) | undefined;
|
|
267
|
+
const unavailable = new Promise<never>((_resolve, reject) => {
|
|
268
|
+
timer = setTimeout(
|
|
269
|
+
() =>
|
|
270
|
+
reject(new DerivedIndexLagError('Timed out waiting for the local full-text query host; retry this query')),
|
|
271
|
+
timeout
|
|
272
|
+
);
|
|
273
|
+
if (signal) {
|
|
274
|
+
abort = () => reject(signal.reason ?? new Error('Index wait aborted'));
|
|
275
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
try {
|
|
279
|
+
return await Promise.race([this.#derivedHostReady.promise, unavailable]);
|
|
280
|
+
} finally {
|
|
281
|
+
clearTimeout(timer!);
|
|
282
|
+
if (abort) signal!.removeEventListener('abort', abort);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async close(): Promise<void> {
|
|
287
|
+
this.#closed = true;
|
|
288
|
+
const publicationSubscription = this.#publicationSubscription;
|
|
289
|
+
this.#publicationSubscription = undefined;
|
|
290
|
+
publicationSubscription?.close();
|
|
291
|
+
const retirement = this.#retireAllReaders();
|
|
292
|
+
void retirement.then(
|
|
293
|
+
() => this.#unregister(),
|
|
294
|
+
() => this.#unregister()
|
|
295
|
+
);
|
|
296
|
+
await withTimeout(
|
|
297
|
+
retirement,
|
|
298
|
+
(this.#maxSearchBudgetMilliseconds ?? 30_000) + READER_DRAIN_GRACE_MILLISECONDS,
|
|
299
|
+
() => new ServerError('Full-text reader drain did not settle before timeout', 503)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async pause(readinessId: string, ownerEpoch: bigint): Promise<void> {
|
|
304
|
+
const current = this.#pauses.get(readinessId);
|
|
305
|
+
if (current !== undefined && current > ownerEpoch) return;
|
|
306
|
+
if (current !== ownerEpoch) {
|
|
307
|
+
logger.info?.(
|
|
308
|
+
`Pausing full-text readers for '${this.#definition.name}' while '${readinessId}' resets at owner epoch ${ownerEpoch}`
|
|
309
|
+
);
|
|
310
|
+
if (
|
|
311
|
+
readinessId !== this.#options.readinessId &&
|
|
312
|
+
readDerivedIndexReadiness(this.#options.auditStore, this.#options.readinessId).state !== 'ready'
|
|
313
|
+
)
|
|
314
|
+
this.#foreignPausesObservedBeforeReady.set(readinessId, ownerEpoch);
|
|
315
|
+
else this.#foreignPausesObservedBeforeReady.delete(readinessId);
|
|
316
|
+
}
|
|
317
|
+
this.#pauses.set(readinessId, ownerEpoch);
|
|
318
|
+
await this.#retireAllReaders();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
resume(readinessId: string, ownerEpoch: bigint): void {
|
|
322
|
+
if (!this.#closed && this.#pauses.get(readinessId) === ownerEpoch) {
|
|
323
|
+
this.#pauses.delete(readinessId);
|
|
324
|
+
this.#foreignPausesObservedBeforeReady.delete(readinessId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async #search(
|
|
329
|
+
condition: FullTextCondition,
|
|
330
|
+
context: any,
|
|
331
|
+
options: {
|
|
332
|
+
filter?: (id: unknown, entry?: any) => boolean;
|
|
333
|
+
minResults?: number;
|
|
334
|
+
resultOffset?: number;
|
|
335
|
+
assertTransactionActive?: () => void;
|
|
336
|
+
}
|
|
337
|
+
): Promise<Array<{ key: unknown; $score: number; $highlights?: Record<string, unknown>; loadedEntry: any }>> {
|
|
338
|
+
const readiness = readDerivedIndexReadiness(this.#options.auditStore, this.#options.readinessId);
|
|
339
|
+
assertReady(readiness, this.#definition.name);
|
|
340
|
+
if (!this.#derivedHost) throw new IndexRebuildingError(`Full-text index '${this.#definition.name}' is not ready`);
|
|
341
|
+
if (options.minResults === 0) return [];
|
|
342
|
+
const publicationGeneration = this.#publicationRevision();
|
|
343
|
+
const acquisitionTimeout = Math.min(
|
|
344
|
+
MAX_WAIT_FOR_INDEX_MILLISECONDS,
|
|
345
|
+
Math.max(1, condition.waitForIndexMilliseconds ?? MAX_WAIT_FOR_INDEX_MILLISECONDS)
|
|
346
|
+
);
|
|
347
|
+
const acquisition = this.#acquireReader(readiness.ownerEpoch, publicationGeneration);
|
|
348
|
+
let abortListener: (() => void) | undefined;
|
|
349
|
+
const abort = context?.signal
|
|
350
|
+
? new Promise<never>((_resolve, reject) => {
|
|
351
|
+
abortListener = () => reject(context.signal.reason ?? new Error('Full-text search aborted'));
|
|
352
|
+
if (context.signal.aborted) abortListener();
|
|
353
|
+
else context.signal.addEventListener('abort', abortListener, { once: true });
|
|
354
|
+
})
|
|
355
|
+
: undefined;
|
|
356
|
+
let lease: Awaited<typeof acquisition>;
|
|
357
|
+
try {
|
|
358
|
+
lease = await withTimeout(
|
|
359
|
+
abort ? Promise.race([acquisition, abort]) : acquisition,
|
|
360
|
+
acquisitionTimeout,
|
|
361
|
+
() => new ServerError('Full-text reader acquisition exceeded the index-wait budget', 503)
|
|
362
|
+
);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
void acquisition.then(
|
|
365
|
+
(orphaned) => orphaned.release(),
|
|
366
|
+
() => {}
|
|
367
|
+
);
|
|
368
|
+
throw error;
|
|
369
|
+
} finally {
|
|
370
|
+
if (abortListener) context.signal.removeEventListener('abort', abortListener);
|
|
371
|
+
}
|
|
372
|
+
const reader = lease.reader;
|
|
373
|
+
try {
|
|
374
|
+
const deadline = performance.now() + this.#maxSearchBudgetMilliseconds!;
|
|
375
|
+
const maxSearchWindow = this.#maxSearchWindow!;
|
|
376
|
+
const query = condition.fullTextQuery ?? leafExpression(condition);
|
|
377
|
+
const autocomplete = expressionUsesAutocomplete(query);
|
|
378
|
+
const searchWindow = autocomplete ? this.#maxAutocompleteResults! : maxSearchWindow;
|
|
379
|
+
const bounded = options.minResults !== undefined;
|
|
380
|
+
const target = bounded ? Math.max(1, options.minResults!) : searchWindow;
|
|
381
|
+
if (target > searchWindow)
|
|
382
|
+
throw new ClientError(
|
|
383
|
+
`Full-text query exceeds the ${searchWindow}-result ${autocomplete ? 'autocomplete ' : ''}search window; reduce offset or limit`,
|
|
384
|
+
400
|
|
385
|
+
);
|
|
386
|
+
const accepted: Array<{
|
|
387
|
+
key: unknown;
|
|
388
|
+
$score: number;
|
|
389
|
+
$highlights?: Record<string, unknown>;
|
|
390
|
+
nativeId: string;
|
|
391
|
+
record: Record<string, unknown>;
|
|
392
|
+
recordEntry: any;
|
|
393
|
+
}> = [];
|
|
394
|
+
let offset = 0;
|
|
395
|
+
let moreMayExist = false;
|
|
396
|
+
let staleVersionHits = 0;
|
|
397
|
+
let expiredHits = 0;
|
|
398
|
+
const transaction = context && this.#options.Table._readTxnForContext(context);
|
|
399
|
+
while (accepted.length < target && offset < searchWindow) {
|
|
400
|
+
if (context?.signal?.aborted) throw context.signal.reason ?? new Error('Full-text search aborted');
|
|
401
|
+
const desiredPageSize = bounded
|
|
402
|
+
? Math.max(MIN_RAW_PAGE_SIZE, (target - accepted.length) * RAW_PAGE_OVERFETCH_FACTOR)
|
|
403
|
+
: RAW_PAGE_SIZE;
|
|
404
|
+
const limit = autocomplete ? searchWindow : Math.min(RAW_PAGE_SIZE, desiredPageSize, searchWindow - offset);
|
|
405
|
+
const result = await reader.search(
|
|
406
|
+
{
|
|
407
|
+
query,
|
|
408
|
+
offset,
|
|
409
|
+
limit,
|
|
410
|
+
...(!bounded && offset === 0 ? { exactTotal: true } : null),
|
|
411
|
+
},
|
|
412
|
+
{ remainingBudgetMilliseconds: remainingSearchBudget(deadline) }
|
|
413
|
+
);
|
|
414
|
+
if (!bounded && !autocomplete && result.totalRelation === 'exact' && result.total > searchWindow)
|
|
415
|
+
throw new ClientError(`Full-text query exceeds the ${searchWindow}-result search window; add a limit`, 400);
|
|
416
|
+
moreMayExist =
|
|
417
|
+
result.totalRelation === 'exact' ? offset + result.hits.length < result.total : result.hits.length === limit;
|
|
418
|
+
if (result.hits.length === 0) break;
|
|
419
|
+
for (const hit of result.hits) {
|
|
420
|
+
const key = decodeNativeId(hit.id, this.#options.Table.tableId);
|
|
421
|
+
options.assertTransactionActive?.();
|
|
422
|
+
const entry = this.#options.Table.primaryStore.getEntry(key, { transaction });
|
|
423
|
+
if (typeof hit.version !== 'string')
|
|
424
|
+
throw new ServerError('Full-text index returned a hit without a source version', 500);
|
|
425
|
+
if (!entry?.value || hit.version !== String(entry.version)) {
|
|
426
|
+
staleVersionHits++;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (this.#options.Table.isFullTextSearchEntryCurrent?.(entry) === false) {
|
|
430
|
+
staleVersionHits++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (entry.expiresAt !== undefined && entry.expiresAt < Date.now()) {
|
|
434
|
+
expiredHits++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (options.filter && !options.filter(key, entry)) continue;
|
|
438
|
+
accepted.push({ key, $score: hit.score, nativeId: hit.id, record: entry.value, recordEntry: entry });
|
|
439
|
+
if (accepted.length >= target) break;
|
|
440
|
+
}
|
|
441
|
+
offset += result.hits.length;
|
|
442
|
+
if (autocomplete) break;
|
|
443
|
+
if (!moreMayExist) break;
|
|
444
|
+
if (accepted.length < target) await new Promise((resolve) => setImmediate(resolve));
|
|
445
|
+
}
|
|
446
|
+
if (!bounded && moreMayExist)
|
|
447
|
+
throw new ClientError(`Full-text query exceeds the ${searchWindow}-result search window; add a limit`, 400);
|
|
448
|
+
if (bounded && accepted.length < target && moreMayExist && staleVersionHits > 0)
|
|
449
|
+
throw new DerivedIndexLagError(
|
|
450
|
+
`Full-text index '${this.#definition.name}' changed while searching; retry this query`
|
|
451
|
+
);
|
|
452
|
+
if (bounded && accepted.length < target && moreMayExist)
|
|
453
|
+
throw new ClientError(
|
|
454
|
+
expiredHits > 0
|
|
455
|
+
? `Expired full-text matches exhausted the ${searchWindow}-result search window; retry after index cleanup or narrow the query`
|
|
456
|
+
: `Full-text filters exhausted the ${searchWindow}-result search window; narrow the query or reduce offset`,
|
|
457
|
+
400
|
|
458
|
+
);
|
|
459
|
+
if (condition.includeHighlights && accepted.length > 0)
|
|
460
|
+
await this.#addHighlights(
|
|
461
|
+
reader,
|
|
462
|
+
lease.definition,
|
|
463
|
+
condition,
|
|
464
|
+
accepted.slice(options.resultOffset ?? 0),
|
|
465
|
+
deadline
|
|
466
|
+
);
|
|
467
|
+
return accepted.map((entry) => ({
|
|
468
|
+
key: entry.key,
|
|
469
|
+
$score: entry.$score,
|
|
470
|
+
loadedEntry: entry.recordEntry,
|
|
471
|
+
...(entry.$highlights ? { $highlights: entry.$highlights } : null),
|
|
472
|
+
}));
|
|
473
|
+
} finally {
|
|
474
|
+
await lease.release();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async #addHighlights(
|
|
479
|
+
reader: NativeFullTextReader,
|
|
480
|
+
definition: FullTextDefinition,
|
|
481
|
+
condition: FullTextCondition,
|
|
482
|
+
entries: Array<{
|
|
483
|
+
key: unknown;
|
|
484
|
+
$score: number;
|
|
485
|
+
$highlights?: Record<string, unknown>;
|
|
486
|
+
nativeId: string;
|
|
487
|
+
record: Record<string, unknown>;
|
|
488
|
+
recordEntry: any;
|
|
489
|
+
}>,
|
|
490
|
+
deadline: number
|
|
491
|
+
): Promise<void> {
|
|
492
|
+
const highlighting = definition.highlighting;
|
|
493
|
+
if (!highlighting) return;
|
|
494
|
+
const highlightFields = new Set(
|
|
495
|
+
definition.fields.filter((field) => field.highlight === true).map((field) => field.name)
|
|
496
|
+
);
|
|
497
|
+
if (highlightFields.size === 0) return;
|
|
498
|
+
const leaves = (condition.fullTextLeaves ?? [leafDescriptor(condition)]).map((leaf) => ({
|
|
499
|
+
...leaf,
|
|
500
|
+
fields: leaf.fields?.filter((field) => highlightFields.has(field)) ?? [...highlightFields],
|
|
501
|
+
}));
|
|
502
|
+
const byId = new Map(entries.map((entry) => [entry.nativeId, entry]));
|
|
503
|
+
const leafGroups = new Map<string, { fields: string[]; leaves: typeof leaves }>();
|
|
504
|
+
for (const leaf of leaves) {
|
|
505
|
+
if (leaf.fields.length === 0) continue;
|
|
506
|
+
const key = JSON.stringify([...leaf.fields].sort());
|
|
507
|
+
let group = leafGroups.get(key);
|
|
508
|
+
if (!group) {
|
|
509
|
+
const selected = new Set(leaf.fields);
|
|
510
|
+
const fields = definition.fields.map(({ name }) => name).filter((name) => selected.has(name));
|
|
511
|
+
leafGroups.set(key, (group = { fields, leaves: [] }));
|
|
512
|
+
}
|
|
513
|
+
group.leaves.push(leaf);
|
|
514
|
+
}
|
|
515
|
+
for (const { fields, leaves: fieldLeaves } of leafGroups.values()) {
|
|
516
|
+
const leafFields = new Set(fields);
|
|
517
|
+
for (const sourceEntries of traceEntryBatches(
|
|
518
|
+
entries,
|
|
519
|
+
definition,
|
|
520
|
+
leafFields,
|
|
521
|
+
this.#maxTraceRecords!,
|
|
522
|
+
this.#maxTraceSourceBytes!
|
|
523
|
+
)) {
|
|
524
|
+
const records = await Promise.all(
|
|
525
|
+
sourceEntries.map(async ({ nativeId, record }) => ({
|
|
526
|
+
id: nativeId,
|
|
527
|
+
fields: await sourceFields(record, definition, leafFields, deadline),
|
|
528
|
+
}))
|
|
529
|
+
);
|
|
530
|
+
for (const leaf of fieldLeaves) {
|
|
531
|
+
for (const traceRecords of traceSourceBatches(records, this.#maxTraceSourceBytes!)) {
|
|
532
|
+
const traced = await reader.traceMatches({ text: leaf.text, mode: leaf.mode, fields }, traceRecords, {
|
|
533
|
+
remainingBudgetMilliseconds: remainingSearchBudget(deadline),
|
|
534
|
+
snippets: true,
|
|
535
|
+
fragmentLength: highlighting.fragmentLength,
|
|
536
|
+
maxFragmentsPerValue: highlighting.maxFragments,
|
|
537
|
+
});
|
|
538
|
+
if (!traced.complete) throw new ServerError('Full-text index returned incomplete highlights', 500);
|
|
539
|
+
for (const record of traced.records) {
|
|
540
|
+
const entry = byId.get(record.id);
|
|
541
|
+
if (!entry) continue;
|
|
542
|
+
const highlights = (entry.$highlights ??= Object.create(null));
|
|
543
|
+
for (const value of record.values) {
|
|
544
|
+
if (!leafFields.has(value.field)) continue;
|
|
545
|
+
const values = (highlights[value.field] ??= []) as unknown[];
|
|
546
|
+
values.push({
|
|
547
|
+
valueIndex: value.valueIndex,
|
|
548
|
+
spans: value.spans,
|
|
549
|
+
...(value.fragments ? { fragments: value.fragments } : null),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async #acquireReader(
|
|
560
|
+
ownerEpoch: bigint,
|
|
561
|
+
publicationGeneration: bigint
|
|
562
|
+
): Promise<{ reader: NativeFullTextReader; definition: FullTextDefinition; release: () => Promise<void> }> {
|
|
563
|
+
const slot = await this.#readerFor(ownerEpoch, publicationGeneration, true);
|
|
564
|
+
let released = false;
|
|
565
|
+
return {
|
|
566
|
+
reader: slot.reader,
|
|
567
|
+
definition: slot.definition,
|
|
568
|
+
release: () => {
|
|
569
|
+
if (released) return Promise.resolve();
|
|
570
|
+
released = true;
|
|
571
|
+
slot.active--;
|
|
572
|
+
if (slot.active === 0) {
|
|
573
|
+
for (const resolve of slot.idleWaiters?.splice(0) ?? []) resolve();
|
|
574
|
+
if (slot.retired) {
|
|
575
|
+
try {
|
|
576
|
+
void this.#closeReaderSlot(slot).catch((error) => this.#warnReaderClose(error));
|
|
577
|
+
} catch {}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return Promise.resolve();
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#readerFor(ownerEpoch: bigint, publicationGeneration: bigint, reserve = false): Promise<ReaderSlot> {
|
|
586
|
+
if (this.#closed) return Promise.reject(new ServerError('Full-text index is closed', 503));
|
|
587
|
+
let ownReadiness: DerivedIndexReadiness | undefined;
|
|
588
|
+
let hasForeignPause = false;
|
|
589
|
+
for (const [readinessId, pausedEpoch] of this.#pauses) {
|
|
590
|
+
const readiness = readDerivedIndexReadiness(this.#options.auditStore, readinessId);
|
|
591
|
+
const completed = readiness.state === 'ready' && readiness.ownerEpoch >= pausedEpoch;
|
|
592
|
+
const successorCompleted =
|
|
593
|
+
readinessId !== this.#options.readinessId &&
|
|
594
|
+
this.#foreignPausesObservedBeforeReady.get(readinessId) === pausedEpoch &&
|
|
595
|
+
(ownReadiness ??= readDerivedIndexReadiness(this.#options.auditStore, this.#options.readinessId)).state ===
|
|
596
|
+
'ready';
|
|
597
|
+
if (completed || successorCompleted) {
|
|
598
|
+
resumeNativeFullTextQueryReaders(this.#nativeOptions.path, readinessId, pausedEpoch);
|
|
599
|
+
} else if (readinessId !== this.#options.readinessId) hasForeignPause = true;
|
|
600
|
+
}
|
|
601
|
+
if (hasForeignPause) {
|
|
602
|
+
const { auditStore, indexId, readinessId } = this.#options;
|
|
603
|
+
const lockKey = `derived-index:${indexId}:runner`;
|
|
604
|
+
if (auditStore.tryLock(lockKey)) {
|
|
605
|
+
try {
|
|
606
|
+
// Every generation holds this lock through reset and shutdown, so acquisition proves reset quiescence.
|
|
607
|
+
if (readDerivedIndexReadiness(auditStore, readinessId).state === 'ready')
|
|
608
|
+
for (const [pausedId, pausedEpoch] of [...this.#pauses])
|
|
609
|
+
if (pausedId !== readinessId)
|
|
610
|
+
resumeNativeFullTextQueryReaders(this.#nativeOptions.path, pausedId, pausedEpoch);
|
|
611
|
+
} finally {
|
|
612
|
+
auditStore.unlock(lockKey);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const blocked = this.#pauses.entries().next();
|
|
617
|
+
if (!blocked.done) {
|
|
618
|
+
const [readinessId, ownerEpoch] = blocked.value;
|
|
619
|
+
return Promise.reject(
|
|
620
|
+
new IndexRebuildingError(
|
|
621
|
+
`Full-text index '${this.#definition.name}' is waiting for reader fence '${readinessId}' at owner epoch ${ownerEpoch}`
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const current = this.#readerSlot;
|
|
626
|
+
if (
|
|
627
|
+
!this.#readerOperation &&
|
|
628
|
+
current &&
|
|
629
|
+
!current.retired &&
|
|
630
|
+
current.ownerEpoch === ownerEpoch &&
|
|
631
|
+
current.publicationGeneration >= publicationGeneration &&
|
|
632
|
+
current.configurationRevision === this.#configurationRevision
|
|
633
|
+
) {
|
|
634
|
+
if (reserve) current.active++;
|
|
635
|
+
return Promise.resolve(current);
|
|
636
|
+
}
|
|
637
|
+
if (this.#readerOperation)
|
|
638
|
+
return this.#readerOperation.then(() => this.#readerFor(ownerEpoch, publicationGeneration, reserve));
|
|
639
|
+
const configurationRevision = this.#configurationRevision;
|
|
640
|
+
this.#readerOperation = (async () => {
|
|
641
|
+
const existing = this.#readerSlot;
|
|
642
|
+
const existingMatchesIdentity =
|
|
643
|
+
existing?.ownerEpoch === ownerEpoch && existing.configurationRevision === configurationRevision;
|
|
644
|
+
// Installing this operation synchronously prevents leases after observing a newer publication.
|
|
645
|
+
const canReload = existing?.active === 0 && existingMatchesIdentity;
|
|
646
|
+
try {
|
|
647
|
+
let reader: NativeFullTextReader;
|
|
648
|
+
let publication: FullTextPublication;
|
|
649
|
+
if (canReload) {
|
|
650
|
+
const reloadedPublicationGeneration = this.#publicationRevision();
|
|
651
|
+
await existing.reader.reload();
|
|
652
|
+
existing.publication = this.#decodeReaderPublication(existing.reader);
|
|
653
|
+
existing.reloadFailures = 0;
|
|
654
|
+
existing.publicationGeneration = reloadedPublicationGeneration;
|
|
655
|
+
return existing;
|
|
656
|
+
} else {
|
|
657
|
+
const binding = await this.#getBinding();
|
|
658
|
+
reader = await binding.openNativeFullTextReader(this.#nativeOptions);
|
|
659
|
+
try {
|
|
660
|
+
publication = this.#decodeReaderPublication(reader);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
await reader.close().catch((closeError) => this.#warnReaderClose(closeError));
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
if (existing) this.#retireReaderSlot(existing);
|
|
666
|
+
}
|
|
667
|
+
const slot = {
|
|
668
|
+
reader,
|
|
669
|
+
definition: this.#definition,
|
|
670
|
+
ownerEpoch,
|
|
671
|
+
publicationGeneration,
|
|
672
|
+
publication,
|
|
673
|
+
configurationRevision,
|
|
674
|
+
active: 0,
|
|
675
|
+
retired: false,
|
|
676
|
+
reloadFailures: 0,
|
|
677
|
+
};
|
|
678
|
+
this.#readerSlot = slot;
|
|
679
|
+
return slot;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (canReload && error instanceof FullTextReaderPublicationError) {
|
|
682
|
+
this.#retireReaderSlot(existing);
|
|
683
|
+
} else if (canReload && ++existing.reloadFailures >= MAX_RELOAD_FAILURES_BEFORE_REOPEN) {
|
|
684
|
+
this.#retireReaderSlot(existing);
|
|
685
|
+
} else if (!canReload && existing && existing.active === 0 && !existingMatchesIdentity) {
|
|
686
|
+
this.#readerSlot = undefined;
|
|
687
|
+
this.#retireReaderSlot(existing);
|
|
688
|
+
}
|
|
689
|
+
throw error;
|
|
690
|
+
} finally {
|
|
691
|
+
this.#readerOperation = undefined;
|
|
692
|
+
}
|
|
693
|
+
})();
|
|
694
|
+
return this.#readerOperation.then(() => this.#readerFor(ownerEpoch, publicationGeneration, reserve));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#decodeReaderPublication(reader: NativeFullTextReader): FullTextPublication {
|
|
698
|
+
try {
|
|
699
|
+
if (reader.committedPayload === undefined) throw new Error('missing publication');
|
|
700
|
+
const publication = decodeFullTextPublication(reader.committedPayload);
|
|
701
|
+
if (!publication.cursor) throw new Error('missing durable cursor');
|
|
702
|
+
this.#publicationRebuildRequested = false;
|
|
703
|
+
return publication;
|
|
704
|
+
} catch (cause) {
|
|
705
|
+
this.#requestRebuild();
|
|
706
|
+
throw new FullTextReaderPublicationError(
|
|
707
|
+
`Full-text index '${this.#definition.name}' has no valid search checkpoint`,
|
|
708
|
+
{ cause }
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#refreshReader(openCold: boolean): void {
|
|
714
|
+
try {
|
|
715
|
+
if (this.#closed || (!openCold && !this.#readerSlot && !this.#readerOperation)) return;
|
|
716
|
+
const readiness = readDerivedIndexReadiness(this.#options.auditStore, this.#options.readinessId);
|
|
717
|
+
if (readiness.state !== 'ready') return;
|
|
718
|
+
void this.#readerFor(readiness.ownerEpoch, this.#publicationRevision()).catch((error) => {
|
|
719
|
+
if (nativeErrorNeedsRebuild(error)) this.#requestRebuild();
|
|
720
|
+
});
|
|
721
|
+
this.#refreshFailureWarned = false;
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (this.#refreshFailureWarned) return;
|
|
724
|
+
this.#refreshFailureWarned = true;
|
|
725
|
+
try {
|
|
726
|
+
logger.warn?.('Could not refresh a full-text reader after native publication', error);
|
|
727
|
+
} catch {}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
#publicationRevision(): bigint {
|
|
732
|
+
if (this.#closed) throw new ServerError('Full-text index is closed', 503);
|
|
733
|
+
if (!this.#publicationSubscription)
|
|
734
|
+
this.#publicationSubscription = subscribeDerivedIndexPublications(
|
|
735
|
+
this.#options.auditStore,
|
|
736
|
+
this.#options.readinessId,
|
|
737
|
+
() => this.#refreshReader(false)
|
|
738
|
+
);
|
|
739
|
+
return this.#publicationSubscription.revision();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
#requestRebuild(): void {
|
|
743
|
+
const host = this.#derivedHost;
|
|
744
|
+
if (!host || this.#publicationRebuildRequested) return;
|
|
745
|
+
try {
|
|
746
|
+
if (host.requestRebuild()) this.#publicationRebuildRequested = true;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
try {
|
|
749
|
+
logger.warn?.(`Could not request a rebuild for full-text index '${this.#definition.name}'`, error);
|
|
750
|
+
} catch {}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
#retireReaderSlot(slot: ReaderSlot): void {
|
|
755
|
+
if (slot.retired) return;
|
|
756
|
+
slot.retired = true;
|
|
757
|
+
this.#retiredReaderSlots.add(slot);
|
|
758
|
+
if (this.#readerSlot === slot) this.#readerSlot = undefined;
|
|
759
|
+
if (slot.active === 0) this.#closeReaderSlot(slot).catch((error) => this.#warnReaderClose(error));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#closeReaderSlot(slot: ReaderSlot): Promise<void> {
|
|
763
|
+
if (!slot.closeOperation) {
|
|
764
|
+
const operation = Promise.resolve()
|
|
765
|
+
.then(() => slot.reader.close())
|
|
766
|
+
.then((result) => {
|
|
767
|
+
this.#retiredReaderSlots.delete(slot);
|
|
768
|
+
if (result?.cleanupError) this.#warnReaderClose(result.cleanupError);
|
|
769
|
+
});
|
|
770
|
+
slot.closeOperation = operation;
|
|
771
|
+
void operation.catch(() => {
|
|
772
|
+
if (slot.closeOperation === operation) slot.closeOperation = undefined;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return slot.closeOperation;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async #retireAllReaders(): Promise<void> {
|
|
779
|
+
await this.#readerOperation?.catch(() => undefined);
|
|
780
|
+
if (this.#readerSlot) this.#retireReaderSlot(this.#readerSlot);
|
|
781
|
+
await Promise.all([...this.#retiredReaderSlots].map((slot) => this.#closeWhenIdle(slot)));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#unregister(): void {
|
|
785
|
+
const path = this.#nativeOptions.path;
|
|
786
|
+
const indexes = queryIndexesByPath.get(path);
|
|
787
|
+
indexes?.delete(this);
|
|
788
|
+
const sameReadinessRemains = [...(indexes ?? [])].some(
|
|
789
|
+
(index) => index.#options.readinessId === this.#options.readinessId
|
|
790
|
+
);
|
|
791
|
+
if (!sameReadinessRemains) rememberRetiredReadinessId(path, this.#options.readinessId);
|
|
792
|
+
if (!indexes || indexes.size === 0) {
|
|
793
|
+
queryIndexesByPath.delete(path);
|
|
794
|
+
pausedQueryPaths.delete(path);
|
|
795
|
+
retiredReadinessIdsByPath.delete(path);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (sameReadinessRemains) return;
|
|
799
|
+
const ownerEpoch = pausedQueryPaths.get(path)?.get(this.#options.readinessId);
|
|
800
|
+
if (ownerEpoch === undefined) return;
|
|
801
|
+
clearPathPause(path, this.#options.readinessId, ownerEpoch);
|
|
802
|
+
for (const index of indexes) index.resume(this.#options.readinessId, ownerEpoch);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async #closeWhenIdle(slot: ReaderSlot): Promise<void> {
|
|
806
|
+
if (slot.active > 0) await new Promise<void>((resolve) => (slot.idleWaiters ??= []).push(resolve));
|
|
807
|
+
await this.#closeReaderSlot(slot).catch((error) => this.#warnReaderClose(error));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
#warnReaderClose(error: unknown): void {
|
|
811
|
+
try {
|
|
812
|
+
logger.warn?.('Could not close a retired full-text reader; the reader remains retired', error);
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async #getBinding(): Promise<NativeFullTextModule> {
|
|
817
|
+
if (this.#binding) return this.#binding;
|
|
818
|
+
const configured = this.#options.binding;
|
|
819
|
+
const binding = configured
|
|
820
|
+
? typeof configured === 'function'
|
|
821
|
+
? await configured()
|
|
822
|
+
: configured
|
|
823
|
+
: await loadFullTextNativeBinding();
|
|
824
|
+
const info = validateFullTextQueryRuntimeInfo(await binding.runtimeInfo());
|
|
825
|
+
this.#nativeOptions.limits.searchThreads = nativeFullTextSearchThreads(
|
|
826
|
+
info,
|
|
827
|
+
this.#nativeOptions.limits.searchThreads
|
|
828
|
+
);
|
|
829
|
+
this.#maxSearchWindow = info.limits.maxSearchWindow;
|
|
830
|
+
this.#maxAutocompleteResults = info.limits.maxAutocompleteResults;
|
|
831
|
+
this.#maxSearchBudgetMilliseconds = info.limits.maxSearchBudgetMilliseconds;
|
|
832
|
+
this.#maxTraceRecords = info.limits.maxTraceRecords;
|
|
833
|
+
this.#maxTraceSourceBytes = info.limits.maxTraceSourceBytes;
|
|
834
|
+
return (this.#binding = binding);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
#queryCoverage(maxLagMilliseconds: number): DerivedIndexCoverage {
|
|
838
|
+
const coverage = this.#readCoverage(maxLagMilliseconds);
|
|
839
|
+
if (!coverage || coverage.state === 'unknown') {
|
|
840
|
+
this.#refreshReader(true);
|
|
841
|
+
const age = coverage?.lagUpperBoundMilliseconds;
|
|
842
|
+
throw new DerivedIndexLagError(
|
|
843
|
+
`Cannot certify full-text index coverage within ${maxLagMilliseconds} ms` +
|
|
844
|
+
(age === undefined ? '; freshness is unknown' : `; last certified ${Math.ceil(age)} ms ago`) +
|
|
845
|
+
'; retry this query'
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
return coverage;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
#readCoverage(maxLagMilliseconds: number): DerivedIndexCoverage {
|
|
852
|
+
return readDerivedIndexCoverage(
|
|
853
|
+
this.#options.auditStore,
|
|
854
|
+
this.#options.readinessId,
|
|
855
|
+
() => this.#readerSlot?.publication.cursor,
|
|
856
|
+
maxLagMilliseconds
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export async function pauseNativeFullTextQueryReaders(
|
|
862
|
+
path: string,
|
|
863
|
+
readinessId: string,
|
|
864
|
+
ownerEpoch: bigint,
|
|
865
|
+
allowUnregisteredReadiness = false
|
|
866
|
+
): Promise<void> {
|
|
867
|
+
const indexes = queryIndexesByPath.get(path);
|
|
868
|
+
if (!indexes || indexes.size === 0) return;
|
|
869
|
+
if (!allowUnregisteredReadiness && retiredReadinessIdsByPath.get(path)?.has(readinessId)) return;
|
|
870
|
+
let pauses = pausedQueryPaths.get(path);
|
|
871
|
+
if (!pauses) pausedQueryPaths.set(path, (pauses = new Map()));
|
|
872
|
+
const current = pauses.get(readinessId);
|
|
873
|
+
if (current === undefined || current <= ownerEpoch) pauses.set(readinessId, ownerEpoch);
|
|
874
|
+
await Promise.all([...indexes].map((index) => index.pause(readinessId, ownerEpoch)));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function resumeNativeFullTextQueryReaders(path: string, readinessId: string, ownerEpoch: bigint): void {
|
|
878
|
+
clearPathPause(path, readinessId, ownerEpoch);
|
|
879
|
+
for (const index of queryIndexesByPath.get(path) ?? []) index.resume(readinessId, ownerEpoch);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function clearPathPause(path: string, readinessId: string, ownerEpoch: bigint): void {
|
|
883
|
+
const pauses = pausedQueryPaths.get(path);
|
|
884
|
+
if (pauses?.get(readinessId) !== ownerEpoch) return;
|
|
885
|
+
pauses.delete(readinessId);
|
|
886
|
+
if (pauses.size === 0) pausedQueryPaths.delete(path);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function rememberRetiredReadinessId(path: string, readinessId: string): void {
|
|
890
|
+
let readinessIds = retiredReadinessIdsByPath.get(path);
|
|
891
|
+
if (!readinessIds) retiredReadinessIdsByPath.set(path, (readinessIds = new Set()));
|
|
892
|
+
readinessIds.delete(readinessId);
|
|
893
|
+
readinessIds.add(readinessId);
|
|
894
|
+
if (readinessIds.size > MAX_RETIRED_READINESS_IDS_PER_PATH) readinessIds.delete(readinessIds.values().next().value!);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function assertFreshnessOptions(maxIndexLagMilliseconds: number, waitForIndexMilliseconds: number): void {
|
|
898
|
+
if (
|
|
899
|
+
typeof maxIndexLagMilliseconds !== 'number' ||
|
|
900
|
+
!Number.isFinite(maxIndexLagMilliseconds) ||
|
|
901
|
+
maxIndexLagMilliseconds < 0
|
|
902
|
+
)
|
|
903
|
+
throw new ClientError('maxIndexLagMilliseconds must be a finite nonnegative number', 400);
|
|
904
|
+
if (
|
|
905
|
+
typeof waitForIndexMilliseconds !== 'number' ||
|
|
906
|
+
!Number.isFinite(waitForIndexMilliseconds) ||
|
|
907
|
+
waitForIndexMilliseconds < 0 ||
|
|
908
|
+
waitForIndexMilliseconds > MAX_WAIT_FOR_INDEX_MILLISECONDS
|
|
909
|
+
)
|
|
910
|
+
throw new ClientError(
|
|
911
|
+
`waitForIndexMilliseconds must be a finite number between 0 and ${MAX_WAIT_FOR_INDEX_MILLISECONDS}`,
|
|
912
|
+
400
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function leafExpression(condition: FullTextCondition): NativeFullTextSearchExpression {
|
|
917
|
+
const leaf = leafDescriptor(condition);
|
|
918
|
+
return { text: leaf.text, mode: leaf.mode, ...(leaf.fields ? { fields: leaf.fields } : null) };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function expressionUsesAutocomplete(expression: NativeFullTextSearchExpression): boolean {
|
|
922
|
+
if ('text' in expression) return expression.mode === 'prefix' || expression.mode === 'fuzzy-prefix';
|
|
923
|
+
if (expression.operator === 'not') return expressionUsesAutocomplete(expression.clause);
|
|
924
|
+
return expression.clauses.some(expressionUsesAutocomplete);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function leafDescriptor(condition: FullTextCondition): {
|
|
928
|
+
text: string;
|
|
929
|
+
mode: NativeFullTextSearchMode;
|
|
930
|
+
fields?: string[];
|
|
931
|
+
} {
|
|
932
|
+
const mode = fullTextComparatorMode(condition.comparator);
|
|
933
|
+
if (!mode || typeof condition.value !== 'string' || condition.value.length === 0)
|
|
934
|
+
throw new ClientError('Full-text conditions require a supported comparator and non-empty string value', 400);
|
|
935
|
+
return { text: condition.value, mode, ...(condition.fields ? { fields: condition.fields } : null) };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function sourceFields(
|
|
939
|
+
record: Record<string, unknown>,
|
|
940
|
+
definition: FullTextDefinition,
|
|
941
|
+
selected: ReadonlySet<string>,
|
|
942
|
+
queryDeadline: number
|
|
943
|
+
): Promise<Record<string, string | string[]>> {
|
|
944
|
+
const fields: Record<string, string | string[]> = Object.create(null);
|
|
945
|
+
const deadline = Math.min(queryDeadline, performance.now() + HIGHLIGHT_BLOB_READ_TIMEOUT_MILLISECONDS);
|
|
946
|
+
for (const source of definition.fields) {
|
|
947
|
+
if (!selected.has(source.name)) continue;
|
|
948
|
+
const value = record[source.name];
|
|
949
|
+
if (typeof value === 'string') fields[source.name] = value;
|
|
950
|
+
else if (Array.isArray(value)) fields[source.name] = value.filter((entry) => typeof entry === 'string');
|
|
951
|
+
else if (source.mediaType === 'text/plain' && value instanceof Blob) {
|
|
952
|
+
const remaining = deadline - performance.now();
|
|
953
|
+
if (remaining <= 0) throw new DerivedIndexLagError('Full-text highlight source read timed out; retry the query');
|
|
954
|
+
let bytes: ArrayBuffer;
|
|
955
|
+
try {
|
|
956
|
+
bytes = await withTimeout(value.arrayBuffer(), remaining);
|
|
957
|
+
} catch {
|
|
958
|
+
throw new DerivedIndexLagError('Full-text highlight source is temporarily unavailable; retry the query');
|
|
959
|
+
}
|
|
960
|
+
fields[source.name] = UTF8_DECODER.decode(bytes);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return fields;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function traceSourceBytes(fields: Record<string, string | string[]>): number {
|
|
967
|
+
let bytes = 0;
|
|
968
|
+
for (const value of Object.values(fields)) {
|
|
969
|
+
if (typeof value === 'string') bytes += Buffer.byteLength(value);
|
|
970
|
+
else for (const item of value) bytes += Buffer.byteLength(item);
|
|
971
|
+
}
|
|
972
|
+
return bytes;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function traceEntryBytes(
|
|
976
|
+
record: Record<string, unknown>,
|
|
977
|
+
definition: FullTextDefinition,
|
|
978
|
+
selected: ReadonlySet<string>
|
|
979
|
+
): number {
|
|
980
|
+
let bytes = 0;
|
|
981
|
+
for (const source of definition.fields) {
|
|
982
|
+
if (!selected.has(source.name)) continue;
|
|
983
|
+
const value = record[source.name];
|
|
984
|
+
if (typeof value === 'string') bytes += Buffer.byteLength(value);
|
|
985
|
+
else if (Array.isArray(value)) {
|
|
986
|
+
for (const item of value) if (typeof item === 'string') bytes += Buffer.byteLength(item);
|
|
987
|
+
} else if (source.mediaType === 'text/plain' && value instanceof Blob) bytes += value.size;
|
|
988
|
+
}
|
|
989
|
+
return bytes;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function traceEntryBatches<T extends { record: Record<string, unknown> }>(
|
|
993
|
+
entries: T[],
|
|
994
|
+
definition: FullTextDefinition,
|
|
995
|
+
selected: ReadonlySet<string>,
|
|
996
|
+
maxRecords: number,
|
|
997
|
+
maxBytes: number
|
|
998
|
+
): T[][] {
|
|
999
|
+
const batches: T[][] = [];
|
|
1000
|
+
let batch: T[] = [];
|
|
1001
|
+
let batchBytes = 0;
|
|
1002
|
+
for (const entry of entries) {
|
|
1003
|
+
const bytes = traceEntryBytes(entry.record, definition, selected);
|
|
1004
|
+
if (bytes > maxBytes)
|
|
1005
|
+
throw new ClientError(`Full-text highlight source exceeds the ${maxBytes}-byte native trace limit`, 400);
|
|
1006
|
+
if (batch.length > 0 && (batch.length >= maxRecords || batchBytes + bytes > maxBytes)) {
|
|
1007
|
+
batches.push(batch);
|
|
1008
|
+
batch = [];
|
|
1009
|
+
batchBytes = 0;
|
|
1010
|
+
}
|
|
1011
|
+
batch.push(entry);
|
|
1012
|
+
batchBytes += bytes;
|
|
1013
|
+
}
|
|
1014
|
+
if (batch.length > 0) batches.push(batch);
|
|
1015
|
+
return batches;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function traceSourceBatches<T extends { fields: Record<string, string | string[]> }>(
|
|
1019
|
+
records: T[],
|
|
1020
|
+
maxBytes: number
|
|
1021
|
+
): T[][] {
|
|
1022
|
+
const batches: T[][] = [];
|
|
1023
|
+
let batch: T[] = [];
|
|
1024
|
+
let batchBytes = 0;
|
|
1025
|
+
for (const record of records) {
|
|
1026
|
+
const bytes = traceSourceBytes(record.fields);
|
|
1027
|
+
if (bytes > maxBytes)
|
|
1028
|
+
throw new ClientError(`Full-text highlight source exceeds the ${maxBytes}-byte native trace limit`, 400);
|
|
1029
|
+
if (batch.length > 0 && batchBytes + bytes > maxBytes) {
|
|
1030
|
+
batches.push(batch);
|
|
1031
|
+
batch = [];
|
|
1032
|
+
batchBytes = 0;
|
|
1033
|
+
}
|
|
1034
|
+
batch.push(record);
|
|
1035
|
+
batchBytes += bytes;
|
|
1036
|
+
}
|
|
1037
|
+
if (batch.length > 0) batches.push(batch);
|
|
1038
|
+
return batches;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function decodeNativeId(id: string, tableId: number): unknown {
|
|
1042
|
+
const prefix = `${tableId}.`;
|
|
1043
|
+
if (!id.startsWith(prefix)) throw new ServerError('Full-text index returned an invalid record identity', 500);
|
|
1044
|
+
try {
|
|
1045
|
+
return fromBufferKey(Buffer.from(id.slice(prefix.length), 'base64url'));
|
|
1046
|
+
} catch {
|
|
1047
|
+
throw new ServerError('Full-text index returned an invalid record identity', 500);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function assertReady(readiness: DerivedIndexReadiness, name: string): void {
|
|
1052
|
+
if (readiness.state === 'ready') return;
|
|
1053
|
+
if (readiness.state === 'unavailable') throw new ServerError(`Full-text index '${name}' is unavailable`, 503);
|
|
1054
|
+
throw new IndexRebuildingError(`Full-text index '${name}' is not ready`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function publicSearchError(error: unknown, name: string): Error {
|
|
1058
|
+
if (error instanceof FullTextReaderPublicationError)
|
|
1059
|
+
return new IndexRebuildingError(`Full-text index '${name}' is not ready`);
|
|
1060
|
+
if (error instanceof ClientError || error instanceof ServerError) return error;
|
|
1061
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
1062
|
+
if (code === 'E_RELOAD_FAILED')
|
|
1063
|
+
return new DerivedIndexLagError(
|
|
1064
|
+
`Full-text index '${name}' could not refresh its search snapshot; retry this query`
|
|
1065
|
+
);
|
|
1066
|
+
if (typeof code === 'string' && REBUILD_REQUIRED_CODES.has(code))
|
|
1067
|
+
return new IndexRebuildingError(`Full-text index '${name}' is not ready`);
|
|
1068
|
+
if (code === 'E_QUEUE_FULL' || code === 'E_TIMEOUT' || code === 'E_RESOURCE_LIMIT')
|
|
1069
|
+
return new ServerError(`Full-text index '${name}' is busy`, 503);
|
|
1070
|
+
if (code === 'E_INVALID_ARGUMENT' || code === 'E_PREFIX_TOO_BROAD')
|
|
1071
|
+
return new ClientError(`Full-text query on '${name}' is invalid`, 400);
|
|
1072
|
+
if (code === 'E_RESULT_TOO_LARGE') return new ClientError(`Full-text query on '${name}' is too large`, 413);
|
|
1073
|
+
return new ServerError(`Full-text search on '${name}' failed`, 500);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function nativeErrorNeedsRebuild(error: unknown): boolean {
|
|
1077
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
1078
|
+
return typeof code === 'string' && REBUILD_REQUIRED_CODES.has(code);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const REBUILD_REQUIRED_CODES = new Set([
|
|
1082
|
+
'E_INDEX_NOT_READY',
|
|
1083
|
+
'E_INDEX_CORRUPT',
|
|
1084
|
+
'E_IDENTITY_MISMATCH',
|
|
1085
|
+
'E_INCOMPLETE_CREATE',
|
|
1086
|
+
'E_SCHEMA_MISMATCH',
|
|
1087
|
+
'E_INDEX_FORMAT_INCOMPATIBLE',
|
|
1088
|
+
]);
|
|
1089
|
+
|
|
1090
|
+
function remainingSearchBudget(deadline: number): number {
|
|
1091
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
1092
|
+
if (remaining <= 0) throw new ServerError('Full-text search exceeded its execution budget', 503);
|
|
1093
|
+
return remaining;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function digestGeneration(value: string): string {
|
|
1097
|
+
return createHash('sha256').update(value).digest('hex');
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function queryDefinitionSnapshot(definition: FullTextDefinition): string {
|
|
1101
|
+
return JSON.stringify({
|
|
1102
|
+
fields: definition.fields.map(({ name, weight, highlight }) => ({ name, weight, highlight })),
|
|
1103
|
+
highlighting: definition.highlighting,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function withTimeout<T>(
|
|
1108
|
+
promise: Promise<T>,
|
|
1109
|
+
milliseconds: number,
|
|
1110
|
+
timeoutError: () => Error = () => new Error('Full-text highlight source read timed out')
|
|
1111
|
+
): Promise<T> {
|
|
1112
|
+
let timer: NodeJS.Timeout;
|
|
1113
|
+
return Promise.race([
|
|
1114
|
+
promise,
|
|
1115
|
+
new Promise<never>((_resolve, reject) => {
|
|
1116
|
+
timer = setTimeout(() => reject(timeoutError()), milliseconds);
|
|
1117
|
+
timer.unref?.();
|
|
1118
|
+
}),
|
|
1119
|
+
]).finally(() => clearTimeout(timer));
|
|
1120
|
+
}
|