@smthrs/gateway 0.35.0 → 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +198 -0
- package/LICENSE +1 -1
- package/README.md +92 -0
- package/dist/cjs/Diagnosis.d.ts +275 -0
- package/dist/cjs/Diagnosis.d.ts.map +1 -0
- package/dist/cjs/Diagnosis.js +476 -0
- package/dist/cjs/Diagnosis.js.map +7 -0
- package/dist/cjs/EngineTrace.d.ts +76 -0
- package/dist/cjs/EngineTrace.d.ts.map +1 -0
- package/dist/cjs/EngineTrace.js +509 -0
- package/dist/cjs/EngineTrace.js.map +7 -0
- package/dist/cjs/GatewayError.d.ts +54 -0
- package/dist/cjs/GatewayError.d.ts.map +1 -0
- package/dist/cjs/GatewayError.js +51 -0
- package/dist/cjs/GatewayError.js.map +7 -0
- package/dist/cjs/GatewayProjection.d.ts +480 -0
- package/dist/cjs/GatewayProjection.d.ts.map +1 -0
- package/dist/cjs/GatewayProjection.js +471 -0
- package/dist/cjs/GatewayProjection.js.map +7 -0
- package/dist/cjs/GatewayRpcs.d.ts +1886 -0
- package/dist/cjs/GatewayRpcs.d.ts.map +1 -0
- package/dist/cjs/GatewayRpcs.js +121 -0
- package/dist/cjs/GatewayRpcs.js.map +7 -0
- package/dist/cjs/GatewaySchema.d.ts +3274 -0
- package/dist/cjs/GatewaySchema.d.ts.map +1 -0
- package/dist/cjs/GatewaySchema.js +141 -0
- package/dist/cjs/GatewaySchema.js.map +7 -0
- package/dist/cjs/GatewayServer.d.ts +427 -0
- package/dist/cjs/GatewayServer.d.ts.map +1 -0
- package/dist/cjs/GatewayServer.js +428 -0
- package/dist/cjs/GatewayServer.js.map +7 -0
- package/dist/cjs/HostBranchPresence.d.ts +19 -0
- package/dist/cjs/HostBranchPresence.d.ts.map +1 -0
- package/dist/cjs/HostBranchPresence.js +82 -0
- package/dist/cjs/HostBranchPresence.js.map +7 -0
- package/dist/cjs/Projections.d.ts +209 -0
- package/dist/cjs/Projections.d.ts.map +1 -0
- package/dist/cjs/Projections.js +906 -0
- package/dist/cjs/Projections.js.map +7 -0
- package/dist/cjs/RunDevTools.d.ts +181 -0
- package/dist/cjs/RunDevTools.d.ts.map +1 -0
- package/dist/cjs/RunDevTools.js +195 -0
- package/dist/cjs/RunDevTools.js.map +7 -0
- package/dist/cjs/RunHistory.d.ts +185 -0
- package/dist/cjs/RunHistory.d.ts.map +1 -0
- package/dist/cjs/RunHistory.js +68 -0
- package/dist/cjs/RunHistory.js.map +7 -0
- package/dist/cjs/RunTrace.d.ts +753 -0
- package/dist/cjs/RunTrace.d.ts.map +1 -0
- package/dist/cjs/RunTrace.js +1595 -0
- package/dist/cjs/RunTrace.js.map +7 -0
- package/dist/cjs/RuntimeBridge.d.ts +1386 -0
- package/dist/cjs/RuntimeBridge.d.ts.map +1 -0
- package/dist/cjs/RuntimeBridge.js +598 -0
- package/dist/cjs/RuntimeBridge.js.map +7 -0
- package/dist/cjs/bun/BunGateway.d.ts +20 -0
- package/dist/cjs/bun/BunGateway.d.ts.map +1 -0
- package/dist/cjs/bun/BunGateway.js +62 -0
- package/dist/cjs/bun/BunGateway.js.map +7 -0
- package/dist/cjs/index.d.ts +65 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +59 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/NativeGateway.d.ts +176 -0
- package/dist/cjs/internal/NativeGateway.d.ts.map +1 -0
- package/dist/cjs/internal/NativeGateway.js +187 -0
- package/dist/cjs/internal/NativeGateway.js.map +7 -0
- package/dist/cjs/internal/actionRenderings.d.ts +12 -0
- package/dist/cjs/internal/actionRenderings.d.ts.map +1 -0
- package/dist/cjs/internal/actionRenderings.js +224 -0
- package/dist/cjs/internal/actionRenderings.js.map +7 -0
- package/dist/cjs/internal/callEvents.d.ts +97 -0
- package/dist/cjs/internal/callEvents.d.ts.map +1 -0
- package/dist/cjs/internal/callEvents.js +169 -0
- package/dist/cjs/internal/callEvents.js.map +7 -0
- package/dist/cjs/internal/digestIndex.d.ts +31 -0
- package/dist/cjs/internal/digestIndex.d.ts.map +1 -0
- package/dist/cjs/internal/digestIndex.js +105 -0
- package/dist/cjs/internal/digestIndex.js.map +7 -0
- package/dist/cjs/internal/digestMemory.d.ts +19 -0
- package/dist/cjs/internal/digestMemory.d.ts.map +1 -0
- package/dist/cjs/internal/digestMemory.js +33 -0
- package/dist/cjs/internal/digestMemory.js.map +7 -0
- package/dist/cjs/internal/inspectLabels.d.ts +13 -0
- package/dist/cjs/internal/inspectLabels.d.ts.map +1 -0
- package/dist/cjs/internal/inspectLabels.js +29 -0
- package/dist/cjs/internal/inspectLabels.js.map +7 -0
- package/dist/cjs/internal/nativeResolution.d.ts +41 -0
- package/dist/cjs/internal/nativeResolution.d.ts.map +1 -0
- package/dist/cjs/internal/nativeResolution.js +62 -0
- package/dist/cjs/internal/nativeResolution.js.map +7 -0
- package/dist/cjs/internal/nodeEvents.d.ts +47 -0
- package/dist/cjs/internal/nodeEvents.d.ts.map +1 -0
- package/dist/cjs/internal/nodeEvents.js +60 -0
- package/dist/cjs/internal/nodeEvents.js.map +7 -0
- package/dist/cjs/node/NodeGateway.d.ts +16 -0
- package/dist/cjs/node/NodeGateway.d.ts.map +1 -0
- package/dist/cjs/node/NodeGateway.js +48 -0
- package/dist/cjs/node/NodeGateway.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Diagnosis.d.ts +275 -0
- package/dist/esm/Diagnosis.d.ts.map +1 -0
- package/dist/esm/Diagnosis.js +697 -0
- package/dist/esm/Diagnosis.js.map +1 -0
- package/dist/esm/EngineTrace.d.ts +76 -0
- package/dist/esm/EngineTrace.d.ts.map +1 -0
- package/dist/esm/EngineTrace.js +557 -0
- package/dist/esm/EngineTrace.js.map +1 -0
- package/dist/esm/GatewayError.d.ts +54 -0
- package/dist/esm/GatewayError.d.ts.map +1 -0
- package/dist/esm/GatewayError.js +60 -0
- package/dist/esm/GatewayError.js.map +1 -0
- package/dist/esm/GatewayProjection.d.ts +480 -0
- package/dist/esm/GatewayProjection.d.ts.map +1 -0
- package/dist/esm/GatewayProjection.js +799 -0
- package/dist/esm/GatewayProjection.js.map +1 -0
- package/dist/esm/GatewayRpcs.d.ts +1886 -0
- package/dist/esm/GatewayRpcs.d.ts.map +1 -0
- package/dist/esm/GatewayRpcs.js +145 -0
- package/dist/esm/GatewayRpcs.js.map +1 -0
- package/dist/esm/GatewaySchema.d.ts +3274 -0
- package/dist/esm/GatewaySchema.d.ts.map +1 -0
- package/dist/esm/GatewaySchema.js +244 -0
- package/dist/esm/GatewaySchema.js.map +1 -0
- package/dist/esm/GatewayServer.d.ts +427 -0
- package/dist/esm/GatewayServer.d.ts.map +1 -0
- package/dist/esm/GatewayServer.js +725 -0
- package/dist/esm/GatewayServer.js.map +1 -0
- package/dist/esm/HostBranchPresence.d.ts +19 -0
- package/dist/esm/HostBranchPresence.d.ts.map +1 -0
- package/dist/esm/HostBranchPresence.js +57 -0
- package/dist/esm/HostBranchPresence.js.map +1 -0
- package/dist/esm/Projections.d.ts +209 -0
- package/dist/esm/Projections.d.ts.map +1 -0
- package/dist/esm/Projections.js +1254 -0
- package/dist/esm/Projections.js.map +1 -0
- package/dist/esm/RunDevTools.d.ts +181 -0
- package/dist/esm/RunDevTools.d.ts.map +1 -0
- package/dist/esm/RunDevTools.js +281 -0
- package/dist/esm/RunDevTools.js.map +1 -0
- package/dist/esm/RunHistory.d.ts +185 -0
- package/dist/esm/RunHistory.d.ts.map +1 -0
- package/dist/esm/RunHistory.js +109 -0
- package/dist/esm/RunHistory.js.map +1 -0
- package/dist/esm/RunTrace.d.ts +753 -0
- package/dist/esm/RunTrace.d.ts.map +1 -0
- package/dist/esm/RunTrace.js +2106 -0
- package/dist/esm/RunTrace.js.map +1 -0
- package/dist/esm/RuntimeBridge.d.ts +1386 -0
- package/dist/esm/RuntimeBridge.d.ts.map +1 -0
- package/dist/esm/RuntimeBridge.js +659 -0
- package/dist/esm/RuntimeBridge.js.map +1 -0
- package/dist/esm/bun/BunGateway.d.ts +20 -0
- package/dist/esm/bun/BunGateway.d.ts.map +1 -0
- package/dist/esm/bun/BunGateway.js +32 -0
- package/dist/esm/bun/BunGateway.js.map +1 -0
- package/dist/esm/index.d.ts +65 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +65 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/NativeGateway.d.ts +176 -0
- package/dist/esm/internal/NativeGateway.d.ts.map +1 -0
- package/dist/esm/internal/NativeGateway.js +268 -0
- package/dist/esm/internal/NativeGateway.js.map +1 -0
- package/dist/esm/internal/actionRenderings.d.ts +12 -0
- package/dist/esm/internal/actionRenderings.d.ts.map +1 -0
- package/dist/esm/internal/actionRenderings.js +212 -0
- package/dist/esm/internal/actionRenderings.js.map +1 -0
- package/dist/esm/internal/callEvents.d.ts +97 -0
- package/dist/esm/internal/callEvents.d.ts.map +1 -0
- package/dist/esm/internal/callEvents.js +206 -0
- package/dist/esm/internal/callEvents.js.map +1 -0
- package/dist/esm/internal/digestIndex.d.ts +31 -0
- package/dist/esm/internal/digestIndex.d.ts.map +1 -0
- package/dist/esm/internal/digestIndex.js +87 -0
- package/dist/esm/internal/digestIndex.js.map +1 -0
- package/dist/esm/internal/digestMemory.d.ts +19 -0
- package/dist/esm/internal/digestMemory.d.ts.map +1 -0
- package/dist/esm/internal/digestMemory.js +23 -0
- package/dist/esm/internal/digestMemory.js.map +1 -0
- package/dist/esm/internal/inspectLabels.d.ts +13 -0
- package/dist/esm/internal/inspectLabels.d.ts.map +1 -0
- package/dist/esm/internal/inspectLabels.js +17 -0
- package/dist/esm/internal/inspectLabels.js.map +1 -0
- package/dist/esm/internal/nativeResolution.d.ts +41 -0
- package/dist/esm/internal/nativeResolution.d.ts.map +1 -0
- package/dist/esm/internal/nativeResolution.js +78 -0
- package/dist/esm/internal/nativeResolution.js.map +1 -0
- package/dist/esm/internal/nodeEvents.d.ts +47 -0
- package/dist/esm/internal/nodeEvents.d.ts.map +1 -0
- package/dist/esm/internal/nodeEvents.js +60 -0
- package/dist/esm/internal/nodeEvents.js.map +1 -0
- package/dist/esm/node/NodeGateway.d.ts +16 -0
- package/dist/esm/node/NodeGateway.d.ts.map +1 -0
- package/dist/esm/node/NodeGateway.js +13 -0
- package/dist/esm/node/NodeGateway.js.map +1 -0
- package/dist/vendor/adapters.json +2707 -0
- package/dist/vendor/bun/BunChildProcessSpawner.d.ts +7 -0
- package/dist/vendor/bun/BunChildProcessSpawner.js +14 -0
- package/dist/vendor/bun/BunClusterHttp.d.ts +83 -0
- package/dist/vendor/bun/BunClusterHttp.js +39 -0
- package/dist/vendor/bun/BunClusterSocket.d.ts +65 -0
- package/dist/vendor/bun/BunClusterSocket.js +22 -0
- package/dist/vendor/bun/BunCrypto.d.ts +10 -0
- package/dist/vendor/bun/BunCrypto.js +8 -0
- package/dist/vendor/bun/BunFileSystem.d.ts +10 -0
- package/dist/vendor/bun/BunFileSystem.js +9 -0
- package/dist/vendor/bun/BunHttpClient.d.ts +9 -0
- package/dist/vendor/bun/BunHttpClient.js +1 -0
- package/dist/vendor/bun/BunHttpPlatform.d.ts +10 -0
- package/dist/vendor/bun/BunHttpPlatform.js +11 -0
- package/dist/vendor/bun/BunHttpServer.d.ts +142 -0
- package/dist/vendor/bun/BunHttpServer.js +38 -0
- package/dist/vendor/bun/BunHttpServerRequest.d.ts +18 -0
- package/dist/vendor/bun/BunHttpServerRequest.js +7 -0
- package/dist/vendor/bun/BunMultipart.d.ts +31 -0
- package/dist/vendor/bun/BunMultipart.js +12 -0
- package/dist/vendor/bun/BunPath.d.ts +24 -0
- package/dist/vendor/bun/BunPath.js +12 -0
- package/dist/vendor/bun/BunRedis.d.ts +49 -0
- package/dist/vendor/bun/BunRedis.js +11 -0
- package/dist/vendor/bun/BunRuntime.d.ts +85 -0
- package/dist/vendor/bun/BunRuntime.js +8 -0
- package/dist/vendor/bun/BunServices.d.ts +34 -0
- package/dist/vendor/bun/BunServices.js +23 -0
- package/dist/vendor/bun/BunSink.d.ts +8 -0
- package/dist/vendor/bun/BunSink.js +12 -0
- package/dist/vendor/bun/BunSocket.d.ts +40 -0
- package/dist/vendor/bun/BunSocket.js +29 -0
- package/dist/vendor/bun/BunSocketServer.d.ts +8 -0
- package/dist/vendor/bun/BunSocketServer.js +21 -0
- package/dist/vendor/bun/BunStdio.d.ts +11 -0
- package/dist/vendor/bun/BunStdio.js +10 -0
- package/dist/vendor/bun/BunStream.d.ts +19 -0
- package/dist/vendor/bun/BunStream.js +30 -0
- package/dist/vendor/bun/BunTerminal.d.ts +21 -0
- package/dist/vendor/bun/BunTerminal.js +10 -0
- package/dist/vendor/bun/BunWorker.d.ts +20 -0
- package/dist/vendor/bun/BunWorker.js +9 -0
- package/dist/vendor/bun/BunWorkerRunner.d.ts +12 -0
- package/dist/vendor/bun/BunWorkerRunner.js +7 -0
- package/dist/vendor/bun/LICENSE +21 -0
- package/dist/vendor/bun/index.d.ts +92 -0
- package/dist/vendor/bun/index.js +107 -0
- package/dist/vendor/chunk-2GCOZTTW.js +48 -0
- package/dist/vendor/chunk-37CUR3PO.js +81 -0
- package/dist/vendor/chunk-3ARKKENZ.js +45 -0
- package/dist/vendor/chunk-3NV2UDBI.js +7 -0
- package/dist/vendor/chunk-3OSW7CT7.js +143 -0
- package/dist/vendor/chunk-62R63L74.js +62 -0
- package/dist/vendor/chunk-6AQLBC2I.js +118 -0
- package/dist/vendor/chunk-6C5H7S6B.js +18 -0
- package/dist/vendor/chunk-6DY5RBZD.js +85 -0
- package/dist/vendor/chunk-6UPBD47P.js +53 -0
- package/dist/vendor/chunk-AVVVN75Z.js +96 -0
- package/dist/vendor/chunk-B4DHEMBB.js +14 -0
- package/dist/vendor/chunk-BCK3NJOX.js +15 -0
- package/dist/vendor/chunk-BFBIHJ6X.js +0 -0
- package/dist/vendor/chunk-CES4OUM4.js +63 -0
- package/dist/vendor/chunk-D64L6YND.js +59 -0
- package/dist/vendor/chunk-DS2R6WOM.js +18 -0
- package/dist/vendor/chunk-DX4Z23DM.js +65 -0
- package/dist/vendor/chunk-DZ2VOUDP.js +564 -0
- package/dist/vendor/chunk-FOXN6JPJ.js +61 -0
- package/dist/vendor/chunk-FPQF7CR5.js +55 -0
- package/dist/vendor/chunk-FUTCTCEG.js +20 -0
- package/dist/vendor/chunk-GUPAV2YZ.js +18 -0
- package/dist/vendor/chunk-H2Z2Y4QH.js +70 -0
- package/dist/vendor/chunk-HAEDVSSM.js +129 -0
- package/dist/vendor/chunk-HY7BBL3K.js +93 -0
- package/dist/vendor/chunk-IFTTT6D5.js +31 -0
- package/dist/vendor/chunk-KQDSNB7M.js +26 -0
- package/dist/vendor/chunk-LIHLXXGQ.js +26 -0
- package/dist/vendor/chunk-LKCGQEE5.js +28 -0
- package/dist/vendor/chunk-MH2J3BYB.js +0 -0
- package/dist/vendor/chunk-ML2NAFH2.js +131 -0
- package/dist/vendor/chunk-MLFIZ5O2.js +0 -0
- package/dist/vendor/chunk-MLKGABMK.js +9 -0
- package/dist/vendor/chunk-MSCLEMYX.js +71 -0
- package/dist/vendor/chunk-OBWXQKVP.js +57 -0
- package/dist/vendor/chunk-QR557K66.js +20 -0
- package/dist/vendor/chunk-R4QUY7DH.js +18 -0
- package/dist/vendor/chunk-RGECXXJS.js +18 -0
- package/dist/vendor/chunk-TFY7W6D4.js +36 -0
- package/dist/vendor/chunk-TGPIVY4A.js +104 -0
- package/dist/vendor/chunk-TGXCX6W3.js +398 -0
- package/dist/vendor/chunk-TOHSFXMV.js +437 -0
- package/dist/vendor/chunk-UHIDPUFF.js +34 -0
- package/dist/vendor/chunk-ULT6CWUI.js +109 -0
- package/dist/vendor/chunk-UXBGYQR3.js +18 -0
- package/dist/vendor/chunk-V6LUGQ7O.js +21 -0
- package/dist/vendor/chunk-W4OASVX6.js +92 -0
- package/dist/vendor/chunk-WAII54GA.js +18 -0
- package/dist/vendor/chunk-WECQDT36.js +426 -0
- package/dist/vendor/chunk-WHKMOC7Z.js +28 -0
- package/dist/vendor/chunk-WVFOZ2UL.js +40 -0
- package/dist/vendor/chunk-X73E2UPI.js +18 -0
- package/dist/vendor/chunk-XCYARUII.js +63 -0
- package/dist/vendor/chunk-XFEVTDVD.js +34 -0
- package/dist/vendor/chunk-XFZZYSJ4.js +22 -0
- package/dist/vendor/chunk-XJWJXLXJ.js +18 -0
- package/dist/vendor/chunk-XY3Q62EO.js +18 -0
- package/dist/vendor/chunk-YDASSC75.js +394 -0
- package/dist/vendor/chunk-Z6C7SO5Z.js +507 -0
- package/dist/vendor/chunk-Z7QNPJEB.js +229 -0
- package/dist/vendor/chunk-Z7QPP4YH.js +249 -0
- package/dist/vendor/chunk-ZCHYLHJH.js +86 -0
- package/dist/vendor/chunk-ZIQVYVMA.js +22 -0
- package/dist/vendor/chunk-ZPIG2CUA.js +34 -0
- package/dist/vendor/cjs/bun/BunChildProcessSpawner.d.ts +7 -0
- package/dist/vendor/cjs/bun/BunChildProcessSpawner.js +1 -0
- package/dist/vendor/cjs/bun/BunClusterHttp.d.ts +83 -0
- package/dist/vendor/cjs/bun/BunClusterHttp.js +1 -0
- package/dist/vendor/cjs/bun/BunClusterSocket.d.ts +65 -0
- package/dist/vendor/cjs/bun/BunClusterSocket.js +1 -0
- package/dist/vendor/cjs/bun/BunCrypto.d.ts +10 -0
- package/dist/vendor/cjs/bun/BunCrypto.js +1 -0
- package/dist/vendor/cjs/bun/BunFileSystem.d.ts +10 -0
- package/dist/vendor/cjs/bun/BunFileSystem.js +1 -0
- package/dist/vendor/cjs/bun/BunHttpClient.d.ts +9 -0
- package/dist/vendor/cjs/bun/BunHttpClient.js +1 -0
- package/dist/vendor/cjs/bun/BunHttpPlatform.d.ts +10 -0
- package/dist/vendor/cjs/bun/BunHttpPlatform.js +1 -0
- package/dist/vendor/cjs/bun/BunHttpServer.d.ts +142 -0
- package/dist/vendor/cjs/bun/BunHttpServer.js +1 -0
- package/dist/vendor/cjs/bun/BunHttpServerRequest.d.ts +18 -0
- package/dist/vendor/cjs/bun/BunHttpServerRequest.js +1 -0
- package/dist/vendor/cjs/bun/BunMultipart.d.ts +31 -0
- package/dist/vendor/cjs/bun/BunMultipart.js +1 -0
- package/dist/vendor/cjs/bun/BunPath.d.ts +24 -0
- package/dist/vendor/cjs/bun/BunPath.js +1 -0
- package/dist/vendor/cjs/bun/BunRedis.d.ts +49 -0
- package/dist/vendor/cjs/bun/BunRedis.js +1 -0
- package/dist/vendor/cjs/bun/BunRuntime.d.ts +85 -0
- package/dist/vendor/cjs/bun/BunRuntime.js +1 -0
- package/dist/vendor/cjs/bun/BunServices.d.ts +34 -0
- package/dist/vendor/cjs/bun/BunServices.js +1 -0
- package/dist/vendor/cjs/bun/BunSink.d.ts +8 -0
- package/dist/vendor/cjs/bun/BunSink.js +1 -0
- package/dist/vendor/cjs/bun/BunSocket.d.ts +40 -0
- package/dist/vendor/cjs/bun/BunSocket.js +1 -0
- package/dist/vendor/cjs/bun/BunSocketServer.d.ts +8 -0
- package/dist/vendor/cjs/bun/BunSocketServer.js +1 -0
- package/dist/vendor/cjs/bun/BunStdio.d.ts +11 -0
- package/dist/vendor/cjs/bun/BunStdio.js +1 -0
- package/dist/vendor/cjs/bun/BunStream.d.ts +19 -0
- package/dist/vendor/cjs/bun/BunStream.js +1 -0
- package/dist/vendor/cjs/bun/BunTerminal.d.ts +21 -0
- package/dist/vendor/cjs/bun/BunTerminal.js +1 -0
- package/dist/vendor/cjs/bun/BunWorker.d.ts +20 -0
- package/dist/vendor/cjs/bun/BunWorker.js +1 -0
- package/dist/vendor/cjs/bun/BunWorkerRunner.d.ts +12 -0
- package/dist/vendor/cjs/bun/BunWorkerRunner.js +1 -0
- package/dist/vendor/cjs/bun/index.d.ts +92 -0
- package/dist/vendor/cjs/bun/index.js +1 -0
- package/dist/vendor/cjs/node/NodeChildProcessSpawner.d.ts +7 -0
- package/dist/vendor/cjs/node/NodeChildProcessSpawner.js +1 -0
- package/dist/vendor/cjs/node/NodeClusterHttp.d.ts +63 -0
- package/dist/vendor/cjs/node/NodeClusterHttp.js +1 -0
- package/dist/vendor/cjs/node/NodeClusterSocket.d.ts +79 -0
- package/dist/vendor/cjs/node/NodeClusterSocket.js +1 -0
- package/dist/vendor/cjs/node/NodeCrypto.d.ts +10 -0
- package/dist/vendor/cjs/node/NodeCrypto.js +1 -0
- package/dist/vendor/cjs/node/NodeFileSystem.d.ts +10 -0
- package/dist/vendor/cjs/node/NodeFileSystem.js +1 -0
- package/dist/vendor/cjs/node/NodeHttpClient.d.ts +179 -0
- package/dist/vendor/cjs/node/NodeHttpClient.js +1 -0
- package/dist/vendor/cjs/node/NodeHttpIncomingMessage.d.ts +62 -0
- package/dist/vendor/cjs/node/NodeHttpIncomingMessage.js +1 -0
- package/dist/vendor/cjs/node/NodeHttpPlatform.d.ts +36 -0
- package/dist/vendor/cjs/node/NodeHttpPlatform.js +1 -0
- package/dist/vendor/cjs/node/NodeHttpServer.d.ts +120 -0
- package/dist/vendor/cjs/node/NodeHttpServer.js +1 -0
- package/dist/vendor/cjs/node/NodeHttpServerRequest.d.ts +29 -0
- package/dist/vendor/cjs/node/NodeHttpServerRequest.js +1 -0
- package/dist/vendor/cjs/node/NodeMultipart.d.ts +46 -0
- package/dist/vendor/cjs/node/NodeMultipart.js +1 -0
- package/dist/vendor/cjs/node/NodeMultipartParser.d.ts +97 -0
- package/dist/vendor/cjs/node/NodeMultipartParser.js +1 -0
- package/dist/vendor/cjs/node/NodePath.d.ts +27 -0
- package/dist/vendor/cjs/node/NodePath.js +1 -0
- package/dist/vendor/cjs/node/NodeRedis.d.ts +74 -0
- package/dist/vendor/cjs/node/NodeRedis.js +1 -0
- package/dist/vendor/cjs/node/NodeRuntime.d.ts +85 -0
- package/dist/vendor/cjs/node/NodeRuntime.js +1 -0
- package/dist/vendor/cjs/node/NodeServices.d.ts +34 -0
- package/dist/vendor/cjs/node/NodeServices.js +1 -0
- package/dist/vendor/cjs/node/NodeSink.d.ts +8 -0
- package/dist/vendor/cjs/node/NodeSink.js +1 -0
- package/dist/vendor/cjs/node/NodeSocket.d.ts +38 -0
- package/dist/vendor/cjs/node/NodeSocket.js +1 -0
- package/dist/vendor/cjs/node/NodeSocketServer.d.ts +8 -0
- package/dist/vendor/cjs/node/NodeSocketServer.js +1 -0
- package/dist/vendor/cjs/node/NodeStdio.d.ts +11 -0
- package/dist/vendor/cjs/node/NodeStdio.js +1 -0
- package/dist/vendor/cjs/node/NodeStream.d.ts +8 -0
- package/dist/vendor/cjs/node/NodeStream.js +1 -0
- package/dist/vendor/cjs/node/NodeTerminal.d.ts +21 -0
- package/dist/vendor/cjs/node/NodeTerminal.js +1 -0
- package/dist/vendor/cjs/node/NodeWorker.d.ts +22 -0
- package/dist/vendor/cjs/node/NodeWorker.js +1 -0
- package/dist/vendor/cjs/node/NodeWorkerRunner.d.ts +12 -0
- package/dist/vendor/cjs/node/NodeWorkerRunner.js +1 -0
- package/dist/vendor/cjs/node/Undici.d.ts +28 -0
- package/dist/vendor/cjs/node/Undici.js +1 -0
- package/dist/vendor/cjs/node/index.d.ts +100 -0
- package/dist/vendor/cjs/node/index.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.d.ts +57 -0
- package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeClusterSocket.d.ts +22 -0
- package/dist/vendor/cjs/node-shared/NodeClusterSocket.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeCrypto.d.ts +27 -0
- package/dist/vendor/cjs/node-shared/NodeCrypto.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeFileSystem.d.ts +11 -0
- package/dist/vendor/cjs/node-shared/NodeFileSystem.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeHttpCompression.d.ts +36 -0
- package/dist/vendor/cjs/node-shared/NodeHttpCompression.js +1 -0
- package/dist/vendor/cjs/node-shared/NodePath.d.ts +27 -0
- package/dist/vendor/cjs/node-shared/NodePath.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeRuntime.d.ts +47 -0
- package/dist/vendor/cjs/node-shared/NodeRuntime.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeSink.d.ts +69 -0
- package/dist/vendor/cjs/node-shared/NodeSink.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeSocket.d.ts +134 -0
- package/dist/vendor/cjs/node-shared/NodeSocket.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeSocketServer.d.ts +80 -0
- package/dist/vendor/cjs/node-shared/NodeSocketServer.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeStdio.d.ts +12 -0
- package/dist/vendor/cjs/node-shared/NodeStdio.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeStream.d.ts +179 -0
- package/dist/vendor/cjs/node-shared/NodeStream.js +1 -0
- package/dist/vendor/cjs/node-shared/NodeTerminal.d.ts +22 -0
- package/dist/vendor/cjs/node-shared/NodeTerminal.js +1 -0
- package/dist/vendor/cjs/node-shared/index.d.ts +56 -0
- package/dist/vendor/cjs/node-shared/index.js +1 -0
- package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
- package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.js +1 -0
- package/dist/vendor/cjs/node-shared/internal/utils.d.ts +2 -0
- package/dist/vendor/cjs/node-shared/internal/utils.js +1 -0
- package/dist/vendor/cjs/package.json +1 -0
- package/dist/vendor/cjs/platform-bun.d.ts +92 -0
- package/dist/vendor/cjs/platform-bun.js +1 -0
- package/dist/vendor/cjs/platform-node-shared.d.ts +56 -0
- package/dist/vendor/cjs/platform-node-shared.js +1 -0
- package/dist/vendor/cjs/platform-node.d.ts +100 -0
- package/dist/vendor/cjs/platform-node.js +1 -0
- package/dist/vendor/node/LICENSE +21 -0
- package/dist/vendor/node/NodeChildProcessSpawner.d.ts +7 -0
- package/dist/vendor/node/NodeChildProcessSpawner.js +14 -0
- package/dist/vendor/node/NodeClusterHttp.d.ts +63 -0
- package/dist/vendor/node/NodeClusterHttp.js +43 -0
- package/dist/vendor/node/NodeClusterSocket.d.ts +79 -0
- package/dist/vendor/node/NodeClusterSocket.js +30 -0
- package/dist/vendor/node/NodeCrypto.d.ts +10 -0
- package/dist/vendor/node/NodeCrypto.js +8 -0
- package/dist/vendor/node/NodeFileSystem.d.ts +10 -0
- package/dist/vendor/node/NodeFileSystem.js +9 -0
- package/dist/vendor/node/NodeHttpClient.d.ts +179 -0
- package/dist/vendor/node/NodeHttpClient.js +46 -0
- package/dist/vendor/node/NodeHttpIncomingMessage.d.ts +62 -0
- package/dist/vendor/node/NodeHttpIncomingMessage.js +10 -0
- package/dist/vendor/node/NodeHttpPlatform.d.ts +36 -0
- package/dist/vendor/node/NodeHttpPlatform.js +16 -0
- package/dist/vendor/node/NodeHttpServer.d.ts +120 -0
- package/dist/vendor/node/NodeHttpServer.js +46 -0
- package/dist/vendor/node/NodeHttpServerRequest.d.ts +29 -0
- package/dist/vendor/node/NodeHttpServerRequest.js +9 -0
- package/dist/vendor/node/NodeMultipart.d.ts +46 -0
- package/dist/vendor/node/NodeMultipart.js +15 -0
- package/dist/vendor/node/NodeMultipartParser.d.ts +97 -0
- package/dist/vendor/node/NodeMultipartParser.js +11 -0
- package/dist/vendor/node/NodePath.d.ts +27 -0
- package/dist/vendor/node/NodePath.js +12 -0
- package/dist/vendor/node/NodeRedis.d.ts +74 -0
- package/dist/vendor/node/NodeRedis.js +11 -0
- package/dist/vendor/node/NodeRuntime.d.ts +85 -0
- package/dist/vendor/node/NodeRuntime.js +8 -0
- package/dist/vendor/node/NodeServices.d.ts +34 -0
- package/dist/vendor/node/NodeServices.js +23 -0
- package/dist/vendor/node/NodeSink.d.ts +8 -0
- package/dist/vendor/node/NodeSink.js +12 -0
- package/dist/vendor/node/NodeSocket.d.ts +38 -0
- package/dist/vendor/node/NodeSocket.js +31 -0
- package/dist/vendor/node/NodeSocketServer.d.ts +8 -0
- package/dist/vendor/node/NodeSocketServer.js +21 -0
- package/dist/vendor/node/NodeStdio.d.ts +11 -0
- package/dist/vendor/node/NodeStdio.js +10 -0
- package/dist/vendor/node/NodeStream.d.ts +8 -0
- package/dist/vendor/node/NodeStream.js +27 -0
- package/dist/vendor/node/NodeTerminal.d.ts +21 -0
- package/dist/vendor/node/NodeTerminal.js +10 -0
- package/dist/vendor/node/NodeWorker.d.ts +22 -0
- package/dist/vendor/node/NodeWorker.js +9 -0
- package/dist/vendor/node/NodeWorkerRunner.d.ts +12 -0
- package/dist/vendor/node/NodeWorkerRunner.js +7 -0
- package/dist/vendor/node/Undici.d.ts +28 -0
- package/dist/vendor/node/Undici.js +9 -0
- package/dist/vendor/node/index.d.ts +100 -0
- package/dist/vendor/node/index.js +115 -0
- package/dist/vendor/node-shared/LICENSE +21 -0
- package/dist/vendor/node-shared/NodeChildProcessSpawner.d.ts +57 -0
- package/dist/vendor/node-shared/NodeChildProcessSpawner.js +13 -0
- package/dist/vendor/node-shared/NodeClusterSocket.d.ts +22 -0
- package/dist/vendor/node-shared/NodeClusterSocket.js +11 -0
- package/dist/vendor/node-shared/NodeCrypto.d.ts +27 -0
- package/dist/vendor/node-shared/NodeCrypto.js +9 -0
- package/dist/vendor/node-shared/NodeFileSystem.d.ts +11 -0
- package/dist/vendor/node-shared/NodeFileSystem.js +8 -0
- package/dist/vendor/node-shared/NodeHttpCompression.d.ts +36 -0
- package/dist/vendor/node-shared/NodeHttpCompression.js +13 -0
- package/dist/vendor/node-shared/NodePath.d.ts +27 -0
- package/dist/vendor/node-shared/NodePath.js +11 -0
- package/dist/vendor/node-shared/NodeRuntime.d.ts +47 -0
- package/dist/vendor/node-shared/NodeRuntime.js +7 -0
- package/dist/vendor/node-shared/NodeSink.d.ts +69 -0
- package/dist/vendor/node-shared/NodeSink.js +11 -0
- package/dist/vendor/node-shared/NodeSocket.d.ts +134 -0
- package/dist/vendor/node-shared/NodeSocket.js +23 -0
- package/dist/vendor/node-shared/NodeSocketServer.d.ts +80 -0
- package/dist/vendor/node-shared/NodeSocketServer.js +20 -0
- package/dist/vendor/node-shared/NodeStdio.d.ts +12 -0
- package/dist/vendor/node-shared/NodeStdio.js +9 -0
- package/dist/vendor/node-shared/NodeStream.d.ts +179 -0
- package/dist/vendor/node-shared/NodeStream.js +26 -0
- package/dist/vendor/node-shared/NodeTerminal.d.ts +22 -0
- package/dist/vendor/node-shared/NodeTerminal.js +9 -0
- package/dist/vendor/node-shared/index.d.ts +56 -0
- package/dist/vendor/node-shared/index.js +58 -0
- package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
- package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.js +7 -0
- package/dist/vendor/node-shared/internal/utils.d.ts +2 -0
- package/dist/vendor/node-shared/internal/utils.js +7 -0
- package/dist/vendor/platform-bun.d.ts +92 -0
- package/dist/vendor/platform-bun.js +107 -0
- package/dist/vendor/platform-node-shared.d.ts +56 -0
- package/dist/vendor/platform-node-shared.js +58 -0
- package/dist/vendor/platform-node.d.ts +100 -0
- package/dist/vendor/platform-node.js +115 -0
- package/docs/README.md +158 -0
- package/docs/api.md +601 -0
- package/docs/concepts/projections.md +250 -0
- package/docs/concepts/subscriptions.md +204 -0
- package/docs/concepts/trust-boundary.md +256 -0
- package/docs/guides/diagnose-a-run.md +115 -0
- package/docs/guides/follow-a-run.md +150 -0
- package/docs/guides/host-the-gateway.md +153 -0
- package/docs/guides/read-a-projection.md +138 -0
- package/docs/guides/serve-beyond-loopback.md +166 -0
- package/docs/guides/submit-an-approval.md +187 -0
- package/docs/guides/testing.md +148 -0
- package/docs/installation.md +89 -0
- package/docs/quickstart.md +200 -0
- package/docs/troubleshooting.md +355 -0
- package/package.json +264 -32
- package/src/Diagnosis.ts +892 -0
- package/src/EngineTrace.ts +627 -0
- package/src/GatewayError.ts +71 -0
- package/src/GatewayProjection.ts +959 -0
- package/src/GatewayRpcs.ts +179 -0
- package/src/GatewaySchema.ts +516 -0
- package/src/GatewayServer.ts +932 -0
- package/src/HostBranchPresence.ts +76 -0
- package/src/Projections.ts +1735 -0
- package/src/RunDevTools.ts +394 -0
- package/src/RunHistory.ts +174 -0
- package/src/RunTrace.ts +2796 -0
- package/src/RuntimeBridge.ts +835 -0
- package/src/bun/BunGateway.ts +52 -0
- package/src/index.ts +77 -0
- package/src/internal/NativeGateway.ts +360 -0
- package/src/internal/actionRenderings.ts +212 -0
- package/src/internal/callEvents.ts +257 -0
- package/src/internal/digestIndex.ts +134 -0
- package/src/internal/digestMemory.ts +27 -0
- package/src/internal/inspectLabels.ts +18 -0
- package/src/internal/nativeResolution.ts +103 -0
- package/src/internal/nodeEvents.ts +100 -0
- package/src/node/NodeGateway.ts +30 -0
- package/openapi.yaml +0 -29945
- package/src/README.md +0 -39
- package/src/api/README.md +0 -23
- package/src/api/apiCollectionNames.js +0 -10
- package/src/api/index.d.ts +0 -126
- package/src/api/index.js +0 -14
- package/src/api/normalizeApiRow.js +0 -36
- package/src/api/serializeAccountRow.js +0 -10
- package/src/api/serializeApprovalRow.js +0 -53
- package/src/api/serializeComparisonScoreRow.js +0 -18
- package/src/api/serializeCronRow.js +0 -30
- package/src/api/serializeDocRow.js +0 -20
- package/src/api/serializeMemoryFactRow.js +0 -21
- package/src/api/serializePromptRow.js +0 -10
- package/src/api/serializeRunEventRow.js +0 -28
- package/src/api/serializeRunRow.js +0 -93
- package/src/api/serializeScoreDetailRow.js +0 -25
- package/src/api/serializeScoreRow.js +0 -26
- package/src/api/serializeTicketRow.js +0 -20
- package/src/api/serializeWorkflowRow.js +0 -10
- package/src/auth/scopes.d.ts +0 -19
- package/src/auth/scopes.js +0 -164
- package/src/index.d.ts +0 -316
- package/src/index.js +0 -108
- package/src/rpc/gatewayRpcTypes.ts +0 -186
- package/src/rpc/index.d.ts +0 -223
- package/src/rpc/index.js +0 -2527
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway read path: served projections computed from the control plane.
|
|
3
|
+
*
|
|
4
|
+
* Every projection here is a fold over facts `@smthrs/control` already
|
|
5
|
+
* publishes: `Control.list` for run and flow listings, `Control.watch` for a
|
|
6
|
+
* run's ordered events. The gateway never opens the engine database, so a
|
|
7
|
+
* projection served to a browser through a relay is the same projection a
|
|
8
|
+
* local reader computes, and a projection cannot drift from the control plane
|
|
9
|
+
* by reading a column the control plane does not expose.
|
|
10
|
+
*
|
|
11
|
+
* A snapshot brackets its journal read with run-summary reads and retries
|
|
12
|
+
* when the summary changes. Rows and cursor share the resulting event buffer,
|
|
13
|
+
* so a completion cannot be acknowledged with the preceding run status.
|
|
14
|
+
*
|
|
15
|
+
* A subscription is a snapshot followed by deltas. A delta recomputes the
|
|
16
|
+
* selector's rows from the accumulated events rather than patching them: a
|
|
17
|
+
* projection is a reproducible fold (`@smthrs/journal` `Projection`), and
|
|
18
|
+
* recomputation is the only delta that cannot disagree with a fresh snapshot.
|
|
19
|
+
* `run-events` appends immutable rows. `transcript` normally appends too, but
|
|
20
|
+
* sends an existing snapshot reset when a committed native call fact upgrades
|
|
21
|
+
* previously delivered telemetry. Events accumulate in the stream, so each delta
|
|
22
|
+
* folds without re-reading history. Resuming a historical run summary first
|
|
23
|
+
* rebuilds its compacted health prefix once through durable follow replay.
|
|
24
|
+
*
|
|
25
|
+
* A workspace subscription follows every journal partition without a cursor.
|
|
26
|
+
* The follower stores each retained run's last position and drops replay
|
|
27
|
+
* through that cutoff. Sources and exclusion verdicts share the snapshot's
|
|
28
|
+
* run ceiling; eviction discards a verdict together with its cursor state.
|
|
29
|
+
* Events are gathered for a short window and folded into one frame, and each
|
|
30
|
+
* run's rows are cached so a burst refolds only the runs whose journals grew.
|
|
31
|
+
*
|
|
32
|
+
* @since 1.0.0
|
|
33
|
+
*/
|
|
34
|
+
import { Control } from "@smthrs/control/Control";
|
|
35
|
+
import * as ControlSchema from "@smthrs/control/ControlSchema";
|
|
36
|
+
import * as Health from "@smthrs/control/Health";
|
|
37
|
+
import { Context, Effect, Layer, Schema, Stream } from "effect";
|
|
38
|
+
import * as Diagnosis from "./Diagnosis.js";
|
|
39
|
+
import { GatewayError, settingRefusal } from "./GatewayError.js";
|
|
40
|
+
import * as GatewayProjection from "./GatewayProjection.js";
|
|
41
|
+
import * as GatewaySchema from "./GatewaySchema.js";
|
|
42
|
+
import { callEventKey, nativeCallEvent, nativeStepEvent } from "./internal/callEvents.js";
|
|
43
|
+
import { retainedDigestBytes } from "./internal/digestMemory.js";
|
|
44
|
+
import * as NativeResolution from "./internal/nativeResolution.js";
|
|
45
|
+
/**
|
|
46
|
+
* How often an idle subscription emits a keepalive frame.
|
|
47
|
+
*
|
|
48
|
+
* The Smithers Cloud relay cuts an idle tunnel at 600 s (the deployed relay behavior), so a
|
|
49
|
+
* quiet run must produce a frame well inside that window or the connection is
|
|
50
|
+
* dropped and every follower reconnects. Thirty seconds leaves twenty
|
|
51
|
+
* heartbeats of margin.
|
|
52
|
+
*
|
|
53
|
+
* @since 1.0.0
|
|
54
|
+
* @category models
|
|
55
|
+
*/
|
|
56
|
+
export const heartbeatIntervalMillis = 30_000;
|
|
57
|
+
/**
|
|
58
|
+
* The most runs one workspace projection folds, newest first.
|
|
59
|
+
*
|
|
60
|
+
* A workspace projection reads one full journal per run, so the number of
|
|
61
|
+
* runs it folds is bounded on purpose. This is the gateway's own ceiling, not
|
|
62
|
+
* the control plane's default page size. It is 500 because that equals
|
|
63
|
+
* `ControlSchema.maxPageSize`, letting the control plane satisfy the whole
|
|
64
|
+
* gateway allowance in one page when it can.
|
|
65
|
+
*
|
|
66
|
+
* @since 1.0.0
|
|
67
|
+
* @category models
|
|
68
|
+
*/
|
|
69
|
+
export const maxWorkspaceRuns = 500;
|
|
70
|
+
/** Whether `run` was created after `other`, ties broken by run id. */
|
|
71
|
+
const createdAfter = (run, other) => run.createdAt !== other.createdAt ? run.createdAt > other.createdAt : run.runId > other.runId;
|
|
72
|
+
/**
|
|
73
|
+
* The most journal events one run projection retains in its window.
|
|
74
|
+
*
|
|
75
|
+
* This is a retention ceiling, not a refusal. A run that journals more keeps
|
|
76
|
+
* its most recent {@link maxEventsPerRun} events and folds everything older
|
|
77
|
+
* into the carried digest that {@link maxProjectionBytes} describes.
|
|
78
|
+
*
|
|
79
|
+
* @since 1.0.0
|
|
80
|
+
* @category models
|
|
81
|
+
*/
|
|
82
|
+
export const maxEventsPerRun = 10_000;
|
|
83
|
+
/**
|
|
84
|
+
* The most journal events one run projection reads before it stops reading.
|
|
85
|
+
*
|
|
86
|
+
* The window ceiling bounds memory; this bounds work. A projection folds every
|
|
87
|
+
* event it reads, so a run with a million of them would cost a million decodes
|
|
88
|
+
* on every snapshot however few it kept. Ten times the window is the honest
|
|
89
|
+
* compromise: every real run is far inside it, and a runaway one still answers
|
|
90
|
+
* rather than running the reader out of time.
|
|
91
|
+
*
|
|
92
|
+
* @since 1.0.0
|
|
93
|
+
* @category models
|
|
94
|
+
*/
|
|
95
|
+
export const maxEventsScanned = 100_000;
|
|
96
|
+
/**
|
|
97
|
+
* The most events one `run-events` snapshot page carries.
|
|
98
|
+
*
|
|
99
|
+
* `run-events` is the one selector whose rows are the journal itself, so it is
|
|
100
|
+
* the one selector a client must page rather than fold. A snapshot answers at
|
|
101
|
+
* most this many events and a cursor to ask for the next page from, so reading
|
|
102
|
+
* a long run costs one bounded response per page instead of one response the
|
|
103
|
+
* size of the whole run.
|
|
104
|
+
*
|
|
105
|
+
* @since 1.0.0
|
|
106
|
+
* @category models
|
|
107
|
+
*/
|
|
108
|
+
export const maxEventsPerPage = 1_000;
|
|
109
|
+
/**
|
|
110
|
+
* The most terminal runs of one flow a duration fold measures.
|
|
111
|
+
*
|
|
112
|
+
* A prediction wants recent history, not all of it: an old run measured a
|
|
113
|
+
* different declaration on a different machine. The newest finished runs are
|
|
114
|
+
* the ones whose durations still describe what the flow does now, and the
|
|
115
|
+
* bound is what keeps a snapshot from reading every journal a flow ever wrote.
|
|
116
|
+
*
|
|
117
|
+
* @since 1.0.0
|
|
118
|
+
* @category models
|
|
119
|
+
*/
|
|
120
|
+
export const maxDurationRuns = 20;
|
|
121
|
+
/**
|
|
122
|
+
* The largest encoded event window or projected row set one run admits.
|
|
123
|
+
*
|
|
124
|
+
* The row budget is a refusal: rows go on the wire, and a frame larger than
|
|
125
|
+
* this is one no client asked for. The window budget is retention: events
|
|
126
|
+
* older than the newest {@link maxProjectionBytes} are folded into a carried
|
|
127
|
+
* digest instead of being held. Its compact identity contributions share this
|
|
128
|
+
* budget; if they cannot fit, the read refuses instead of reporting inexact
|
|
129
|
+
* counters or retaining an unbounded identity set.
|
|
130
|
+
*
|
|
131
|
+
* @since 1.0.0
|
|
132
|
+
* @category models
|
|
133
|
+
*/
|
|
134
|
+
export const maxProjectionBytes = 4 * 1024 * 1024;
|
|
135
|
+
/**
|
|
136
|
+
* The largest encoded form one retained journal event may take.
|
|
137
|
+
*
|
|
138
|
+
* A model request body, a response transcript, and a tool's captured output
|
|
139
|
+
* are all journaled inline, and any one of them can be megabytes. Nothing a
|
|
140
|
+
* projection reads out of a payload is long: a seat name, a flow name, an
|
|
141
|
+
* outcome, a token count, a first line. A retained event is therefore clipped
|
|
142
|
+
* to this budget rather than held whole, which is what keeps one wiki refresh
|
|
143
|
+
* that retried a model call nine times from costing a run card its whole
|
|
144
|
+
* history. `run-events` pages preserve native engine evidence because clients
|
|
145
|
+
* decode its complete contracts to offer actions. Other large bodies remain
|
|
146
|
+
* clipped; their full content belongs in run artifacts.
|
|
147
|
+
*
|
|
148
|
+
* @since 1.0.0
|
|
149
|
+
* @category models
|
|
150
|
+
*/
|
|
151
|
+
export const maxEventBytes = 16 * 1024;
|
|
152
|
+
/**
|
|
153
|
+
* The longest string a clipped payload keeps.
|
|
154
|
+
*
|
|
155
|
+
* Applied only to an event already over {@link maxEventBytes}, so an ordinary
|
|
156
|
+
* event reaches a projection byte for byte.
|
|
157
|
+
*/
|
|
158
|
+
const clippedTextPoints = 2_048;
|
|
159
|
+
/**
|
|
160
|
+
* How long a workspace follower gathers events before folding one delta.
|
|
161
|
+
*
|
|
162
|
+
* A workspace delta replaces every row, so its cost is the whole workspace,
|
|
163
|
+
* not the one event that arrived. Gathering a burst into one frame bounds the
|
|
164
|
+
* frame rate at twenty per second however many runs journal at once, and
|
|
165
|
+
* fifty milliseconds is below what a dashboard reader can notice.
|
|
166
|
+
*/
|
|
167
|
+
const workspaceDeltaCoalesceMillis = 50;
|
|
168
|
+
/** The most events one workspace batch gathers before folding inside the window. */
|
|
169
|
+
const workspaceDeltaBatchSize = 1_024;
|
|
170
|
+
/**
|
|
171
|
+
* The gateway read path.
|
|
172
|
+
*
|
|
173
|
+
* @since 1.0.0
|
|
174
|
+
* @category services
|
|
175
|
+
*/
|
|
176
|
+
export class Projections extends Context.Service()("@smthrs/gateway/Projections") {
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The part of a control-plane failure a client is allowed to see: the failure's
|
|
180
|
+
* tag and its stable code, never its message, its nested cause, or the SQL and
|
|
181
|
+
* file paths a `PersistenceError` carries.
|
|
182
|
+
*
|
|
183
|
+
* Server logs carry only an operation identifier and an allowlisted summary.
|
|
184
|
+
* `GatewayError` is the RPC error schema, so anything left on it is serialized
|
|
185
|
+
* to every bearer holder and
|
|
186
|
+
* forwarded to the browser by the product relay, and a cause JSON cannot encode
|
|
187
|
+
* would make the error frame itself fail to encode.
|
|
188
|
+
*/
|
|
189
|
+
const summarize = (cause) => {
|
|
190
|
+
const record = typeof cause === "object" && cause !== null ? cause : undefined;
|
|
191
|
+
if (record === undefined || typeof record._tag !== "string")
|
|
192
|
+
return undefined;
|
|
193
|
+
return typeof record.code === "string" ? { _tag: record._tag, code: record.code } : { _tag: record._tag };
|
|
194
|
+
};
|
|
195
|
+
/** Log only known control-plane identifiers, never arbitrary backend text. */
|
|
196
|
+
const logReadFailure = (operation, cause) => {
|
|
197
|
+
const summary = summarize(cause);
|
|
198
|
+
const known = summary?._tag === "/control/PersistenceError" && summary.code === "persistence_failed" ||
|
|
199
|
+
summary?._tag === "/control/Unavailable" && summary.code === "unavailable";
|
|
200
|
+
return Effect.logWarning({ operation, ...(known ? summary : {}) });
|
|
201
|
+
};
|
|
202
|
+
/** The read failed, with only a public error summary. */
|
|
203
|
+
const unavailable = (message, cause) => {
|
|
204
|
+
const summary = summarize(cause);
|
|
205
|
+
return new GatewayError({
|
|
206
|
+
code: "run_unavailable",
|
|
207
|
+
message,
|
|
208
|
+
...(summary === undefined ? {} : { cause: summary })
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
/** The request named something the control plane cannot serve. */
|
|
212
|
+
const malformed = (message) => new GatewayError({ code: "malformed_request", message });
|
|
213
|
+
/** A projection exceeded the bounded read or wire budget. */
|
|
214
|
+
const resourceLimit = (message) => new GatewayError({ code: "resource_limit", message });
|
|
215
|
+
/** The run a selector is scoped to, or undefined for a workspace selector. */
|
|
216
|
+
const scopeOf = (selector) => "runId" in selector ? selector.runId : undefined;
|
|
217
|
+
const emptyEventBuffer = (compactHealth = false) => ({
|
|
218
|
+
compactHealth,
|
|
219
|
+
seen: false,
|
|
220
|
+
events: [],
|
|
221
|
+
encodedBytes: 2,
|
|
222
|
+
lastPosition: { value: 0, offset: 0 },
|
|
223
|
+
carry: undefined,
|
|
224
|
+
dropped: 0
|
|
225
|
+
});
|
|
226
|
+
const textEncoder = new TextEncoder();
|
|
227
|
+
const encodedSize = (value) => textEncoder.encode(JSON.stringify(value)).byteLength;
|
|
228
|
+
/**
|
|
229
|
+
* One payload with every long string cut to {@link clippedTextPoints}.
|
|
230
|
+
*
|
|
231
|
+
* Structure is preserved so a payload stays decodable: a clipped string is
|
|
232
|
+
* still a string, an object keeps its keys, and an array keeps its length.
|
|
233
|
+
* Only the text shrinks, and `Diagnosis.clip` marks the cut.
|
|
234
|
+
*/
|
|
235
|
+
const clipDeep = (value) => {
|
|
236
|
+
if (typeof value === "string")
|
|
237
|
+
return Diagnosis.clip(value, clippedTextPoints);
|
|
238
|
+
if (Array.isArray(value))
|
|
239
|
+
return value.map(clipDeep);
|
|
240
|
+
if (typeof value === "object" && value !== null) {
|
|
241
|
+
return Object.fromEntries(Object.entries(value).map(([key, member]) => [key, clipDeep(member)]));
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
};
|
|
245
|
+
const isJsonObject = (json) => typeof json === "object" && json !== null && !Array.isArray(json);
|
|
246
|
+
/**
|
|
247
|
+
* The retained form of one event: itself when it is small, a clipped copy when
|
|
248
|
+
* it is not.
|
|
249
|
+
*
|
|
250
|
+
* A payload that is still over the budget after clipping is replaced outright.
|
|
251
|
+
* That is a payload whose size is in its shape rather than in its text — tens
|
|
252
|
+
* of thousands of keys — and no projection reads such a shape. A relevance
|
|
253
|
+
* reading is the exception: a large catalog makes its flow and skill verdicts
|
|
254
|
+
* that wide, and the run card reads its memory verdicts
|
|
255
|
+
* (`RunTrace.runMemoryOf`), so those are retained beside the marker.
|
|
256
|
+
*/
|
|
257
|
+
const retainedEvent = (event) => {
|
|
258
|
+
if (encodedSize(event) <= maxEventBytes)
|
|
259
|
+
return event;
|
|
260
|
+
const clipped = { ...event, payload: clipDeep(event.payload) };
|
|
261
|
+
if (encodedSize(clipped) <= maxEventBytes)
|
|
262
|
+
return clipped;
|
|
263
|
+
const marker = { truncated: true, encodedBytes: encodedSize(event.payload) };
|
|
264
|
+
const payload = event.payload;
|
|
265
|
+
if (event.kind === "control.agent.relevance-settled" && isJsonObject(payload)) {
|
|
266
|
+
const memory = (verdicts) => Array.isArray(verdicts)
|
|
267
|
+
? verdicts.filter((verdict) => typeof verdict === "object" && verdict !== null && verdict.kind === "memory")
|
|
268
|
+
: [];
|
|
269
|
+
const compact = {
|
|
270
|
+
...event,
|
|
271
|
+
payload: {
|
|
272
|
+
...marker,
|
|
273
|
+
kept: clipDeep(memory(payload.kept)),
|
|
274
|
+
withheld: clipDeep(memory(payload.withheld))
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
if (encodedSize(compact) <= maxEventBytes)
|
|
278
|
+
return compact;
|
|
279
|
+
}
|
|
280
|
+
return { ...event, payload: marker };
|
|
281
|
+
};
|
|
282
|
+
const decodeEvent = Schema.decodeUnknownSync(ControlSchema.ControlEvent);
|
|
283
|
+
const decodeFrame = Schema.decodeUnknownSync(GatewaySchema.GatewayFrame);
|
|
284
|
+
const decodeSnapshot = Schema.decodeUnknownSync(GatewaySchema.ProjectionSnapshot);
|
|
285
|
+
const decodedEvent = (candidate, message) => Effect.suspend(() => {
|
|
286
|
+
try {
|
|
287
|
+
const event = decodeEvent(candidate);
|
|
288
|
+
// Graph topology is a bounded contract. Refuse an oversized legacy page
|
|
289
|
+
// intact; clipping a node id or dependency would silently change the DAG.
|
|
290
|
+
const payload = event.payload;
|
|
291
|
+
if (event.kind === "control.engine.event" && typeof payload === "object" && payload !== null &&
|
|
292
|
+
"eventType" in payload && (payload.eventType === "flows.engine.plan-recorded" ||
|
|
293
|
+
payload.eventType === "flows.engine.subgraph-appended") &&
|
|
294
|
+
encodedSize(event) > maxEventBytes) {
|
|
295
|
+
return Effect.fail(resourceLimit(`One graph event exceeds ${maxEventBytes} encoded bytes`));
|
|
296
|
+
}
|
|
297
|
+
return Number.isSafeInteger(event.sequence) && event.sequence >= 0
|
|
298
|
+
? Effect.succeed(event)
|
|
299
|
+
: Effect.fail(unavailable(message, undefined));
|
|
300
|
+
}
|
|
301
|
+
catch (cause) {
|
|
302
|
+
return Effect.fail(unavailable(message, cause));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
const appendEvent = (state, candidate, message, run) => Effect.flatMap(decodedEvent(candidate, message), (decoded) => {
|
|
306
|
+
// Ordering is checked before admission by eventsOf, runDeltaFrames and
|
|
307
|
+
// workspaceDeltaFrames. bufferOf only replays an ordered cursor prefix.
|
|
308
|
+
// A payload larger than one projection reads is clipped before it is
|
|
309
|
+
// measured, so a journaled model body costs the window its own size and
|
|
310
|
+
// not the body's.
|
|
311
|
+
// A bound root's typed answer must remain exact across event clipping.
|
|
312
|
+
// Retain it in the same bounded digest as evicted root decisions; the
|
|
313
|
+
// event window carries only a marker, never a partial typed answer.
|
|
314
|
+
const resolution = encodedSize(decoded) > maxEventBytes ? NativeResolution.fromEvent(decoded) : undefined;
|
|
315
|
+
let carry = state.carry;
|
|
316
|
+
if (resolution?.result !== undefined) {
|
|
317
|
+
carry = Diagnosis.combine(carry ?? Diagnosis.emptyDigest(), {
|
|
318
|
+
...Diagnosis.emptyDigest(),
|
|
319
|
+
nativeResolution: resolution
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const event = resolution?.result === undefined ? retainedEvent(decoded) : {
|
|
323
|
+
...decoded,
|
|
324
|
+
payload: { truncated: true, encodedBytes: encodedSize(decoded.payload) }
|
|
325
|
+
};
|
|
326
|
+
// `decodedEvent` has already rebuilt payload through `Schema.Json`, so it
|
|
327
|
+
// contains neither accessors nor `toJSON` hooks and JSON encoding cannot
|
|
328
|
+
// execute caller code here.
|
|
329
|
+
const bytes = encodedSize(event);
|
|
330
|
+
let removedBytes = 0;
|
|
331
|
+
let keep = true;
|
|
332
|
+
if (state.compactHealth && event.kind === Health.statusObservedEventType) {
|
|
333
|
+
const decoded = Schema.decodeUnknownOption(Health.HealthObservation)(event.payload);
|
|
334
|
+
// Match the subject before incarnation precedence or eviction, as the
|
|
335
|
+
// authoritative health fold does. Foreign evidence must not displace
|
|
336
|
+
// this run's reading, but its raw journal position still advances below.
|
|
337
|
+
if (decoded._tag === "None" || decoded.value.outcome === "discarded" ||
|
|
338
|
+
decoded.value.subjectId !== `run:${run.runId}`)
|
|
339
|
+
keep = false;
|
|
340
|
+
else {
|
|
341
|
+
const same = state.events.findIndex((old) => old.kind === Health.statusObservedEventType &&
|
|
342
|
+
typeof old.payload === "object" && old.payload !== null && !Array.isArray(old.payload) &&
|
|
343
|
+
"incarnation" in old.payload &&
|
|
344
|
+
old.payload.incarnation === decoded.value.incarnation);
|
|
345
|
+
if (same >= 0) {
|
|
346
|
+
const previous = state.events[same];
|
|
347
|
+
const prior = previous.payload;
|
|
348
|
+
if (prior.evidenceSeq > decoded.value.evidenceSeq)
|
|
349
|
+
keep = false;
|
|
350
|
+
else
|
|
351
|
+
removedBytes += textEncoder.encode(JSON.stringify(state.events.splice(same, 1)[0])).byteLength + 1;
|
|
352
|
+
}
|
|
353
|
+
// Pin the authoritative incarnation's strongest reading. Late former
|
|
354
|
+
// owners must not evict it and let weaker evidence replace it later.
|
|
355
|
+
// The other slots retain recent opaque incarnations within the same cap.
|
|
356
|
+
const healthEntries = state.events.filter((old) => old.kind === Health.statusObservedEventType);
|
|
357
|
+
if (keep && healthEntries.length >= 16) {
|
|
358
|
+
const incarnation = Health.runIncarnation(run);
|
|
359
|
+
// There is at most one entry per incarnation, so an unprotected entry exists.
|
|
360
|
+
const oldest = state.events.indexOf(healthEntries.find((old) => old.payload.incarnation !== incarnation));
|
|
361
|
+
removedBytes += textEncoder.encode(JSON.stringify(state.events.splice(oldest, 1)[0])).byteLength + 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (state.compactHealth && event.kind === "control.monitor.beat")
|
|
366
|
+
keep = false;
|
|
367
|
+
let encodedBytes = Math.max(2, state.encodedBytes - removedBytes) +
|
|
368
|
+
retainedDigestBytes(carry) - retainedDigestBytes(state.carry) +
|
|
369
|
+
(keep ? bytes + (state.events.length === 0 ? 0 : 1) : 0);
|
|
370
|
+
// Unreachable today: every term is a byte length, `state.encodedBytes` is
|
|
371
|
+
// bounded by `maxProjectionBytes` by the loop below, and one event is
|
|
372
|
+
// bounded by the string the control plane sent, so the sum is always a
|
|
373
|
+
// safe integer. The refusal stays because a later term need not be, and a
|
|
374
|
+
// window that cannot count its own bytes must refuse rather than report.
|
|
375
|
+
/* v8 ignore next 3 -- unreachable while every term above is a byte length. */
|
|
376
|
+
if (!Number.isSafeInteger(encodedBytes)) {
|
|
377
|
+
return Effect.fail(unavailable(message, undefined));
|
|
378
|
+
}
|
|
379
|
+
const offset = state.seen && event.sequence === state.lastPosition.value
|
|
380
|
+
? state.lastPosition.offset + 1
|
|
381
|
+
: 0;
|
|
382
|
+
if (keep)
|
|
383
|
+
state.events.push(event);
|
|
384
|
+
// Event bodies may leave the window while compact identity contributions
|
|
385
|
+
// keep the diagnosis exact. Both share the byte budget; if even the scalar
|
|
386
|
+
// state cannot fit, refuse instead of silently forgetting identities.
|
|
387
|
+
let dropped = state.dropped;
|
|
388
|
+
while (state.events.length > maxEventsPerRun || encodedBytes > maxProjectionBytes) {
|
|
389
|
+
const evicted = state.events.shift();
|
|
390
|
+
if (evicted === undefined) {
|
|
391
|
+
return Effect.fail(resourceLimit(`Projection identity state exceeds ${maxProjectionBytes} encoded bytes`));
|
|
392
|
+
}
|
|
393
|
+
encodedBytes = Math.max(2, encodedBytes - encodedSize(evicted) - (state.events.length === 0 ? 0 : 1));
|
|
394
|
+
const previousCarryBytes = retainedDigestBytes(carry);
|
|
395
|
+
carry = Diagnosis.combine(carry ?? Diagnosis.emptyDigest(), Diagnosis.digest([evicted], run.runId));
|
|
396
|
+
encodedBytes += retainedDigestBytes(carry) - previousCarryBytes;
|
|
397
|
+
dropped += 1;
|
|
398
|
+
}
|
|
399
|
+
return Effect.succeed({
|
|
400
|
+
compactHealth: state.compactHealth,
|
|
401
|
+
seen: true,
|
|
402
|
+
events: state.events,
|
|
403
|
+
encodedBytes,
|
|
404
|
+
lastPosition: { value: event.sequence, offset },
|
|
405
|
+
carry,
|
|
406
|
+
dropped
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
const bufferOf = (events, message, compactHealth, run) => events.reduce((state, event) => Effect.flatMap(state, (buffer) => appendEvent(buffer, event, message, run)), Effect.succeed(emptyEventBuffer(compactHealth)));
|
|
410
|
+
const cursorOf = (selector, position) => ({
|
|
411
|
+
selector,
|
|
412
|
+
projection: selector._tag,
|
|
413
|
+
runId: scopeOf(selector) ?? null,
|
|
414
|
+
value: position.value,
|
|
415
|
+
offset: position.offset
|
|
416
|
+
});
|
|
417
|
+
const comparePosition = (left, right) => left.value === right.value ? left.offset - right.offset : left.value - right.value;
|
|
418
|
+
const positionedEvents = (events) => {
|
|
419
|
+
let sequence = -1;
|
|
420
|
+
let offset = 0;
|
|
421
|
+
return events.map((event) => {
|
|
422
|
+
offset = event.sequence === sequence ? offset + 1 : 0;
|
|
423
|
+
sequence = event.sequence;
|
|
424
|
+
return { event, position: { value: sequence, offset } };
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
const sameSelector = (left, right) => {
|
|
428
|
+
if (left._tag !== right._tag)
|
|
429
|
+
return false;
|
|
430
|
+
switch (left._tag) {
|
|
431
|
+
case "workspace-runs":
|
|
432
|
+
return left.flowId === right.flowId;
|
|
433
|
+
case "approvals":
|
|
434
|
+
return left.runId === right.runId;
|
|
435
|
+
case "node-output": {
|
|
436
|
+
const candidate = right;
|
|
437
|
+
return left.runId === candidate.runId && left.nodeId === candidate.nodeId;
|
|
438
|
+
}
|
|
439
|
+
case "flow-durations":
|
|
440
|
+
return left.flowId === right.flowId;
|
|
441
|
+
default:
|
|
442
|
+
return left.runId === right.runId;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* The rows one run contributes to a selector.
|
|
447
|
+
*
|
|
448
|
+
* Pure and total over facts already read: it never reads the control plane, so
|
|
449
|
+
* a delta folded from accumulated events and a fresh snapshot compute the same
|
|
450
|
+
* rows from the same events. `workspace-runs` appears here because a workspace
|
|
451
|
+
* listing is one summary row per run, folded by this same function.
|
|
452
|
+
*/
|
|
453
|
+
const rowsOfRun = (selector, source, now) => {
|
|
454
|
+
switch (selector._tag) {
|
|
455
|
+
case "workspace-runs":
|
|
456
|
+
case "run-summary":
|
|
457
|
+
return [GatewayProjection.runSummary(source.run, source.events, now, source.carry)];
|
|
458
|
+
case "run-tree":
|
|
459
|
+
return GatewayProjection.runTree(source.run, source.events);
|
|
460
|
+
case "run-events":
|
|
461
|
+
return source.events;
|
|
462
|
+
case "transcript":
|
|
463
|
+
return GatewayProjection.transcript(source.events);
|
|
464
|
+
case "approvals":
|
|
465
|
+
return GatewayProjection.approvals(source.events, source.run);
|
|
466
|
+
case "node-output":
|
|
467
|
+
return GatewayProjection.nodeOutput(source.events).filter((row) => row.nodeId === selector.nodeId);
|
|
468
|
+
case "flow-durations":
|
|
469
|
+
// One run contributes the executions it measured, never a percentile:
|
|
470
|
+
// a percentile over one run is that run, and the rows this selector
|
|
471
|
+
// serves are folded across every run the flow has finished. The carry
|
|
472
|
+
// travels with the window because the record that names this run's
|
|
473
|
+
// native root is the first one a bounded window drops.
|
|
474
|
+
return GatewayProjection.nodeDurations(source.events, source.carry);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
/** Turns opened so far, for numbering appended transcript rows without a refold. */
|
|
478
|
+
const turnsOpened = (events) => GatewayProjection.transcript(events).at(-1)?.turn ?? 0;
|
|
479
|
+
/**
|
|
480
|
+
* The transcript rows one appended event contributes.
|
|
481
|
+
*
|
|
482
|
+
* A transcript row is immutable once folded and only the turn counter carries
|
|
483
|
+
* across events, so folding the one event and offsetting its turn produces
|
|
484
|
+
* exactly the rows a full refold would append.
|
|
485
|
+
*/
|
|
486
|
+
const transcriptAppend = (event, turnsBefore) => GatewayProjection.transcript([event]).map((row) => ({ ...row, turn: row.turn + turnsBefore }));
|
|
487
|
+
/** Snapshot and follow admit exactly the same runs to the workspace inbox. */
|
|
488
|
+
const eligibleForWorkspace = (selector, source) => selector._tag !== "approvals" ||
|
|
489
|
+
source.run.status === "waiting-approval" &&
|
|
490
|
+
GatewayProjection.approvals(source.events, source.run).some((row) => row.status === "pending");
|
|
491
|
+
/**
|
|
492
|
+
* Which open human wait a pending approval row is about, if it is about one.
|
|
493
|
+
*
|
|
494
|
+
* A nested wait is rolled onto the root AND onto every execution between, so
|
|
495
|
+
* one question reaches a workspace listing once per ancestor. The inbox is a
|
|
496
|
+
* list of questions, not of the runs that contain them, so it keeps one row
|
|
497
|
+
* for each wait point: the one its outermost owner projects. See
|
|
498
|
+
* {@link ownerRank}.
|
|
499
|
+
*/
|
|
500
|
+
const waitIdentity = (row) => row.waitRunId === undefined ? undefined : `${row.waitRunId}:${row.requestId}`;
|
|
501
|
+
/**
|
|
502
|
+
* How far out each listed run sits, as a comparable key: fewer listed
|
|
503
|
+
* ancestors first, then the earlier created, then the run id.
|
|
504
|
+
*
|
|
505
|
+
* Ancestry is the explicit `parentRunId` chain among the runs listed, because
|
|
506
|
+
* the listing order is the control plane's to choose (the SQL listing is
|
|
507
|
+
* newest first) and two runs of one tree can share a creation millisecond.
|
|
508
|
+
* Creation order breaks the ties ancestry leaves: an ancestor exists before
|
|
509
|
+
* the execution it spawned.
|
|
510
|
+
*/
|
|
511
|
+
const ownerRank = (runs) => {
|
|
512
|
+
const listed = new Map(runs.map((run) => [run.runId, run]));
|
|
513
|
+
const depths = new Map();
|
|
514
|
+
const depthOf = (run) => {
|
|
515
|
+
const known = depths.get(run.runId);
|
|
516
|
+
if (known !== undefined)
|
|
517
|
+
return known;
|
|
518
|
+
// Marked before the walk so a cyclic parent chain ends instead of looping.
|
|
519
|
+
depths.set(run.runId, 0);
|
|
520
|
+
const parent = run.parentRunId === undefined ? undefined : listed.get(run.parentRunId);
|
|
521
|
+
const depth = parent === undefined ? 0 : depthOf(parent) + 1;
|
|
522
|
+
depths.set(run.runId, depth);
|
|
523
|
+
return depth;
|
|
524
|
+
};
|
|
525
|
+
return (left, right) => depthOf(left) - depthOf(right) || (createdAfter(left, right) ? 1 : -1);
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* The workspace approvals inbox over each run's own pending rows.
|
|
529
|
+
*
|
|
530
|
+
* Every read of the inbox, snapshot and delta alike, folds through here, so a
|
|
531
|
+
* nested question is one row owned by its outermost run whichever read
|
|
532
|
+
* produced it. Rows keep the order of the runs given.
|
|
533
|
+
*/
|
|
534
|
+
const approvalInbox = (perRun) => {
|
|
535
|
+
const owners = new Map();
|
|
536
|
+
const rank = ownerRank(perRun.map(({ run }) => run));
|
|
537
|
+
for (const { run, rows } of [...perRun].sort((left, right) => rank(left.run, right.run))) {
|
|
538
|
+
for (const row of rows) {
|
|
539
|
+
const identity = waitIdentity(row);
|
|
540
|
+
if (identity !== undefined && !owners.has(identity))
|
|
541
|
+
owners.set(identity, run.runId);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const inbox = [];
|
|
545
|
+
for (const { run, rows } of perRun) {
|
|
546
|
+
for (const row of rows) {
|
|
547
|
+
const identity = waitIdentity(row);
|
|
548
|
+
if (identity !== undefined) {
|
|
549
|
+
if (owners.get(identity) !== run.runId)
|
|
550
|
+
continue;
|
|
551
|
+
// Taken once: a question listed twice by its owner is still one row.
|
|
552
|
+
owners.delete(identity);
|
|
553
|
+
}
|
|
554
|
+
inbox.push(row);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return inbox;
|
|
558
|
+
};
|
|
559
|
+
/** One run's rows that are still owed an answer: its share of the inbox. */
|
|
560
|
+
const pendingApprovals = (source) => GatewayProjection.approvals(source.events, source.run).filter((row) => row.status === "pending");
|
|
561
|
+
/**
|
|
562
|
+
* The rows a workspace selector projects across every run it read.
|
|
563
|
+
*
|
|
564
|
+
* Most of them are one run's rows, concatenated. Two are not. The approvals
|
|
565
|
+
* inbox wants only what a human still owes an answer to, once per question
|
|
566
|
+
* (see {@link approvalInbox}), where a run card wants that run's decided gates
|
|
567
|
+
* too. And `flow-durations` is a cross-run fold: every run contributes the
|
|
568
|
+
* executions it measured and the percentiles are ranked over all of them at
|
|
569
|
+
* once, so a run contributes a sample rather than a row.
|
|
570
|
+
*/
|
|
571
|
+
const rowsOfWorkspace = (selector, runs, now) => {
|
|
572
|
+
if (selector._tag === "flow-durations") {
|
|
573
|
+
return GatewayProjection.flowDurations(selector.flowId, runs.flatMap((source) => rowsOfRun(selector, source, now)));
|
|
574
|
+
}
|
|
575
|
+
if (selector._tag !== "approvals")
|
|
576
|
+
return runs.flatMap((source) => rowsOfRun(selector, source, now));
|
|
577
|
+
return approvalInbox(runs.map((source) => ({ run: source.run, rows: pendingApprovals(source) })));
|
|
578
|
+
};
|
|
579
|
+
/** The rows a selector projects from the facts one read produced. */
|
|
580
|
+
const rowsOf = (selector, source, now) => source._tag === "run" ? rowsOfRun(selector, source.run, now) : rowsOfWorkspace(selector, source.runs, now);
|
|
581
|
+
const boundedRows = (selector, candidate) => Effect.suspend(() => {
|
|
582
|
+
try {
|
|
583
|
+
const schema = Schema.Array(GatewaySchema.rowSchemaFor(selector));
|
|
584
|
+
const rows = Schema.decodeUnknownSync(schema)(candidate);
|
|
585
|
+
const encoded = Schema.encodeUnknownSync(schema)(rows);
|
|
586
|
+
const bytes = textEncoder.encode(JSON.stringify(encoded)).byteLength;
|
|
587
|
+
return bytes <= maxProjectionBytes
|
|
588
|
+
? Effect.succeed(rows)
|
|
589
|
+
: Effect.fail(resourceLimit(`Projection rows exceed ${maxProjectionBytes} encoded bytes`));
|
|
590
|
+
}
|
|
591
|
+
catch (cause) {
|
|
592
|
+
return Effect.fail(unavailable("Projection produced invalid rows", cause));
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
const frameOf = (candidate) => Effect.suspend(() => {
|
|
596
|
+
try {
|
|
597
|
+
return Effect.succeed(decodeFrame(candidate));
|
|
598
|
+
/* v8 ignore next -- rows, selectors, cursors, and tags were admitted before this internal assembly step. */
|
|
599
|
+
}
|
|
600
|
+
catch (cause) {
|
|
601
|
+
/* v8 ignore next -- the assembled frame consists only of already-admitted fields. */
|
|
602
|
+
return Effect.fail(unavailable("Projection produced an invalid frame", cause));
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
const snapshotOf = (candidate) => Effect.suspend(() => {
|
|
606
|
+
try {
|
|
607
|
+
return Effect.succeed(decodeSnapshot(candidate));
|
|
608
|
+
/* v8 ignore next -- boundedRows and cursorOf admit every field before this internal assembly step. */
|
|
609
|
+
}
|
|
610
|
+
catch (cause) {
|
|
611
|
+
/* v8 ignore next -- the assembled snapshot consists only of already-admitted fields. */
|
|
612
|
+
return Effect.fail(unavailable("Projection produced an invalid snapshot", cause));
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
/**
|
|
616
|
+
* The cursor the read reached.
|
|
617
|
+
*
|
|
618
|
+
* A run's cursor is the last sequence its journal committed, taken from the
|
|
619
|
+
* same array the rows were folded from. A workspace cursor is `0`: control
|
|
620
|
+
* journal sequences belong to per-run partitions (`@smthrs/control`
|
|
621
|
+
* `ControlLive.streamForRun` and `snapshotHighWater` are keyed by `runId`), so
|
|
622
|
+
* there is no workspace-wide sequence to advertise and no workspace resume.
|
|
623
|
+
*/
|
|
624
|
+
const cursorPositionOf = (source) => source._tag === "run" ? source.run.lastPosition : { value: 0, offset: 0 };
|
|
625
|
+
/** Implements a read path after its construction settings have been admitted. */
|
|
626
|
+
const makeService = (control, heartbeatMillis, now) => {
|
|
627
|
+
// A bounded, disposable journal read cache. The journal remains authoritative:
|
|
628
|
+
// every read asks it for the suffix, including when the summary is unchanged.
|
|
629
|
+
// Cache eviction or host restart simply rebuilds the same fold from sequence 0.
|
|
630
|
+
const journalBuffers = new Map();
|
|
631
|
+
const eventsOf = (run, compactHealth) => Effect.suspend(() => {
|
|
632
|
+
const runId = run.runId;
|
|
633
|
+
const key = `${runId}:${compactHealth}`;
|
|
634
|
+
const cached = journalBuffers.get(key);
|
|
635
|
+
const seed = cached === undefined ? emptyEventBuffer(compactHealth) : { ...cached, events: [...cached.events] };
|
|
636
|
+
const message = `Reading the events of ${runId} failed`;
|
|
637
|
+
let position;
|
|
638
|
+
return Stream.runFoldEffect(control.watch({
|
|
639
|
+
runId,
|
|
640
|
+
follow: false,
|
|
641
|
+
...(cached === undefined || !cached.seen || cached.lastPosition.value === 0
|
|
642
|
+
? {} :
|
|
643
|
+
{ afterSequence: cached.lastPosition.value - 1 })
|
|
644
|
+
}).pipe(Stream.tapError((cause) => logReadFailure("read-events", cause)), Stream.mapError((cause) => unavailable(message, cause)), Stream.take(maxEventsScanned)), () => seed, (buffer, candidate) => Effect.flatMap(decodedEvent(candidate, message), (event) => {
|
|
645
|
+
if (position !== undefined && event.sequence < position.value) {
|
|
646
|
+
return Effect.fail(unavailable(message, undefined));
|
|
647
|
+
}
|
|
648
|
+
position = { value: event.sequence, offset: position?.value === event.sequence ? position.offset + 1 : 0 };
|
|
649
|
+
if (cached !== undefined && cached.seen && comparePosition(position, cached.lastPosition) <= 0) {
|
|
650
|
+
return Effect.succeed(buffer);
|
|
651
|
+
}
|
|
652
|
+
return appendEvent(buffer, event, message, run);
|
|
653
|
+
})).pipe(Effect.tap((buffer) => Effect.sync(() => {
|
|
654
|
+
const current = journalBuffers.get(key);
|
|
655
|
+
if (current === undefined || comparePosition(buffer.lastPosition, current.lastPosition) >= 0) {
|
|
656
|
+
journalBuffers.delete(key);
|
|
657
|
+
journalBuffers.set(key, { ...buffer, events: [...buffer.events] });
|
|
658
|
+
if (journalBuffers.size > 32)
|
|
659
|
+
journalBuffers.delete(journalBuffers.keys().next().value);
|
|
660
|
+
}
|
|
661
|
+
})));
|
|
662
|
+
});
|
|
663
|
+
/**
|
|
664
|
+
* One bounded page of a run's journal, starting after `after`.
|
|
665
|
+
*
|
|
666
|
+
* This is the `run-events` read, and it is deliberately not the fold every
|
|
667
|
+
* other selector uses. The other selectors answer rows computed from a
|
|
668
|
+
* journal; `run-events` answers the journal, so its response grows with the
|
|
669
|
+
* run unless the read itself is bounded. It reads from the control plane's
|
|
670
|
+
* own cursor, keeps at most {@link maxEventsPerPage} events and
|
|
671
|
+
* {@link maxProjectionBytes}, and reports the position it reached so the
|
|
672
|
+
* caller asks for the next page from there.
|
|
673
|
+
*
|
|
674
|
+
* `afterSequence` replays the cursor's whole journal entry rather than
|
|
675
|
+
* starting inside it, because an entry expands into several events and their
|
|
676
|
+
* offsets are only countable from the entry's start. The replayed prefix is
|
|
677
|
+
* dropped here.
|
|
678
|
+
*/
|
|
679
|
+
const runEventsPage = (run, after) => {
|
|
680
|
+
const runId = run.runId;
|
|
681
|
+
const message = `Reading the events of ${runId} failed`;
|
|
682
|
+
const empty = { events: [], bytes: 2, seen: undefined, last: undefined, full: false };
|
|
683
|
+
// Set when the page is full, so the next pull ends the stream instead of
|
|
684
|
+
// decoding the rest of a long journal into a page that cannot grow.
|
|
685
|
+
let stopped = false;
|
|
686
|
+
return Stream.runFoldEffect(control.watch({
|
|
687
|
+
runId,
|
|
688
|
+
...(after === undefined || after.value === 0 ? {} : { afterSequence: after.value - 1 }),
|
|
689
|
+
follow: false
|
|
690
|
+
}).pipe(Stream.tapError((cause) => logReadFailure("read-events", cause)), Stream.mapError((cause) => unavailable(message, cause)), Stream.take(maxEventsScanned), Stream.takeWhile(() => !stopped)), () => empty, (page, candidate) => page.full ? Effect.succeed(page) : Effect.flatMap(decodedEvent(candidate, `${message}: the control plane returned an invalid event`), (decoded) => {
|
|
691
|
+
// A journal that moves backward is a broken control plane, not a
|
|
692
|
+
// short page, and the fold refuses it exactly as the window does.
|
|
693
|
+
if (page.seen !== undefined && decoded.sequence < page.seen.value) {
|
|
694
|
+
return Effect.fail(unavailable(message, undefined));
|
|
695
|
+
}
|
|
696
|
+
const position = {
|
|
697
|
+
value: decoded.sequence,
|
|
698
|
+
offset: page.seen?.value === decoded.sequence ? page.seen.offset + 1 : 0
|
|
699
|
+
};
|
|
700
|
+
const seen = position;
|
|
701
|
+
if (after !== undefined && comparePosition(position, after) <= 0) {
|
|
702
|
+
return Effect.succeed({ ...page, seen });
|
|
703
|
+
}
|
|
704
|
+
// Native results are contracts, not display text. Clipping changes
|
|
705
|
+
// their meaning and can hide validated actions such as Vibe.
|
|
706
|
+
// Match live deltas, which already carry the original event.
|
|
707
|
+
const event = decoded.kind === "control.engine.event" ? decoded : retainedEvent(decoded);
|
|
708
|
+
if (encodedSize(event) + 2 > maxProjectionBytes) {
|
|
709
|
+
return Effect.fail(resourceLimit(`One journal event exceeds ${maxProjectionBytes} encoded bytes`));
|
|
710
|
+
}
|
|
711
|
+
const bytes = page.bytes + encodedSize(event) + (page.events.length === 0 ? 0 : 1);
|
|
712
|
+
if (page.events.length > 0 && bytes > maxProjectionBytes) {
|
|
713
|
+
stopped = true;
|
|
714
|
+
return Effect.succeed({ ...page, seen, full: true });
|
|
715
|
+
}
|
|
716
|
+
page.events.push(event);
|
|
717
|
+
stopped = page.events.length >= maxEventsPerPage;
|
|
718
|
+
return Effect.succeed({ events: page.events, bytes, seen, last: position, full: stopped });
|
|
719
|
+
})).pipe(Effect.map((page) => ({
|
|
720
|
+
events: page.events,
|
|
721
|
+
lastPosition: page.last ?? after ?? { value: 0, offset: 0 }
|
|
722
|
+
})));
|
|
723
|
+
};
|
|
724
|
+
const runsMatching = (filters, newest) => {
|
|
725
|
+
const singleRun = filters.runId !== undefined;
|
|
726
|
+
// Only runOf omits the bound: it reads one identified run.
|
|
727
|
+
const ceiling = newest ?? 1;
|
|
728
|
+
const page = (accumulated, seen, cursor) => {
|
|
729
|
+
const remaining = ceiling - accumulated.length;
|
|
730
|
+
return control.list({
|
|
731
|
+
_tag: "runs",
|
|
732
|
+
filters,
|
|
733
|
+
limit: remaining,
|
|
734
|
+
...(newest === undefined ? {} : { order: "newest" }),
|
|
735
|
+
...(cursor === undefined ? {} : { cursor })
|
|
736
|
+
}).pipe(Effect.tapError((cause) => logReadFailure("list-runs", cause)), Effect.mapError((cause) => unavailable("Listing runs failed", cause)), Effect.flatMap((response) => {
|
|
737
|
+
if (response._tag !== "runs")
|
|
738
|
+
return Effect.succeed(accumulated);
|
|
739
|
+
const ids = new Set(accumulated.map((run) => run.runId));
|
|
740
|
+
const added = response.items.filter((run) => {
|
|
741
|
+
if (ids.has(run.runId))
|
|
742
|
+
return false;
|
|
743
|
+
ids.add(run.runId);
|
|
744
|
+
return true;
|
|
745
|
+
}).slice(0, remaining);
|
|
746
|
+
const runs = [...accumulated, ...added];
|
|
747
|
+
if (singleRun || added.length === 0 || response.nextCursor === undefined ||
|
|
748
|
+
runs.length >= ceiling || seen.has(response.nextCursor))
|
|
749
|
+
return Effect.succeed(runs);
|
|
750
|
+
seen.add(response.nextCursor);
|
|
751
|
+
// Each continued page adds at least one run, so the run ceiling
|
|
752
|
+
// also caps the number of pages at maxWorkspaceRuns.
|
|
753
|
+
return page(runs, seen, response.nextCursor);
|
|
754
|
+
}));
|
|
755
|
+
};
|
|
756
|
+
return Effect.suspend(() => page([], new Set()));
|
|
757
|
+
};
|
|
758
|
+
/**
|
|
759
|
+
* The one run a run-scoped selector names, or a typed refusal.
|
|
760
|
+
*
|
|
761
|
+
* Every run-scoped selector goes through here, so an unknown run id is
|
|
762
|
+
* refused the same way for all of them. Reading a run's events alone cannot
|
|
763
|
+
* tell an unknown run from a run with no events: `@smthrs/control`
|
|
764
|
+
* `ControlLive.snapshotForRun` answers an empty stream for both.
|
|
765
|
+
*/
|
|
766
|
+
const runOf = (runId) => runsMatching({ runId }).pipe(Effect.flatMap((runs) => {
|
|
767
|
+
const run = runs.find((candidate) => candidate.runId === runId);
|
|
768
|
+
return run === undefined
|
|
769
|
+
? Effect.fail(new GatewayError({ code: "run_not_found", message: `No run ${runId}` }))
|
|
770
|
+
: Effect.succeed(run);
|
|
771
|
+
}));
|
|
772
|
+
/** Pin the journal between equal summaries; refuse a continuously moving row. */
|
|
773
|
+
const consistentRunSource = (before, attempts = 8, compactHealth = true) => Effect.gen(function* () {
|
|
774
|
+
const buffer = yield* eventsOf(before, compactHealth);
|
|
775
|
+
const run = yield* runOf(before.runId);
|
|
776
|
+
if (JSON.stringify(before) === JSON.stringify(run))
|
|
777
|
+
return { run, ...buffer };
|
|
778
|
+
if (attempts === 1)
|
|
779
|
+
return yield* Effect.fail(unavailable("Run changed throughout the snapshot read", undefined));
|
|
780
|
+
return yield* consistentRunSource(run, attempts - 1, compactHealth);
|
|
781
|
+
});
|
|
782
|
+
/** One run and its journal at a reconciled cutoff. */
|
|
783
|
+
const runSourceOf = (runId, compactHealth = true) => Effect.flatMap(runOf(runId), (run) => consistentRunSource(run, 8, compactHealth));
|
|
784
|
+
/** Newest terminal runs, filtered and limited by the control query itself. */
|
|
785
|
+
const durationRunsOf = (flowId) => runsMatching({ flowId, terminal: true }, maxDurationRuns);
|
|
786
|
+
/** Every run a workspace selector folds, and each one's journal. */
|
|
787
|
+
const workspaceSourceOf = (selector) => Effect.flatMap(
|
|
788
|
+
// The inbox asks the control plane which runs are parked rather than
|
|
789
|
+
// reading every run's journal to find out, and a duration fold asks it
|
|
790
|
+
// for one flow's runs rather than for the workspace's.
|
|
791
|
+
selector._tag === "flow-durations"
|
|
792
|
+
? durationRunsOf(selector.flowId)
|
|
793
|
+
: runsMatching(selector._tag === "approvals" ?
|
|
794
|
+
{ status: "waiting-approval" }
|
|
795
|
+
: selector._tag === "workspace-runs" && selector.flowId !== undefined
|
|
796
|
+
? { flowId: selector.flowId }
|
|
797
|
+
: {}, maxWorkspaceRuns), (runs) => Effect.map(Effect.forEach(runs, (run) => consistentRunSource(run), { concurrency: 8 }), (sources) => sources.filter((source) => eligibleForWorkspace(selector, source))));
|
|
798
|
+
/** The reconciled source a snapshot folds. */
|
|
799
|
+
const sourceOf = (selector) => {
|
|
800
|
+
const runId = scopeOf(selector);
|
|
801
|
+
return runId === undefined
|
|
802
|
+
? Effect.map(workspaceSourceOf(selector), (runs) => ({ _tag: "workspace", runs }))
|
|
803
|
+
: Effect.map(runSourceOf(runId, selector._tag !== "run-events"), (run) => ({ _tag: "run", run }));
|
|
804
|
+
};
|
|
805
|
+
const snapshot = (selector, after) => Effect.gen(function* () {
|
|
806
|
+
if (after !== undefined) {
|
|
807
|
+
if (selector._tag !== "run-events") {
|
|
808
|
+
return yield* malformed("Only run-events snapshots accept an after cursor");
|
|
809
|
+
}
|
|
810
|
+
const scope = resumeScope(selector, after);
|
|
811
|
+
if (typeof scope !== "string")
|
|
812
|
+
return yield* scope;
|
|
813
|
+
}
|
|
814
|
+
// `run-events` answers the journal itself, so it pages rather than folds:
|
|
815
|
+
// one bounded read from the caller's cursor, and the position it reached
|
|
816
|
+
// to ask for the next page from. Every other selector answers rows whose
|
|
817
|
+
// size is bounded by the selector, so it folds the reconciled source.
|
|
818
|
+
if (selector._tag === "run-events") {
|
|
819
|
+
const run = yield* runOf(selector.runId);
|
|
820
|
+
const page = yield* runEventsPage(run, after);
|
|
821
|
+
const rows = yield* boundedRows(selector, page.events);
|
|
822
|
+
return yield* snapshotOf({ selector, cursor: cursorOf(selector, page.lastPosition), rows });
|
|
823
|
+
}
|
|
824
|
+
const source = yield* sourceOf(selector);
|
|
825
|
+
const rows = yield* boundedRows(selector, rowsOf(selector, source, now()));
|
|
826
|
+
return yield* snapshotOf({ selector, cursor: cursorOf(selector, cursorPositionOf(source)), rows });
|
|
827
|
+
});
|
|
828
|
+
const snapshotFrames = (selector, source, rows) => {
|
|
829
|
+
const cursor = cursorOf(selector, cursorPositionOf(source));
|
|
830
|
+
return Effect.forEach([
|
|
831
|
+
{ _tag: "snapshot-start", selector, cursor },
|
|
832
|
+
...rows.map((row) => ({ _tag: "row", selector, cursor, row })),
|
|
833
|
+
{ _tag: "snapshot-end", selector, cursor }
|
|
834
|
+
], frameOf);
|
|
835
|
+
};
|
|
836
|
+
/**
|
|
837
|
+
* The rows one delta carries.
|
|
838
|
+
*
|
|
839
|
+
* `run-events`' rows are the ordered events themselves, so its delta is the
|
|
840
|
+
* one event that arrived. `transcript` rows are immutable once folded, so
|
|
841
|
+
* its delta is the rows that event appended, numbered after the turns the
|
|
842
|
+
* follower has already counted. Every other selector answers a full
|
|
843
|
+
* replacement folded from the accumulated events, because recomputation is
|
|
844
|
+
* the only delta that cannot disagree with a fresh snapshot. The caller
|
|
845
|
+
* refreshes authoritative rows before compaction so retention and rendering
|
|
846
|
+
* use the same owner even when a fenced status write journals no event.
|
|
847
|
+
*/
|
|
848
|
+
const deltaRows = (selector, run, event, events, turnsBefore, duplicateCall, carry) => {
|
|
849
|
+
if (selector._tag === "run-events")
|
|
850
|
+
return Effect.succeed([event]);
|
|
851
|
+
if (selector._tag === "transcript") {
|
|
852
|
+
return Effect.succeed(duplicateCall ? [] : transcriptAppend(event, turnsBefore));
|
|
853
|
+
}
|
|
854
|
+
return Effect.succeed(rowsOfRun(selector, { run, events, carry }, now()));
|
|
855
|
+
};
|
|
856
|
+
/**
|
|
857
|
+
* Deltas for one run, following from `from` and folding over the events the
|
|
858
|
+
* caller already read plus each event that arrives.
|
|
859
|
+
*
|
|
860
|
+
* A historical run summary rebuilds its health prefix once from the follow
|
|
861
|
+
* replay. Later deltas only append: re-reading history for every event would
|
|
862
|
+
* make a subscription quadratic in run length and resend the whole log on
|
|
863
|
+
* each frame.
|
|
864
|
+
*/
|
|
865
|
+
const runDeltaFrames = (selector, source, from) => {
|
|
866
|
+
const runId = source.run.runId;
|
|
867
|
+
const message = `Following ${runId} failed`;
|
|
868
|
+
// The final compacted buffer cannot seed an earlier health cursor: a
|
|
869
|
+
// later reading may have replaced the stronger observation held there.
|
|
870
|
+
// Rebuild that prefix in the same bounded fold from the follow replay.
|
|
871
|
+
const replayHealth = selector._tag === "run-summary" && comparePosition(from, source.lastPosition) < 0;
|
|
872
|
+
const seedEvents = positionedEvents(source.events)
|
|
873
|
+
.filter(({ position }) => comparePosition(position, from) <= 0)
|
|
874
|
+
.map(({ event }) => event);
|
|
875
|
+
const seed = replayHealth ?
|
|
876
|
+
Effect.succeed(emptyEventBuffer(true)) :
|
|
877
|
+
comparePosition(from, source.lastPosition) === 0
|
|
878
|
+
? Effect.succeed({
|
|
879
|
+
compactHealth: source.compactHealth,
|
|
880
|
+
seen: source.seen,
|
|
881
|
+
events: source.events,
|
|
882
|
+
encodedBytes: source.encodedBytes,
|
|
883
|
+
lastPosition: source.lastPosition,
|
|
884
|
+
carry: source.carry,
|
|
885
|
+
dropped: source.dropped
|
|
886
|
+
})
|
|
887
|
+
: bufferOf(seedEvents, `${message}: the cursor seed is invalid`, source.compactHealth, source.run);
|
|
888
|
+
return Stream.unwrap(Effect.map(seed, (initial) => control.watch({
|
|
889
|
+
runId,
|
|
890
|
+
// Sequence zero also needs replay for empty seeds and derived offsets.
|
|
891
|
+
...(from.value === 0 || replayHealth ? {} : { afterSequence: from.value - 1 }),
|
|
892
|
+
follow: true
|
|
893
|
+
}).pipe(Stream.tapError((cause) => logReadFailure("follow-run", cause)), Stream.mapError((cause) => unavailable(message, cause)), Stream.mapAccumEffect(() => ({
|
|
894
|
+
buffer: initial,
|
|
895
|
+
observed: undefined,
|
|
896
|
+
turns: selector._tag === "transcript" ? turnsOpened(initial.events) : 0,
|
|
897
|
+
reportedCalls: (() => {
|
|
898
|
+
const seen = new Map();
|
|
899
|
+
if (selector._tag === "transcript") {
|
|
900
|
+
for (const event of initial.events) {
|
|
901
|
+
const key = callEventKey(event);
|
|
902
|
+
if (key !== undefined) {
|
|
903
|
+
seen.set(key, seen.get(key) === true || (nativeCallEvent(event) ?? nativeStepEvent(event)) !== undefined);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return seen;
|
|
908
|
+
})()
|
|
909
|
+
}), (state, candidate) => Effect.flatMap(decodedEvent(candidate, `${message}: the control plane returned an invalid event`), (event) => {
|
|
910
|
+
if (state.observed !== undefined && event.sequence < state.observed.value) {
|
|
911
|
+
return Effect.fail(unavailable(`${message}: event sequences moved backward`, undefined));
|
|
912
|
+
}
|
|
913
|
+
const position = {
|
|
914
|
+
value: event.sequence,
|
|
915
|
+
offset: state.observed?.value === event.sequence ? state.observed.offset + 1 : 0
|
|
916
|
+
};
|
|
917
|
+
const nextState = { ...state, observed: position };
|
|
918
|
+
if ((replayHealth || seedEvents.length > 0 || from.value > 0 || from.offset > 0) &&
|
|
919
|
+
comparePosition(position, from) <= 0) {
|
|
920
|
+
return replayHealth
|
|
921
|
+
? Effect.map(appendEvent(state.buffer, event, `${message}: cursor history is invalid`, source.run), (buffer) => [{ ...nextState, buffer }, []])
|
|
922
|
+
: Effect.succeed([nextState, []]);
|
|
923
|
+
}
|
|
924
|
+
const turnsBefore = state.turns;
|
|
925
|
+
// A resumed follower must also suppress duplicates of calls
|
|
926
|
+
// reported before its cursor, without re-folding that prefix.
|
|
927
|
+
const callKey = selector._tag === "transcript" ? callEventKey(event) : undefined;
|
|
928
|
+
const duplicateCall = callKey !== undefined && state.reportedCalls.has(callKey);
|
|
929
|
+
const authoritative = (nativeCallEvent(event) ?? nativeStepEvent(event)) !== undefined;
|
|
930
|
+
const replaceTranscript = duplicateCall && authoritative && state.reportedCalls.get(callKey) === false;
|
|
931
|
+
if (callKey !== undefined) {
|
|
932
|
+
state.reportedCalls.set(callKey, authoritative || state.reportedCalls.get(callKey) === true);
|
|
933
|
+
}
|
|
934
|
+
const turns = selector._tag === "transcript"
|
|
935
|
+
? transcriptAppend(event, turnsBefore).at(-1)?.turn ?? turnsBefore
|
|
936
|
+
: 0;
|
|
937
|
+
return Effect.flatMap(selector._tag === "run-summary" || selector._tag === "run-tree" || selector._tag === "approvals"
|
|
938
|
+
? runOf(runId)
|
|
939
|
+
: Effect.succeed(source.run), (run) => Effect.map(appendEvent(state.buffer, event, `${message}: event history is invalid`, run), (buffer) => [
|
|
940
|
+
{
|
|
941
|
+
buffer,
|
|
942
|
+
observed: position,
|
|
943
|
+
turns,
|
|
944
|
+
reportedCalls: state.reportedCalls
|
|
945
|
+
},
|
|
946
|
+
[
|
|
947
|
+
{
|
|
948
|
+
run,
|
|
949
|
+
event,
|
|
950
|
+
events: buffer.events,
|
|
951
|
+
carry: buffer.carry,
|
|
952
|
+
position,
|
|
953
|
+
turnsBefore,
|
|
954
|
+
duplicateCall,
|
|
955
|
+
replaceTranscript
|
|
956
|
+
}
|
|
957
|
+
]
|
|
958
|
+
]));
|
|
959
|
+
})), Stream.mapEffect(({ carry, duplicateCall, event, events, position, replaceTranscript, run, turnsBefore }) => replaceTranscript
|
|
960
|
+
? Effect.flatMap(boundedRows(selector, GatewayProjection.transcript(events)), (rows) => Effect.forEach([
|
|
961
|
+
{ _tag: "snapshot-start", selector, cursor: cursorOf(selector, position) },
|
|
962
|
+
...rows.map((row) => ({ _tag: "row", selector, cursor: cursorOf(selector, position), row })),
|
|
963
|
+
{ _tag: "snapshot-end", selector, cursor: cursorOf(selector, position) }
|
|
964
|
+
], frameOf))
|
|
965
|
+
: Effect.flatMap(deltaRows(selector, run, event, events, turnsBefore, duplicateCall, carry), (rows) => Effect.flatMap(boundedRows(selector, rows), (delta) => Effect.map(frameOf({
|
|
966
|
+
_tag: "delta",
|
|
967
|
+
selector,
|
|
968
|
+
cursor: cursorOf(selector, position),
|
|
969
|
+
delta
|
|
970
|
+
}), (frame) => [frame])))), Stream.flattenIterable)));
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* Workspace deltas from the unscoped control follow.
|
|
974
|
+
*
|
|
975
|
+
* Control sequences belong to run partitions, so this stream cannot start
|
|
976
|
+
* after one workspace cursor. It follows without a cursor instead. That
|
|
977
|
+
* replays partition history, and the sources seeded from the snapshot drop
|
|
978
|
+
* events through each run's last folded position. Excluded runs retain only
|
|
979
|
+
* a verdict and replay positions, sharing the source ceiling. Their replay
|
|
980
|
+
* costs no further journal reads until the verdict is evicted.
|
|
981
|
+
*
|
|
982
|
+
* Events are gathered for `workspaceDeltaCoalesceMillis` and admitted as one
|
|
983
|
+
* batch. A batch re-reads each changed run's row once, refolds only those
|
|
984
|
+
* runs' cached rows, and answers one frame, so a burst across many runs
|
|
985
|
+
* costs one frame and one row read per run that changed.
|
|
986
|
+
*/
|
|
987
|
+
const workspaceDeltaFrames = (selector, runs) => Stream.unwrap(Effect.sync(() => {
|
|
988
|
+
const rowsOfSource = (source) => selector._tag === "approvals" ? pendingApprovals(source) : rowsOfRun(selector, source, now());
|
|
989
|
+
const sources = new Map(runs.map((source) => [
|
|
990
|
+
source.run.runId,
|
|
991
|
+
{ source, observed: undefined, rows: rowsOfSource(source) }
|
|
992
|
+
]));
|
|
993
|
+
// A verdict retains no journal. Undefined lastPosition means run_not_found.
|
|
994
|
+
// Sources and verdicts share one ceiling; only verdicts may be evicted.
|
|
995
|
+
const judged = new Map();
|
|
996
|
+
const makeRoom = () => {
|
|
997
|
+
if (sources.size + judged.size >= maxWorkspaceRuns) {
|
|
998
|
+
// Callers have room for a source, so a full cache has a verdict.
|
|
999
|
+
judged.delete(judged.keys().next().value);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const remember = (runId, lastPosition, observed) => {
|
|
1003
|
+
judged.delete(runId);
|
|
1004
|
+
makeRoom();
|
|
1005
|
+
judged.set(runId, { lastPosition, observed });
|
|
1006
|
+
};
|
|
1007
|
+
/** The followed run created first, the one a newer run displaces at the ceiling. */
|
|
1008
|
+
const oldestFollowed = () => {
|
|
1009
|
+
let oldest;
|
|
1010
|
+
for (const followed of sources.values()) {
|
|
1011
|
+
if (oldest === undefined || createdAfter(oldest.source.run, followed.source.run))
|
|
1012
|
+
oldest = followed;
|
|
1013
|
+
}
|
|
1014
|
+
return oldest;
|
|
1015
|
+
};
|
|
1016
|
+
const noFrames = [];
|
|
1017
|
+
const delta = () => Effect.flatMap(boundedRows(selector, selector._tag === "approvals"
|
|
1018
|
+
// Each run's pending rows are cached apart; the inbox is folded
|
|
1019
|
+
// across all of them, exactly as a snapshot folds it.
|
|
1020
|
+
? approvalInbox([...sources.values()].map(({ rows, source }) => ({ run: source.run, rows })))
|
|
1021
|
+
: [...sources.values()].flatMap(({ rows }) => rows)), (rows) => Effect.map(frameOf({
|
|
1022
|
+
_tag: "delta",
|
|
1023
|
+
selector,
|
|
1024
|
+
cursor: cursorOf(selector, { value: 0, offset: 0 }),
|
|
1025
|
+
delta: rows
|
|
1026
|
+
}), (frame) => [frame]));
|
|
1027
|
+
const message = "Following the workspace failed";
|
|
1028
|
+
const admit = (event, refreshed) => Effect.gen(function* () {
|
|
1029
|
+
const runId = event.runId;
|
|
1030
|
+
if (runId === undefined)
|
|
1031
|
+
return "unchanged";
|
|
1032
|
+
const current = sources.get(runId);
|
|
1033
|
+
const verdict = judged.get(runId);
|
|
1034
|
+
const previous = current?.observed ?? verdict?.observed;
|
|
1035
|
+
if (previous !== undefined && event.sequence < previous.value) {
|
|
1036
|
+
return yield* Effect.fail(unavailable(`${message}: event sequences moved backward`, undefined));
|
|
1037
|
+
}
|
|
1038
|
+
const position = {
|
|
1039
|
+
value: event.sequence,
|
|
1040
|
+
offset: previous?.value === event.sequence ? previous.offset + 1 : 0
|
|
1041
|
+
};
|
|
1042
|
+
if (current !== undefined) {
|
|
1043
|
+
current.observed = position;
|
|
1044
|
+
if (current.source.events.length > 0 && comparePosition(position, current.source.lastPosition) <= 0)
|
|
1045
|
+
return "unchanged";
|
|
1046
|
+
if (!refreshed.has(runId)) {
|
|
1047
|
+
current.source = { ...current.source, run: yield* runOf(runId) };
|
|
1048
|
+
refreshed.add(runId);
|
|
1049
|
+
}
|
|
1050
|
+
const buffer = yield* appendEvent(current.source, event, `${message}: event history is invalid`, current.source.run);
|
|
1051
|
+
current.source = { run: current.source.run, ...buffer };
|
|
1052
|
+
return "stale";
|
|
1053
|
+
}
|
|
1054
|
+
if (verdict !== undefined) {
|
|
1055
|
+
verdict.observed = position;
|
|
1056
|
+
if (verdict.lastPosition === undefined || comparePosition(position, verdict.lastPosition) <= 0) {
|
|
1057
|
+
return "unchanged";
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// At the ceiling a run newer than the oldest followed run displaces
|
|
1061
|
+
// it, so the workspace keeps the newest runs as the snapshot does.
|
|
1062
|
+
const displaced = sources.size >= maxWorkspaceRuns ? oldestFollowed() : undefined;
|
|
1063
|
+
if (displaced !== undefined) {
|
|
1064
|
+
const newer = yield* runOf(runId).pipe(Effect.map((run) => createdAfter(run, displaced.source.run)), Effect.catchIf((failure) => failure.code === "run_not_found", () => Effect.succeed(false)));
|
|
1065
|
+
if (!newer)
|
|
1066
|
+
return "unchanged";
|
|
1067
|
+
}
|
|
1068
|
+
return yield* runSourceOf(runId).pipe(Effect.flatMap((source) => {
|
|
1069
|
+
const admitted = source.events.length > 0 && comparePosition(position, source.lastPosition) <= 0
|
|
1070
|
+
? Effect.succeed(source)
|
|
1071
|
+
: Effect.map(appendEvent(source, event, `${message}: event history is invalid`, source.run), (buffer) => ({ run: source.run, ...buffer }));
|
|
1072
|
+
return Effect.map(admitted, (complete) => {
|
|
1073
|
+
if (!eligibleForWorkspace(selector, complete)) {
|
|
1074
|
+
remember(runId, complete.lastPosition, position);
|
|
1075
|
+
return "unchanged";
|
|
1076
|
+
}
|
|
1077
|
+
judged.delete(runId);
|
|
1078
|
+
if (displaced !== undefined && sources.size >= maxWorkspaceRuns) {
|
|
1079
|
+
sources.delete(displaced.source.run.runId);
|
|
1080
|
+
}
|
|
1081
|
+
makeRoom();
|
|
1082
|
+
sources.set(runId, { source: complete, observed: position, rows: rowsOfSource(complete) });
|
|
1083
|
+
return "fresh";
|
|
1084
|
+
});
|
|
1085
|
+
}), Effect.catchIf((failure) => failure.code === "run_not_found", () => {
|
|
1086
|
+
remember(runId, undefined, position);
|
|
1087
|
+
return Effect.succeed("unchanged");
|
|
1088
|
+
}));
|
|
1089
|
+
});
|
|
1090
|
+
/** Refold one grown run using the row refreshed before its batch's compaction, or exclude it. */
|
|
1091
|
+
const refresh = (runId) => Effect.gen(function* () {
|
|
1092
|
+
const current = sources.get(runId);
|
|
1093
|
+
/* v8 ignore next -- only runs still followed after the batch are refreshed. */
|
|
1094
|
+
if (current === undefined || current.observed === undefined)
|
|
1095
|
+
return;
|
|
1096
|
+
const source = current.source;
|
|
1097
|
+
if (eligibleForWorkspace(selector, source)) {
|
|
1098
|
+
current.rows = rowsOfSource(source);
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
sources.delete(runId);
|
|
1102
|
+
remember(runId, source.lastPosition, current.observed);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
return control.watch({ follow: true }).pipe(Stream.tapError((cause) => logReadFailure("follow-workspace", cause)), Stream.mapError((cause) => unavailable(message, cause)), Stream.groupedWithin(workspaceDeltaBatchSize, workspaceDeltaCoalesceMillis), Stream.mapEffect((batch) => Effect.gen(function* () {
|
|
1106
|
+
const stale = new Set();
|
|
1107
|
+
const refreshed = new Set();
|
|
1108
|
+
let changed = false;
|
|
1109
|
+
for (const candidate of batch) {
|
|
1110
|
+
const event = yield* decodedEvent(candidate, `${message}: the control plane returned an invalid event`);
|
|
1111
|
+
const admission = yield* admit(event, refreshed);
|
|
1112
|
+
if (admission === "unchanged")
|
|
1113
|
+
continue;
|
|
1114
|
+
changed = true;
|
|
1115
|
+
if (admission === "stale" && event.runId !== undefined)
|
|
1116
|
+
stale.add(event.runId);
|
|
1117
|
+
}
|
|
1118
|
+
yield* Effect.forEach(stale, refresh, { discard: true });
|
|
1119
|
+
return changed ? yield* delta() : noFrames;
|
|
1120
|
+
})), Stream.flattenIterable);
|
|
1121
|
+
}));
|
|
1122
|
+
/**
|
|
1123
|
+
* Deltas for a selector.
|
|
1124
|
+
*
|
|
1125
|
+
* A run selector follows after its sequence cursor. A workspace selector has
|
|
1126
|
+
* no resume cursor because control journal sequences belong to per-run
|
|
1127
|
+
* partitions, but it can follow every partition without one.
|
|
1128
|
+
*/
|
|
1129
|
+
const deltaFrames = (selector, source, from) => {
|
|
1130
|
+
// A duration row is ranked over the runs a flow has ALREADY finished, so
|
|
1131
|
+
// no event of a live run changes one, and the run that would change one
|
|
1132
|
+
// is not in the window until it settles. The honest delta is another
|
|
1133
|
+
// snapshot, which is what a client takes.
|
|
1134
|
+
if (selector._tag === "flow-durations")
|
|
1135
|
+
return Stream.empty;
|
|
1136
|
+
return source._tag === "workspace"
|
|
1137
|
+
? workspaceDeltaFrames(selector, source.runs)
|
|
1138
|
+
: runDeltaFrames(selector, source.run, from);
|
|
1139
|
+
};
|
|
1140
|
+
/**
|
|
1141
|
+
* The keepalive channel. `Stream.tick` emits immediately and then on the
|
|
1142
|
+
* interval, and an immediate keepalive would arrive before the snapshot it
|
|
1143
|
+
* is supposed to keep alive; dropping the first tick makes the cadence what
|
|
1144
|
+
* it says it is.
|
|
1145
|
+
*/
|
|
1146
|
+
const heartbeats = Stream.tick(heartbeatMillis).pipe(Stream.drop(1), Stream.mapEffect(() => Effect.map(Effect.clockWith((clock) => clock.currentTimeMillis), (atMs) => ({
|
|
1147
|
+
_tag: "heartbeat",
|
|
1148
|
+
atMs
|
|
1149
|
+
}))));
|
|
1150
|
+
/** The run a cursor resumes, or the refusal explaining why it resumes none. */
|
|
1151
|
+
const resumeScope = (selector, after) => {
|
|
1152
|
+
const candidate = after;
|
|
1153
|
+
if (!Number.isSafeInteger(candidate.value) || candidate.value < 0) {
|
|
1154
|
+
return malformed(`Cursor value ${String(candidate.value)} must be a non-negative safe integer`);
|
|
1155
|
+
}
|
|
1156
|
+
if (!Number.isSafeInteger(candidate.offset) || candidate.offset < 0) {
|
|
1157
|
+
return malformed(`Cursor offset ${String(candidate.offset)} must be a non-negative safe integer`);
|
|
1158
|
+
}
|
|
1159
|
+
if (!Schema.is(GatewaySchema.ProjectionCursor)(after)) {
|
|
1160
|
+
return malformed("The projection cursor is not a valid gateway cursor");
|
|
1161
|
+
}
|
|
1162
|
+
const runId = scopeOf(selector);
|
|
1163
|
+
if (after.projection !== selector._tag) {
|
|
1164
|
+
return malformed(`A cursor for the ${after.projection} projection cannot resume a ${selector._tag} subscription`);
|
|
1165
|
+
}
|
|
1166
|
+
if (!sameSelector(after.selector, selector)) {
|
|
1167
|
+
return malformed("A cursor can resume only the exact selector that issued it");
|
|
1168
|
+
}
|
|
1169
|
+
if (runId === undefined) {
|
|
1170
|
+
return malformed(`A ${selector._tag} subscription has no resumable cursor, because control journal sequences belong to per-run partitions`);
|
|
1171
|
+
}
|
|
1172
|
+
if (after.runId !== runId) {
|
|
1173
|
+
return malformed(`A cursor for run ${after.runId ?? "none"} cannot resume a subscription to run ${runId}`);
|
|
1174
|
+
}
|
|
1175
|
+
return runId;
|
|
1176
|
+
};
|
|
1177
|
+
/** Snapshot and follow share the reconciled event buffer. */
|
|
1178
|
+
const fromSnapshot = (selector) => Effect.flatMap(sourceOf(selector), (source) => Effect.flatMap(boundedRows(selector, rowsOf(selector, source, now())), (rows) => Effect.map(snapshotFrames(selector, source, rows), (frames) => Stream.concat(Stream.fromIterable(frames), deltaFrames(selector, source, cursorPositionOf(source))))));
|
|
1179
|
+
/**
|
|
1180
|
+
* The deltas after a cursor the client already holds, with no snapshot.
|
|
1181
|
+
*
|
|
1182
|
+
* The read still happens: a folded projection cannot be recomputed from the
|
|
1183
|
+
* events after the cursor alone. What the client skips is receiving rows it
|
|
1184
|
+
* already has.
|
|
1185
|
+
*/
|
|
1186
|
+
const fromCursor = (selector, after) => {
|
|
1187
|
+
const scope = resumeScope(selector, after);
|
|
1188
|
+
return typeof scope === "string"
|
|
1189
|
+
? Effect.flatMap(runSourceOf(scope, selector._tag !== "run-events"), (source) => {
|
|
1190
|
+
const position = { value: after.value, offset: after.offset };
|
|
1191
|
+
const last = source.lastPosition;
|
|
1192
|
+
if (comparePosition(position, last) > 0) {
|
|
1193
|
+
return Effect.fail(malformed(`Cursor ${after.value}:${after.offset} cannot resume run ${scope} past its last position ${last.value}:${last.offset}`));
|
|
1194
|
+
}
|
|
1195
|
+
const retained = position.value === 0 && position.offset === 0 ||
|
|
1196
|
+
positionedEvents(source.events).some((candidate) => comparePosition(candidate.position, position) === 0);
|
|
1197
|
+
// A valid issued health cursor may have been superseded in the bounded fold.
|
|
1198
|
+
// Verify it against the durable stream rather than accepting arbitrary positions.
|
|
1199
|
+
const issued = retained ? Effect.succeed(true) : source.compactHealth ?
|
|
1200
|
+
control.watch({
|
|
1201
|
+
runId: scope,
|
|
1202
|
+
follow: false,
|
|
1203
|
+
...(position.value === 0 ? {} : { afterSequence: position.value - 1 })
|
|
1204
|
+
}).pipe(Stream.takeWhile((event) => event.sequence <= position.value), Stream.take(maxEventsPerRun + 1), Stream.runCollect, Effect.map((events) => events.length <= maxEventsPerRun && events[position.offset]?.sequence === position.value), Effect.mapError((cause) => unavailable("Checking the projection cursor failed", cause))) :
|
|
1205
|
+
Effect.succeed(false);
|
|
1206
|
+
return Effect.flatMap(issued, (valid) => valid
|
|
1207
|
+
? Effect.succeed(runDeltaFrames(selector, source, position))
|
|
1208
|
+
: Effect.fail(malformed(`Cursor ${after.value}:${after.offset} was not issued by run ${scope}`)));
|
|
1209
|
+
})
|
|
1210
|
+
: Effect.fail(scope);
|
|
1211
|
+
};
|
|
1212
|
+
const subscribe = (selector, after) => Stream.merge(Stream.unwrap(after === undefined ? fromSnapshot(selector) : fromCursor(selector, after)), heartbeats, { haltStrategy: "left" });
|
|
1213
|
+
/*
|
|
1214
|
+
* The service is generic in its selector; the implementation folds every
|
|
1215
|
+
* selector through one code path and is not. The correlation the signature
|
|
1216
|
+
* promises is enforced at runtime rather than assumed: `boundedRows`
|
|
1217
|
+
* decodes each fold against `GatewaySchema.rowSchemaFor(selector)` and
|
|
1218
|
+
* fails the read when a row does not belong to the selector that asked.
|
|
1219
|
+
*/
|
|
1220
|
+
return { snapshot, subscribe };
|
|
1221
|
+
};
|
|
1222
|
+
/**
|
|
1223
|
+
* Builds the read path over a control plane.
|
|
1224
|
+
*
|
|
1225
|
+
* Invalid keepalive settings are returned as typed `bind_failed` failures;
|
|
1226
|
+
* construction never throws synchronously.
|
|
1227
|
+
*
|
|
1228
|
+
* @param control the control service to read through
|
|
1229
|
+
* @param options the keepalive cadence to use
|
|
1230
|
+
* @since 1.0.0
|
|
1231
|
+
* @category constructors
|
|
1232
|
+
*/
|
|
1233
|
+
export const make = (control, options = {}) => Effect.suspend(() => {
|
|
1234
|
+
const refusal = settingRefusal("The gateway keepalive cadence", options.heartbeatMillis);
|
|
1235
|
+
return refusal === undefined
|
|
1236
|
+
? Effect.clockWith((clock) => Effect.succeed(makeService(control, options.heartbeatMillis ?? heartbeatIntervalMillis, () => clock.currentTimeMillisUnsafe())))
|
|
1237
|
+
: Effect.fail(refusal);
|
|
1238
|
+
});
|
|
1239
|
+
/**
|
|
1240
|
+
* Provides the gateway read path over the ambient control plane.
|
|
1241
|
+
*
|
|
1242
|
+
* @since 1.0.0
|
|
1243
|
+
* @category layers
|
|
1244
|
+
*/
|
|
1245
|
+
export const layer = Layer.effect(Projections)(Effect.flatMap(Control, (control) => make(control)));
|
|
1246
|
+
/**
|
|
1247
|
+
* Provides the gateway read path under an explicit keepalive cadence.
|
|
1248
|
+
*
|
|
1249
|
+
* @param options the keepalive cadence to use
|
|
1250
|
+
* @since 1.0.0
|
|
1251
|
+
* @category layers
|
|
1252
|
+
*/
|
|
1253
|
+
export const layerWith = (options) => Layer.effect(Projections)(Effect.flatMap(Control, (control) => make(control, options)));
|
|
1254
|
+
//# sourceMappingURL=Projections.js.map
|