@mysten-incubation/devstack 0.0.1 → 0.1.1
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/README.md +8 -4
- package/dashboard-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard-ui/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
- package/dashboard-ui/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
- package/dashboard-ui/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
- package/dashboard-ui/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
- package/dashboard-ui/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
- package/dashboard-ui/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
- package/dashboard-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard-ui/assets/index-Bmi1UtAg.js +1356 -0
- package/dashboard-ui/assets/index-D5EShVt4.js +3 -0
- package/dashboard-ui/assets/index-Deml9drg.css +1 -0
- package/dashboard-ui/index.html +13 -0
- package/dist/api/define-capabilities.d.mts +14 -3
- package/dist/api/define-capabilities.mjs +18 -1
- package/dist/api/define-capabilities.mjs.map +1 -1
- package/dist/api/define-devstack.d.mts +1 -1
- package/dist/api/define-devstack.mjs +6 -26
- package/dist/api/define-devstack.mjs.map +1 -1
- package/dist/api/define-plugin.d.mts +1 -0
- package/dist/api/define-plugin.mjs +2 -0
- package/dist/api/inference-network.d.mts +132 -0
- package/dist/api/inference-network.mjs +105 -16
- package/dist/api/inference-network.mjs.map +1 -1
- package/dist/api/plugin-errors.d.mts +7 -0
- package/dist/api/run-stack.d.mts +20 -9
- package/dist/api/run-stack.mjs +56 -29
- package/dist/api/run-stack.mjs.map +1 -1
- package/dist/build-integrations/playwright/config.d.mts +6 -4
- package/dist/build-integrations/playwright/config.mjs +5 -3
- package/dist/build-integrations/playwright/config.mjs.map +1 -1
- package/dist/build-integrations/playwright/errors.d.mts +1 -1
- package/dist/build-integrations/playwright/global-setup.d.mts +43 -17
- package/dist/build-integrations/playwright/global-setup.mjs +250 -13
- package/dist/build-integrations/playwright/global-setup.mjs.map +1 -1
- package/dist/build-integrations/playwright/index.d.mts +5 -3
- package/dist/build-integrations/playwright/index.mjs +4 -2
- package/dist/build-integrations/playwright/stack-context.d.mts +0 -1
- package/dist/build-integrations/playwright/stack-context.mjs +24 -63
- package/dist/build-integrations/playwright/stack-context.mjs.map +1 -1
- package/dist/build-integrations/playwright/wallet-context.d.mts +7 -10
- package/dist/build-integrations/playwright/wallet-context.mjs +8 -13
- package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/cold-start-url.d.mts +5 -0
- package/dist/build-integrations/runtime/cold-start-url.mjs +4 -3
- package/dist/build-integrations/runtime/cold-start-url.mjs.map +1 -1
- package/dist/build-integrations/runtime/conventional-routes.d.mts +51 -0
- package/dist/build-integrations/runtime/conventional-routes.mjs +92 -0
- package/dist/build-integrations/runtime/conventional-routes.mjs.map +1 -0
- package/dist/build-integrations/runtime/discover.d.mts +38 -30
- package/dist/build-integrations/runtime/discover.mjs +73 -70
- package/dist/build-integrations/runtime/discover.mjs.map +1 -1
- package/dist/build-integrations/runtime/endpoint-registry.d.mts +3 -1
- package/dist/build-integrations/runtime/endpoint-registry.mjs +3 -1
- package/dist/build-integrations/runtime/endpoint-registry.mjs.map +1 -1
- package/dist/build-integrations/runtime/index.d.mts +7 -2
- package/dist/build-integrations/runtime/index.mjs +9 -2
- package/dist/build-integrations/runtime/manifest-types.d.mts +2 -0
- package/dist/build-integrations/runtime/manifest-types.mjs +3 -0
- package/dist/build-integrations/runtime/playwright-stack-context-slot.d.mts +29 -0
- package/dist/build-integrations/runtime/playwright-stack-context-slot.mjs +10 -0
- package/dist/build-integrations/runtime/playwright-stack-context-slot.mjs.map +1 -0
- package/dist/build-integrations/runtime/read-stack-context.d.mts +1 -1
- package/dist/build-integrations/runtime/read-stack-context.mjs +10 -4
- package/dist/build-integrations/runtime/read-stack-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/resolve-discovery-env.d.mts +48 -0
- package/dist/build-integrations/runtime/resolve-discovery-env.mjs +42 -0
- package/dist/build-integrations/runtime/resolve-discovery-env.mjs.map +1 -0
- package/dist/build-integrations/runtime/stack-context.d.mts +1 -1
- package/dist/build-integrations/runtime/wallet-paths.mjs +2 -0
- package/dist/build-integrations/vite/index.d.mts +51 -0
- package/dist/build-integrations/vite/index.mjs +73 -0
- package/dist/build-integrations/vite/index.mjs.map +1 -0
- package/dist/build-integrations/vitest/env.d.mts +4 -3
- package/dist/build-integrations/vitest/env.mjs +6 -4
- package/dist/build-integrations/vitest/env.mjs.map +1 -1
- package/dist/build-integrations/vitest/errors.d.mts +25 -4
- package/dist/build-integrations/vitest/errors.mjs +15 -2
- package/dist/build-integrations/vitest/errors.mjs.map +1 -1
- package/dist/build-integrations/vitest/index.d.mts +2 -2
- package/dist/build-integrations/vitest/index.mjs +3 -3
- package/dist/build-integrations/vitest/setup.d.mts +4 -2
- package/dist/build-integrations/vitest/setup.mjs +2 -2
- package/dist/build-integrations/vitest/setup.mjs.map +1 -1
- package/dist/build-integrations/vitest/stack-context.d.mts +0 -1
- package/dist/build-integrations/vitest/stack-context.mjs +33 -13
- package/dist/build-integrations/vitest/stack-context.mjs.map +1 -1
- package/dist/cli/bail.mjs +23 -0
- package/dist/cli/bail.mjs.map +1 -0
- package/dist/{surfaces/cli/commands → cli}/doctor-probes.mjs +26 -16
- package/dist/cli/doctor-probes.mjs.map +1 -0
- package/dist/cli/main.d.mts +15 -1
- package/dist/cli/main.mjs +166 -482
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/prune-direct.mjs +31 -201
- package/dist/cli/prune-direct.mjs.map +1 -1
- package/dist/cli/snapshot-reader.mjs +14 -11
- package/dist/cli/snapshot-reader.mjs.map +1 -1
- package/dist/cli/up-lifecycle.mjs +4 -1
- package/dist/cli/up-lifecycle.mjs.map +1 -1
- package/dist/cli/wirings/apply.mjs +95 -0
- package/dist/cli/wirings/apply.mjs.map +1 -0
- package/dist/cli/wirings/build-verb-layers.mjs +42 -0
- package/dist/cli/wirings/build-verb-layers.mjs.map +1 -0
- package/dist/cli/wirings/config-loader.mjs +69 -0
- package/dist/cli/wirings/config-loader.mjs.map +1 -0
- package/dist/cli/wirings/engine-command.mjs +30 -0
- package/dist/cli/wirings/engine-command.mjs.map +1 -0
- package/dist/cli/wirings/identity.mjs +72 -0
- package/dist/cli/wirings/identity.mjs.map +1 -0
- package/dist/cli/wirings/provide-file-system.mjs +7 -0
- package/dist/cli/wirings/provide-file-system.mjs.map +1 -0
- package/dist/cli/wirings/snapshot.mjs +177 -0
- package/dist/cli/wirings/snapshot.mjs.map +1 -0
- package/dist/cli/wirings/up.mjs +287 -0
- package/dist/cli/wirings/up.mjs.map +1 -0
- package/dist/cli/wirings/wipe.mjs +40 -0
- package/dist/cli/wirings/wipe.mjs.map +1 -0
- package/dist/contracts/capability-decl.d.mts +0 -2
- package/dist/contracts/codegenable.d.mts +43 -0
- package/dist/contracts/container-runtime.d.mts +26 -1
- package/dist/contracts/faucet-strategy.d.mts +25 -0
- package/dist/contracts/funding-strategy.d.mts +42 -0
- package/dist/contracts/plugin-expander.mjs +60 -0
- package/dist/contracts/plugin-expander.mjs.map +1 -0
- package/dist/contracts/projection.d.mts +5 -3
- package/dist/contracts/renderer.d.mts +23 -0
- package/dist/contracts/routable.d.mts +14 -1
- package/dist/contracts/snapshotable.d.mts +25 -3
- package/dist/contracts/wallet-protocol.d.mts +52 -0
- package/dist/contracts/wallet-protocol.mjs +52 -0
- package/dist/contracts/wallet-protocol.mjs.map +1 -0
- package/dist/index.d.mts +26 -25
- package/dist/index.mjs +20 -13
- package/dist/{runtime → orchestrators}/built-in-plugin-layers.mjs +10 -5
- package/dist/orchestrators/built-in-plugin-layers.mjs.map +1 -0
- package/dist/orchestrators/codegen/bindings.d.mts +1 -2
- package/dist/orchestrators/codegen/bindings.mjs +52 -186
- package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
- package/dist/orchestrators/codegen/emit.mjs +19 -1
- package/dist/orchestrators/codegen/emit.mjs.map +1 -1
- package/dist/orchestrators/codegen/errors.mjs +8 -3
- package/dist/orchestrators/codegen/errors.mjs.map +1 -1
- package/dist/orchestrators/codegen/format.mjs +47 -39
- package/dist/orchestrators/codegen/format.mjs.map +1 -1
- package/dist/orchestrators/codegen/gitignore.mjs +3 -1
- package/dist/orchestrators/codegen/gitignore.mjs.map +1 -1
- package/dist/orchestrators/codegen/output-location.mjs +41 -0
- package/dist/orchestrators/codegen/output-location.mjs.map +1 -0
- package/dist/orchestrators/codegen/paths.d.mts +1 -1
- package/dist/orchestrators/codegen/paths.mjs +40 -14
- package/dist/orchestrators/codegen/paths.mjs.map +1 -1
- package/dist/orchestrators/codegen/permissions.mjs +3 -1
- package/dist/orchestrators/codegen/permissions.mjs.map +1 -1
- package/dist/orchestrators/codegen/service.mjs +157 -105
- package/dist/orchestrators/codegen/service.mjs.map +1 -1
- package/dist/orchestrators/lifecycle-prune/errors.mjs +31 -0
- package/dist/orchestrators/lifecycle-prune/errors.mjs.map +1 -0
- package/dist/orchestrators/lifecycle-prune/index.mjs +350 -0
- package/dist/orchestrators/lifecycle-prune/index.mjs.map +1 -0
- package/dist/orchestrators/router/cleanup.mjs +26 -27
- package/dist/orchestrators/router/cleanup.mjs.map +1 -1
- package/dist/orchestrators/router/entrypoints.mjs +2 -2
- package/dist/orchestrators/router/entrypoints.mjs.map +1 -1
- package/dist/orchestrators/router/file-provider.mjs +14 -5
- package/dist/orchestrators/router/file-provider.mjs.map +1 -1
- package/dist/orchestrators/router/hostname.mjs +19 -55
- package/dist/orchestrators/router/hostname.mjs.map +1 -1
- package/dist/orchestrators/router/index.mjs +11 -0
- package/dist/orchestrators/router/profile.mjs +2 -2
- package/dist/orchestrators/router/sentinels.mjs +26 -0
- package/dist/orchestrators/router/sentinels.mjs.map +1 -0
- package/dist/orchestrators/router/service.mjs +40 -37
- package/dist/orchestrators/router/service.mjs.map +1 -1
- package/dist/orchestrators/router/traefik-container.mjs +27 -16
- package/dist/orchestrators/router/traefik-container.mjs.map +1 -1
- package/dist/{substrate/runtime → orchestrators}/run.mjs +26 -21
- package/dist/orchestrators/run.mjs.map +1 -0
- package/dist/orchestrators/runtime-composition.d.mts +1 -1
- package/dist/orchestrators/runtime-composition.mjs +123 -39
- package/dist/orchestrators/runtime-composition.mjs.map +1 -1
- package/dist/orchestrators/snapshot/capture-command.d.mts +1 -0
- package/dist/orchestrators/snapshot/capture-command.mjs +25 -0
- package/dist/orchestrators/snapshot/capture-command.mjs.map +1 -0
- package/dist/orchestrators/snapshot/capture.mjs +42 -28
- package/dist/orchestrators/snapshot/capture.mjs.map +1 -1
- package/dist/orchestrators/snapshot/descriptor.mjs +35 -10
- package/dist/orchestrators/snapshot/descriptor.mjs.map +1 -1
- package/dist/orchestrators/snapshot/identity-guard.mjs +1 -1
- package/dist/orchestrators/snapshot/index.mjs +13 -0
- package/dist/orchestrators/snapshot/integrity.d.mts +1 -1
- package/dist/orchestrators/snapshot/integrity.mjs +73 -17
- package/dist/orchestrators/snapshot/integrity.mjs.map +1 -1
- package/dist/orchestrators/snapshot/pending-marker.d.mts +1 -0
- package/dist/orchestrators/snapshot/pending-marker.mjs +99 -0
- package/dist/orchestrators/snapshot/pending-marker.mjs.map +1 -0
- package/dist/orchestrators/snapshot/phase-error.mjs +23 -0
- package/dist/orchestrators/snapshot/phase-error.mjs.map +1 -0
- package/dist/orchestrators/snapshot/prune.mjs +20 -55
- package/dist/orchestrators/snapshot/prune.mjs.map +1 -1
- package/dist/orchestrators/snapshot/recover-pending.d.mts +1 -0
- package/dist/orchestrators/snapshot/recover-pending.mjs +223 -0
- package/dist/orchestrators/snapshot/recover-pending.mjs.map +1 -0
- package/dist/orchestrators/snapshot/restore.mjs +118 -84
- package/dist/orchestrators/snapshot/restore.mjs.map +1 -1
- package/dist/orchestrators/snapshot/service.mjs +53 -56
- package/dist/orchestrators/snapshot/service.mjs.map +1 -1
- package/dist/orchestrators/snapshot/state-document.d.mts +1 -0
- package/dist/orchestrators/snapshot/state-document.mjs +47 -7
- package/dist/orchestrators/snapshot/state-document.mjs.map +1 -1
- package/dist/orchestrators/snapshot/wipe.mjs +67 -10
- package/dist/orchestrators/snapshot/wipe.mjs.map +1 -1
- package/dist/plugins/account/codegen.mjs +9 -8
- package/dist/plugins/account/codegen.mjs.map +1 -1
- package/dist/plugins/account/errors.d.mts +33 -6
- package/dist/plugins/account/errors.mjs.map +1 -1
- package/dist/plugins/account/funding.d.mts +29 -19
- package/dist/plugins/account/funding.mjs +84 -66
- package/dist/plugins/account/funding.mjs.map +1 -1
- package/dist/plugins/account/index.d.mts +2 -4
- package/dist/plugins/account/index.mjs +64 -31
- package/dist/plugins/account/index.mjs.map +1 -1
- package/dist/plugins/account/keypair.mjs +6 -9
- package/dist/plugins/account/keypair.mjs.map +1 -1
- package/dist/plugins/account/lease.mjs +1 -0
- package/dist/plugins/account/lease.mjs.map +1 -1
- package/dist/plugins/account/registry.d.mts +14 -1
- package/dist/plugins/account/registry.mjs +18 -18
- package/dist/plugins/account/registry.mjs.map +1 -1
- package/dist/plugins/account/service.d.mts +23 -4
- package/dist/plugins/account/service.mjs +83 -45
- package/dist/plugins/account/service.mjs.map +1 -1
- package/dist/plugins/account/snapshot.mjs +2 -5
- package/dist/plugins/account/snapshot.mjs.map +1 -1
- package/dist/plugins/account/spans.mjs +16 -0
- package/dist/plugins/account/spans.mjs.map +1 -0
- package/dist/plugins/account/variants/keystore.mjs +6 -8
- package/dist/plugins/account/variants/keystore.mjs.map +1 -1
- package/dist/plugins/action/build-context.d.mts +3 -2
- package/dist/plugins/action/errors.d.mts +16 -12
- package/dist/plugins/action/errors.mjs.map +1 -1
- package/dist/plugins/action/execute.mjs +65 -103
- package/dist/plugins/action/execute.mjs.map +1 -1
- package/dist/plugins/action/index.d.mts +0 -2
- package/dist/plugins/action/index.mjs +17 -13
- package/dist/plugins/action/index.mjs.map +1 -1
- package/dist/plugins/action/service.mjs +38 -35
- package/dist/plugins/action/service.mjs.map +1 -1
- package/dist/plugins/action/spans.mjs +11 -0
- package/dist/plugins/action/spans.mjs.map +1 -0
- package/dist/plugins/coin/codegen.mjs +9 -8
- package/dist/plugins/coin/codegen.mjs.map +1 -1
- package/dist/plugins/coin/discovery.mjs +8 -4
- package/dist/plugins/coin/discovery.mjs.map +1 -1
- package/dist/plugins/coin/index.d.mts +11 -29
- package/dist/plugins/coin/index.mjs +56 -27
- package/dist/plugins/coin/index.mjs.map +1 -1
- package/dist/plugins/coin/metadata.mjs +8 -15
- package/dist/plugins/coin/metadata.mjs.map +1 -1
- package/dist/plugins/coin/mint.d.mts +30 -11
- package/dist/plugins/coin/mint.mjs +78 -74
- package/dist/plugins/coin/mint.mjs.map +1 -1
- package/dist/plugins/coin/registry.mjs +4 -7
- package/dist/plugins/coin/registry.mjs.map +1 -1
- package/dist/plugins/coin/service.d.mts +39 -1
- package/dist/plugins/coin/service.mjs +11 -4
- package/dist/plugins/coin/service.mjs.map +1 -1
- package/dist/plugins/coin/spans.mjs +20 -0
- package/dist/plugins/coin/spans.mjs.map +1 -0
- package/dist/plugins/dashboard/domain.mjs +402 -0
- package/dist/plugins/dashboard/domain.mjs.map +1 -0
- package/dist/plugins/dashboard/index.d.mts +29 -0
- package/dist/plugins/dashboard/index.mjs +91 -0
- package/dist/plugins/dashboard/index.mjs.map +1 -0
- package/dist/plugins/dashboard/origin-policy.mjs +36 -0
- package/dist/plugins/dashboard/origin-policy.mjs.map +1 -0
- package/dist/plugins/dashboard/routable.mjs +27 -0
- package/dist/plugins/dashboard/routable.mjs.map +1 -0
- package/dist/plugins/dashboard/schema/builder.mjs +21 -0
- package/dist/plugins/dashboard/schema/builder.mjs.map +1 -0
- package/dist/plugins/dashboard/schema/enums.mjs +72 -0
- package/dist/plugins/dashboard/schema/enums.mjs.map +1 -0
- package/dist/plugins/dashboard/schema/root.mjs +351 -0
- package/dist/plugins/dashboard/schema/root.mjs.map +1 -0
- package/dist/plugins/dashboard/schema/types.mjs +508 -0
- package/dist/plugins/dashboard/schema/types.mjs.map +1 -0
- package/dist/plugins/dashboard/schema.mjs +11 -0
- package/dist/plugins/dashboard/schema.mjs.map +1 -0
- package/dist/plugins/dashboard/server.mjs +166 -0
- package/dist/plugins/dashboard/server.mjs.map +1 -0
- package/dist/plugins/deepbook/deploy.mjs +53 -115
- package/dist/plugins/deepbook/deploy.mjs.map +1 -1
- package/dist/plugins/deepbook/errors.mjs +1 -0
- package/dist/plugins/deepbook/errors.mjs.map +1 -1
- package/dist/plugins/deepbook/faucet-strategy.mjs +47 -34
- package/dist/plugins/deepbook/faucet-strategy.mjs.map +1 -1
- package/dist/plugins/deepbook/hash.mjs +9 -0
- package/dist/plugins/deepbook/hash.mjs.map +1 -0
- package/dist/plugins/deepbook/index.d.mts +1 -10
- package/dist/plugins/deepbook/index.mjs +25 -22
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/deepbook/pyth/index.mjs +26 -55
- package/dist/plugins/deepbook/pyth/index.mjs.map +1 -1
- package/dist/plugins/deepbook/snapshot.mjs +17 -29
- package/dist/plugins/deepbook/snapshot.mjs.map +1 -1
- package/dist/plugins/deepbook/spans.mjs +18 -0
- package/dist/plugins/deepbook/spans.mjs.map +1 -0
- package/dist/plugins/deepbook/type-strings.mjs +65 -0
- package/dist/plugins/deepbook/type-strings.mjs.map +1 -0
- package/dist/plugins/deepbook/types.d.mts +2 -3
- package/dist/plugins/deepbook/types.mjs.map +1 -1
- package/dist/plugins/faucet/dispatcher.d.mts +21 -2
- package/dist/plugins/faucet/dispatcher.mjs +1 -2
- package/dist/plugins/faucet/dispatcher.mjs.map +1 -1
- package/dist/plugins/faucet/errors.d.mts +11 -24
- package/dist/plugins/faucet/errors.mjs.map +1 -1
- package/dist/plugins/faucet/http.mjs +33 -14
- package/dist/plugins/faucet/http.mjs.map +1 -1
- package/dist/plugins/faucet/index.d.mts +15 -5
- package/dist/plugins/faucet/index.mjs +3 -0
- package/dist/plugins/faucet/index.mjs.map +1 -1
- package/dist/plugins/faucet/spans.mjs +12 -0
- package/dist/plugins/faucet/spans.mjs.map +1 -0
- package/dist/plugins/host-service/errors.d.mts +1 -1
- package/dist/plugins/host-service/index.mjs +34 -8
- package/dist/plugins/host-service/index.mjs.map +1 -1
- package/dist/plugins/host-service/routable.mjs +2 -1
- package/dist/plugins/host-service/routable.mjs.map +1 -1
- package/dist/plugins/host-service/service.d.mts +21 -0
- package/dist/plugins/host-service/service.mjs +136 -81
- package/dist/plugins/host-service/service.mjs.map +1 -1
- package/dist/plugins/internal/acquire-on-chain-artifact.mjs +23 -0
- package/dist/plugins/internal/acquire-on-chain-artifact.mjs.map +1 -0
- package/dist/plugins/internal/codegen-helpers.mjs +18 -0
- package/dist/plugins/internal/codegen-helpers.mjs.map +1 -0
- package/dist/plugins/internal/funding-failure-error.mjs +32 -0
- package/dist/plugins/internal/funding-failure-error.mjs.map +1 -0
- package/dist/plugins/package/build.mjs +1 -1
- package/dist/plugins/package/build.mjs.map +1 -1
- package/dist/plugins/package/codegen.mjs +26 -0
- package/dist/plugins/package/codegen.mjs.map +1 -1
- package/dist/plugins/package/errors.d.mts +16 -6
- package/dist/plugins/package/errors.mjs.map +1 -1
- package/dist/plugins/package/index.d.mts +0 -4
- package/dist/plugins/package/index.mjs +27 -21
- package/dist/plugins/package/index.mjs.map +1 -1
- package/dist/plugins/package/mode-known.mjs +26 -12
- package/dist/plugins/package/mode-known.mjs.map +1 -1
- package/dist/plugins/package/mode-local.mjs +47 -41
- package/dist/plugins/package/mode-local.mjs.map +1 -1
- package/dist/plugins/package/publish-executor.mjs +55 -114
- package/dist/plugins/package/publish-executor.mjs.map +1 -1
- package/dist/plugins/package/registry.mjs +2 -1
- package/dist/plugins/package/registry.mjs.map +1 -1
- package/dist/plugins/package/service.mjs.map +1 -1
- package/dist/plugins/package/spans.mjs +14 -0
- package/dist/plugins/package/spans.mjs.map +1 -0
- package/dist/plugins/postgres/db-ensure.mjs +19 -12
- package/dist/plugins/postgres/db-ensure.mjs.map +1 -1
- package/dist/plugins/postgres/errors.d.mts +5 -0
- package/dist/plugins/postgres/errors.mjs.map +1 -1
- package/dist/plugins/postgres/index.mjs +10 -17
- package/dist/plugins/postgres/index.mjs.map +1 -1
- package/dist/plugins/postgres/service.mjs +58 -24
- package/dist/plugins/postgres/service.mjs.map +1 -1
- package/dist/plugins/postgres/snapshot.mjs.map +1 -1
- package/dist/plugins/postgres/spans.mjs +11 -0
- package/dist/plugins/postgres/spans.mjs.map +1 -0
- package/dist/plugins/router-entrypoints.mjs +4 -4
- package/dist/plugins/router-entrypoints.mjs.map +1 -1
- package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs +15 -13
- package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs.map +1 -1
- package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs +15 -19
- package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs.map +1 -1
- package/dist/plugins/seal/codegen.d.mts +2 -3
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/config-render.mjs +35 -8
- package/dist/plugins/seal/config-render.mjs.map +1 -1
- package/dist/plugins/seal/deploy.mjs +31 -39
- package/dist/plugins/seal/deploy.mjs.map +1 -1
- package/dist/plugins/seal/errors.d.mts +1 -3
- package/dist/plugins/seal/errors.mjs +7 -1
- package/dist/plugins/seal/errors.mjs.map +1 -1
- package/dist/plugins/seal/index.d.mts +1 -6
- package/dist/plugins/seal/index.mjs +40 -14
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/seal/key-manager.d.mts +1 -9
- package/dist/plugins/seal/key-manager.mjs +2 -18
- package/dist/plugins/seal/key-manager.mjs.map +1 -1
- package/dist/plugins/seal/key-server.mjs +37 -36
- package/dist/plugins/seal/key-server.mjs.map +1 -1
- package/dist/plugins/seal/keygen.mjs +16 -17
- package/dist/plugins/seal/keygen.mjs.map +1 -1
- package/dist/plugins/seal/mode/fork-known.mjs +36 -14
- package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
- package/dist/plugins/seal/mode/live.d.mts +6 -4
- package/dist/plugins/seal/mode/live.mjs +14 -21
- package/dist/plugins/seal/mode/live.mjs.map +1 -1
- package/dist/plugins/seal/mode/local-keygen.mjs +57 -21
- package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
- package/dist/plugins/seal/registry-publish.mjs +1 -0
- package/dist/plugins/seal/registry-publish.mjs.map +1 -1
- package/dist/plugins/seal/routable.mjs.map +1 -1
- package/dist/plugins/seal/service.mjs.map +1 -1
- package/dist/plugins/seal/spans.mjs +18 -0
- package/dist/plugins/seal/spans.mjs.map +1 -0
- package/dist/plugins/sui/auto-tick.mjs +17 -23
- package/dist/plugins/sui/auto-tick.mjs.map +1 -1
- package/dist/plugins/sui/chain-build-container.mjs +18 -0
- package/dist/plugins/sui/chain-build-container.mjs.map +1 -0
- package/dist/plugins/sui/chain-probe.d.mts +5 -5
- package/dist/plugins/sui/chain-probe.mjs +28 -58
- package/dist/plugins/sui/chain-probe.mjs.map +1 -1
- package/dist/plugins/sui/codegen.mjs +26 -0
- package/dist/plugins/sui/codegen.mjs.map +1 -1
- package/dist/plugins/sui/errors.d.mts +2 -29
- package/dist/plugins/sui/errors.mjs +1 -3
- package/dist/plugins/sui/errors.mjs.map +1 -1
- package/dist/plugins/sui/fork-faucet-strategy.d.mts +1 -0
- package/dist/plugins/sui/fork-faucet-strategy.mjs +55 -0
- package/dist/plugins/sui/fork-faucet-strategy.mjs.map +1 -0
- package/dist/plugins/sui/fork-orchestration.d.mts +1 -0
- package/dist/plugins/sui/fork-orchestration.mjs +170 -2
- package/dist/plugins/sui/fork-orchestration.mjs.map +1 -1
- package/dist/plugins/sui/fork-transaction.d.mts +2 -0
- package/dist/plugins/sui/fork-transaction.mjs +92 -28
- package/dist/plugins/sui/fork-transaction.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +33 -70
- package/dist/plugins/sui/index.mjs +58 -35
- package/dist/plugins/sui/index.mjs.map +1 -1
- package/dist/plugins/{faucet/strategies/sui-local.mjs → sui/local-faucet-strategy.mjs} +10 -7
- package/dist/plugins/sui/local-faucet-strategy.mjs.map +1 -0
- package/dist/plugins/sui/mode/external.mjs +7 -13
- package/dist/plugins/sui/mode/external.mjs.map +1 -1
- package/dist/plugins/sui/mode/fork.mjs +136 -59
- package/dist/plugins/sui/mode/fork.mjs.map +1 -1
- package/dist/plugins/sui/mode/live.mjs +10 -16
- package/dist/plugins/sui/mode/live.mjs.map +1 -1
- package/dist/plugins/sui/mode/local.mjs +37 -36
- package/dist/plugins/sui/mode/local.mjs.map +1 -1
- package/dist/plugins/sui/mode/shared-boot.mjs +38 -115
- package/dist/plugins/sui/mode/shared-boot.mjs.map +1 -1
- package/dist/plugins/sui/mode/shared.d.mts +1 -5
- package/dist/plugins/sui/mode/shared.mjs.map +1 -1
- package/dist/plugins/sui/mode/spec.d.mts +18 -0
- package/dist/plugins/sui/move-summary-runner.mjs +207 -0
- package/dist/plugins/sui/move-summary-runner.mjs.map +1 -0
- package/dist/plugins/sui/network-resolver.d.mts +4 -3
- package/dist/plugins/sui/service.mjs.map +1 -1
- package/dist/plugins/sui/snapshot.mjs +3 -0
- package/dist/plugins/sui/snapshot.mjs.map +1 -1
- package/dist/plugins/sui/spans.mjs +17 -0
- package/dist/plugins/sui/spans.mjs.map +1 -0
- package/dist/plugins/wallet/codegen.d.mts +0 -1
- package/dist/plugins/wallet/codegen.mjs +8 -9
- package/dist/plugins/wallet/codegen.mjs.map +1 -1
- package/dist/plugins/wallet/index.d.mts +6 -8
- package/dist/plugins/wallet/index.mjs +46 -35
- package/dist/plugins/wallet/index.mjs.map +1 -1
- package/dist/plugins/wallet/origin-policy.mjs +12 -28
- package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
- package/dist/plugins/wallet/pairing.mjs +6 -4
- package/dist/plugins/wallet/pairing.mjs.map +1 -1
- package/dist/plugins/wallet/protocol.d.mts +1 -0
- package/dist/plugins/wallet/protocol.mjs +5 -44
- package/dist/plugins/wallet/protocol.mjs.map +1 -1
- package/dist/plugins/wallet/routable.mjs +2 -8
- package/dist/plugins/wallet/routable.mjs.map +1 -1
- package/dist/plugins/wallet/server.mjs +36 -44
- package/dist/plugins/wallet/server.mjs.map +1 -1
- package/dist/plugins/wallet/service.d.mts +1 -5
- package/dist/plugins/wallet/service.mjs +22 -18
- package/dist/plugins/wallet/service.mjs.map +1 -1
- package/dist/plugins/wallet/spans.mjs +22 -0
- package/dist/plugins/wallet/spans.mjs.map +1 -0
- package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs +33 -8
- package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs.map +1 -1
- package/dist/plugins/walrus/codegen.d.mts +3 -3
- package/dist/plugins/walrus/codegen.mjs.map +1 -1
- package/dist/plugins/walrus/deploy-paths.mjs +21 -5
- package/dist/plugins/walrus/deploy-paths.mjs.map +1 -1
- package/dist/plugins/walrus/deploy.mjs +67 -69
- package/dist/plugins/walrus/deploy.mjs.map +1 -1
- package/dist/plugins/walrus/errors.mjs +1 -0
- package/dist/plugins/walrus/errors.mjs.map +1 -1
- package/dist/plugins/walrus/faucet-strategy.mjs +12 -7
- package/dist/plugins/walrus/faucet-strategy.mjs.map +1 -1
- package/dist/plugins/walrus/index.d.mts +2 -13
- package/dist/plugins/walrus/index.mjs +18 -23
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.mjs +7 -5
- package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
- package/dist/plugins/walrus/mode/local-cluster.mjs +26 -7
- package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
- package/dist/plugins/walrus/registry-publish.mjs.map +1 -1
- package/dist/plugins/walrus/routable.mjs +7 -23
- package/dist/plugins/walrus/routable.mjs.map +1 -1
- package/dist/plugins/walrus/service.mjs.map +1 -1
- package/dist/plugins/walrus/spans.mjs +18 -0
- package/dist/plugins/walrus/spans.mjs.map +1 -0
- package/dist/plugins/walrus/storage-nodes.d.mts +6 -2
- package/dist/plugins/walrus/storage-nodes.mjs +15 -8
- package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
- package/dist/plugins/walrus/wal-swap.mjs +58 -15
- package/dist/plugins/walrus/wal-swap.mjs.map +1 -1
- package/dist/primitives/artifact-publisher.d.mts +1 -1
- package/dist/primitives/artifact-publisher.mjs +15 -0
- package/dist/primitives/artifact-publisher.mjs.map +1 -0
- package/dist/runtime/docker/client.mjs +17 -4
- package/dist/runtime/docker/client.mjs.map +1 -1
- package/dist/runtime/docker/container.mjs +219 -132
- package/dist/runtime/docker/container.mjs.map +1 -1
- package/dist/runtime/docker/errors.mjs +0 -6
- package/dist/runtime/docker/errors.mjs.map +1 -1
- package/dist/runtime/docker/exec.mjs +21 -18
- package/dist/runtime/docker/exec.mjs.map +1 -1
- package/dist/runtime/docker/image.mjs +36 -13
- package/dist/runtime/docker/image.mjs.map +1 -1
- package/dist/runtime/docker/index.mjs +14 -0
- package/dist/runtime/docker/inspect-and-decode.mjs +51 -0
- package/dist/runtime/docker/inspect-and-decode.mjs.map +1 -0
- package/dist/runtime/docker/inventory.mjs +14 -11
- package/dist/runtime/docker/inventory.mjs.map +1 -1
- package/dist/runtime/docker/labels.mjs +38 -5
- package/dist/runtime/docker/labels.mjs.map +1 -1
- package/dist/runtime/docker/logs.d.mts +1 -1
- package/dist/runtime/docker/logs.mjs.map +1 -1
- package/dist/runtime/docker/network.mjs +83 -47
- package/dist/runtime/docker/network.mjs.map +1 -1
- package/dist/runtime/docker/render-run-args.mjs +82 -0
- package/dist/runtime/docker/render-run-args.mjs.map +1 -0
- package/dist/runtime/docker/service.d.mts +1 -1
- package/dist/runtime/docker/service.mjs +12 -7
- package/dist/runtime/docker/service.mjs.map +1 -1
- package/dist/runtime/docker/sweep.mjs +118 -43
- package/dist/runtime/docker/sweep.mjs.map +1 -1
- package/dist/runtime/docker/volume.mjs +2 -0
- package/dist/runtime/docker/volume.mjs.map +1 -1
- package/dist/runtime/docker/wrap.mjs +30 -1
- package/dist/runtime/docker/wrap.mjs.map +1 -1
- package/dist/substrate/cross-process.mjs +14 -8
- package/dist/substrate/cross-process.mjs.map +1 -1
- package/dist/substrate/event-time.mjs +33 -0
- package/dist/substrate/event-time.mjs.map +1 -0
- package/dist/substrate/events.d.mts +28 -8
- package/dist/substrate/identity.d.mts +2 -1
- package/dist/substrate/manifest.d.mts +68 -5
- package/dist/substrate/manifest.mjs +34 -5
- package/dist/substrate/manifest.mjs.map +1 -1
- package/dist/substrate/network.d.mts +3 -5
- package/dist/substrate/options.d.mts +0 -2
- package/dist/substrate/plugin.d.mts +13 -0
- package/dist/substrate/plugin.mjs +4 -2
- package/dist/substrate/plugin.mjs.map +1 -1
- package/dist/substrate/projection.d.mts +35 -3
- package/dist/substrate/runtime/artifact-publisher/index.mjs +19 -9
- package/dist/substrate/runtime/artifact-publisher/index.mjs.map +1 -1
- package/dist/substrate/runtime/atomic-write.mjs +97 -32
- package/dist/substrate/runtime/atomic-write.mjs.map +1 -1
- package/dist/substrate/runtime/cache/index.mjs +3 -0
- package/dist/substrate/runtime/cache/schema.mjs +2 -2
- package/dist/substrate/runtime/cache/schema.mjs.map +1 -1
- package/dist/substrate/runtime/cache/service.mjs +9 -7
- package/dist/substrate/runtime/cache/service.mjs.map +1 -1
- package/dist/substrate/runtime/capability-sinks/index.d.mts +1 -1
- package/dist/substrate/runtime/capability-sinks/index.mjs +3 -0
- package/dist/substrate/runtime/capability-sinks/layer.mjs +1 -1
- package/dist/substrate/runtime/capability-sinks/service.d.mts +61 -2
- package/dist/substrate/runtime/capability-sinks/service.mjs +21 -26
- package/dist/substrate/runtime/capability-sinks/service.mjs.map +1 -1
- package/dist/substrate/runtime/config-validation.d.mts +1 -13
- package/dist/substrate/runtime/config-validation.mjs +2 -91
- package/dist/substrate/runtime/config-validation.mjs.map +1 -1
- package/dist/substrate/runtime/container-runtime.d.mts +1 -0
- package/dist/substrate/runtime/container-runtime.mjs +2 -0
- package/dist/substrate/runtime/control-plane/domain.mjs +101 -0
- package/dist/substrate/runtime/control-plane/domain.mjs.map +1 -0
- package/dist/substrate/runtime/control-plane/service.mjs +7 -0
- package/dist/substrate/runtime/control-plane/service.mjs.map +1 -0
- package/dist/substrate/runtime/cross-process/command-channel/channel.mjs +46 -19
- package/dist/substrate/runtime/cross-process/command-channel/channel.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/file-channel.mjs +30 -12
- package/dist/substrate/runtime/cross-process/command-channel/file-channel.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/index.mjs +5 -0
- package/dist/substrate/runtime/cross-process/command-channel/protocol.mjs +11 -2
- package/dist/substrate/runtime/cross-process/command-channel/protocol.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/runtime-control-lock.mjs +1 -1
- package/dist/substrate/runtime/cross-process/index.mjs +11 -0
- package/dist/substrate/runtime/cross-process/live-clock.mjs +34 -0
- package/dist/substrate/runtime/cross-process/live-clock.mjs.map +1 -0
- package/dist/substrate/runtime/cross-process/liveness.mjs +52 -10
- package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/lock.d.mts +1 -0
- package/dist/substrate/runtime/cross-process/lock.mjs +23 -0
- package/dist/substrate/runtime/cross-process/lock.mjs.map +1 -0
- package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs +64 -0
- package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs.map +1 -0
- package/dist/substrate/runtime/cross-process/roster.mjs +97 -83
- package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/self-pid.mjs +8 -0
- package/dist/substrate/runtime/cross-process/self-pid.mjs.map +1 -0
- package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs +24 -28
- package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/stack-lock.mjs +41 -20
- package/dist/substrate/runtime/cross-process/stack-lock.mjs.map +1 -1
- package/dist/substrate/runtime/current-plugin.mjs +2 -2
- package/dist/substrate/runtime/current-plugin.mjs.map +1 -1
- package/dist/substrate/runtime/errors.mjs +12 -6
- package/dist/substrate/runtime/errors.mjs.map +1 -1
- package/dist/substrate/runtime/format-unknown-error.mjs +30 -0
- package/dist/substrate/runtime/format-unknown-error.mjs.map +1 -0
- package/dist/substrate/runtime/host-bind-mount-owner.mjs +10 -0
- package/dist/substrate/runtime/host-bind-mount-owner.mjs.map +1 -0
- package/dist/substrate/runtime/host-gateway.mjs +20 -0
- package/dist/substrate/runtime/host-gateway.mjs.map +1 -0
- package/dist/substrate/runtime/host-tree-tar/index.mjs +61 -50
- package/dist/substrate/runtime/host-tree-tar/index.mjs.map +1 -1
- package/dist/substrate/runtime/http-probe.d.mts +1 -1
- package/dist/substrate/runtime/index.mjs +14 -0
- package/dist/substrate/runtime/lease-broker/index.d.mts +1 -1
- package/dist/substrate/runtime/lease-broker/index.mjs +2 -0
- package/dist/substrate/runtime/lease-broker/service.d.mts +1 -64
- package/dist/substrate/runtime/lease-broker/service.mjs +1 -1
- package/dist/substrate/runtime/lease-broker/service.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/dep-graph.mjs +22 -3
- package/dist/substrate/runtime/lifecycle/dep-graph.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/index.mjs +9 -0
- package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/plugin-registry.mjs +20 -3
- package/dist/substrate/runtime/lifecycle/plugin-registry.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/selective-restart.mjs +1 -1
- package/dist/substrate/runtime/lifecycle/signals.mjs +5 -5
- package/dist/substrate/runtime/lifecycle/signals.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/state-machine.mjs +1 -1
- package/dist/substrate/runtime/managed-container.d.mts +19 -1
- package/dist/substrate/runtime/managed-container.mjs +20 -1
- package/dist/substrate/runtime/managed-container.mjs.map +1 -1
- package/dist/substrate/runtime/manifest/index.mjs +3 -0
- package/dist/substrate/runtime/manifest/manifest.mjs +15 -11
- package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
- package/dist/substrate/runtime/mode-errors.mjs +20 -0
- package/dist/substrate/runtime/mode-errors.mjs.map +1 -0
- package/dist/substrate/runtime/observability/cascade-formatter.mjs +9 -13
- package/dist/substrate/runtime/observability/cascade-formatter.mjs.map +1 -1
- package/dist/substrate/runtime/observability/formatter-registry.mjs +19 -36
- package/dist/substrate/runtime/observability/formatter-registry.mjs.map +1 -1
- package/dist/substrate/runtime/observability/ignore-with-log.d.mts +1 -0
- package/dist/substrate/runtime/observability/ignore-with-log.mjs +17 -0
- package/dist/substrate/runtime/observability/ignore-with-log.mjs.map +1 -0
- package/dist/substrate/runtime/observability/index.d.mts +1 -1
- package/dist/substrate/runtime/observability/index.mjs +13 -0
- package/dist/substrate/runtime/observability/log-store.d.mts +1 -0
- package/dist/substrate/runtime/observability/log-store.mjs +175 -0
- package/dist/substrate/runtime/observability/log-store.mjs.map +1 -0
- package/dist/substrate/runtime/observability/logger.d.mts +2 -1
- package/dist/substrate/runtime/observability/logger.mjs +5 -9
- package/dist/substrate/runtime/observability/logger.mjs.map +1 -1
- package/dist/substrate/runtime/observability/output-truncate.mjs +35 -0
- package/dist/substrate/runtime/observability/output-truncate.mjs.map +1 -0
- package/dist/substrate/runtime/observability/pretty-error.mjs +9 -12
- package/dist/substrate/runtime/observability/pretty-error.mjs.map +1 -1
- package/dist/substrate/runtime/observability/redaction.d.mts +2 -13
- package/dist/substrate/runtime/observability/redaction.mjs +8 -14
- package/dist/substrate/runtime/observability/redaction.mjs.map +1 -1
- package/dist/substrate/runtime/observability/span-store.d.mts +1 -0
- package/dist/substrate/runtime/observability/span-store.mjs +110 -0
- package/dist/substrate/runtime/observability/span-store.mjs.map +1 -0
- package/dist/substrate/runtime/observability/spans.mjs +14 -14
- package/dist/substrate/runtime/observability/spans.mjs.map +1 -1
- package/dist/substrate/runtime/observability/subprocess-capture.mjs +1 -1
- package/dist/substrate/runtime/passthrough-or-wrap.mjs +33 -0
- package/dist/substrate/runtime/passthrough-or-wrap.mjs.map +1 -0
- package/dist/substrate/runtime/paths.d.mts +1 -1
- package/dist/substrate/runtime/paths.mjs +4 -3
- package/dist/substrate/runtime/paths.mjs.map +1 -1
- package/dist/substrate/runtime/phase-preserving-produce.mjs +40 -0
- package/dist/substrate/runtime/phase-preserving-produce.mjs.map +1 -0
- package/dist/substrate/runtime/port-broker/index.mjs +3 -0
- package/dist/substrate/runtime/port-broker/service.mjs +170 -126
- package/dist/substrate/runtime/port-broker/service.mjs.map +1 -1
- package/dist/substrate/runtime/post-acquire-tasks.mjs +1 -1
- package/dist/substrate/runtime/post-acquire-tasks.mjs.map +1 -1
- package/dist/substrate/runtime/probes.d.mts +7 -5
- package/dist/substrate/runtime/probes.mjs +3 -1
- package/dist/substrate/runtime/probes.mjs.map +1 -1
- package/dist/substrate/runtime/projection/index.mjs +4 -0
- package/dist/substrate/runtime/projection/operational-endpoints.mjs +1 -2
- package/dist/substrate/runtime/projection/operational-endpoints.mjs.map +1 -1
- package/dist/substrate/runtime/projection/persisted.mjs +33 -15
- package/dist/substrate/runtime/projection/persisted.mjs.map +1 -1
- package/dist/substrate/runtime/projection/state-ref.mjs +26 -2
- package/dist/substrate/runtime/projection/state-ref.mjs.map +1 -1
- package/dist/substrate/runtime/projection/update.mjs +88 -24
- package/dist/substrate/runtime/projection/update.mjs.map +1 -1
- package/dist/substrate/runtime/random-suffix.mjs +11 -0
- package/dist/substrate/runtime/random-suffix.mjs.map +1 -0
- package/dist/substrate/runtime/retry-policy.d.mts +59 -1
- package/dist/substrate/runtime/retry-policy.mjs +66 -1
- package/dist/substrate/runtime/retry-policy.mjs.map +1 -1
- package/dist/substrate/runtime/routed-url.d.mts +1 -0
- package/dist/substrate/runtime/routed-url.mjs +79 -0
- package/dist/substrate/runtime/routed-url.mjs.map +1 -0
- package/dist/substrate/runtime/runtime-decode.d.mts +1 -1
- package/dist/substrate/runtime/runtime-decode.mjs.map +1 -1
- package/dist/substrate/runtime/scoped-http-server.mjs +2 -3
- package/dist/substrate/runtime/scoped-http-server.mjs.map +1 -1
- package/dist/substrate/runtime/scoped-multimap/index.mjs +2 -0
- package/dist/substrate/runtime/scoped-multimap/service.mjs +52 -0
- package/dist/substrate/runtime/scoped-multimap/service.mjs.map +1 -0
- package/dist/substrate/runtime/scoped-ref-map/index.mjs +2 -0
- package/dist/substrate/runtime/scoped-ref-map/service.mjs +2 -2
- package/dist/substrate/runtime/scoped-ref-map/service.mjs.map +1 -1
- package/dist/substrate/runtime/stage-and-swap/index.mjs +72 -4
- package/dist/substrate/runtime/stage-and-swap/index.mjs.map +1 -1
- package/dist/substrate/runtime/state-store/index.d.mts +1 -0
- package/dist/substrate/runtime/state-store/index.mjs +3 -0
- package/dist/substrate/runtime/state-store/schema.d.mts +1 -0
- package/dist/substrate/runtime/state-store/schema.mjs +19 -14
- package/dist/substrate/runtime/state-store/schema.mjs.map +1 -1
- package/dist/substrate/runtime/state-store/service.d.mts +1 -0
- package/dist/substrate/runtime/state-store/service.mjs +145 -0
- package/dist/substrate/runtime/state-store/service.mjs.map +1 -0
- package/dist/substrate/runtime/strategy-registry/chain-keyed-strategy-for.d.mts +1 -0
- package/dist/substrate/runtime/strategy-registry/chain-keyed-strategy-for.mjs +33 -0
- package/dist/substrate/runtime/strategy-registry/chain-keyed-strategy-for.mjs.map +1 -0
- package/dist/substrate/runtime/strategy-registry/chain-probe-for.mjs +1 -1
- package/dist/substrate/runtime/strategy-registry/chain-probe-for.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/index.mjs +4 -0
- package/dist/substrate/runtime/strategy-registry/service.mjs +19 -33
- package/dist/substrate/runtime/strategy-registry/service.mjs.map +1 -1
- package/dist/substrate/runtime/subnet-broker.mjs +60 -0
- package/dist/substrate/runtime/subnet-broker.mjs.map +1 -0
- package/dist/substrate/runtime/sui-execute/index.d.mts +21 -1
- package/dist/substrate/runtime/sui-execute/index.mjs +84 -20
- package/dist/substrate/runtime/sui-execute/index.mjs.map +1 -1
- package/dist/substrate/runtime/sui-execute/sign-and-dispatch.d.mts +1 -0
- package/dist/substrate/runtime/sui-execute/sign-and-dispatch.mjs +32 -0
- package/dist/substrate/runtime/sui-execute/sign-and-dispatch.mjs.map +1 -0
- package/dist/substrate/runtime/sui-ledger/object-ref.mjs +32 -0
- package/dist/substrate/runtime/sui-ledger/object-ref.mjs.map +1 -0
- package/dist/substrate/runtime/sui-move-build/index.mjs +93 -19
- package/dist/substrate/runtime/sui-move-build/index.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/acquire-node.mjs +181 -0
- package/dist/substrate/runtime/supervisor/acquire-node.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/background-tasks.mjs +144 -0
- package/dist/substrate/runtime/supervisor/background-tasks.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/command-loop.mjs +68 -0
- package/dist/substrate/runtime/supervisor/command-loop.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs +151 -0
- package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/errors.d.mts +1 -0
- package/dist/substrate/runtime/supervisor/errors.mjs +9 -0
- package/dist/substrate/runtime/supervisor/errors.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/index.d.mts +1 -0
- package/dist/substrate/runtime/supervisor/index.mjs +3 -0
- package/dist/substrate/runtime/supervisor/shutdown.mjs +51 -0
- package/dist/substrate/runtime/supervisor/shutdown.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/start-supervisor.d.mts +1 -0
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs +250 -0
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/state.d.mts +1 -0
- package/dist/substrate/runtime/supervisor/state.mjs +22 -0
- package/dist/substrate/runtime/supervisor/state.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/teardown.mjs +57 -0
- package/dist/substrate/runtime/supervisor/teardown.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/types.d.mts +1 -0
- package/dist/substrate/runtime/supervisor/wiring.mjs +133 -0
- package/dist/substrate/runtime/supervisor/wiring.mjs.map +1 -0
- package/dist/substrate/runtime/typed-env.mjs +8 -0
- package/dist/substrate/runtime/typed-env.mjs.map +1 -0
- package/dist/substrate/state-store.d.mts +1 -0
- package/dist/substrate/versioned-doc-schema.mjs +17 -0
- package/dist/substrate/versioned-doc-schema.mjs.map +1 -0
- package/dist/substrate/versioned-doc-sync.mjs +94 -0
- package/dist/substrate/versioned-doc-sync.mjs.map +1 -0
- package/dist/surfaces/cli/command-tree.mjs.map +1 -1
- package/dist/surfaces/cli/commands/index.mjs +9 -0
- package/dist/surfaces/cli/commands/prune-picker-entry.mjs +13 -0
- package/dist/surfaces/cli/commands/prune-picker-entry.mjs.map +1 -0
- package/dist/surfaces/cli/commands/prune-picker.mjs +3 -2
- package/dist/surfaces/cli/commands/prune-picker.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune.mjs +34 -17
- package/dist/surfaces/cli/commands/prune.mjs.map +1 -1
- package/dist/surfaces/cli/commands/status.mjs +2 -2
- package/dist/surfaces/cli/commands/status.mjs.map +1 -1
- package/dist/surfaces/cli/commands/supervisor-presence.mjs +5 -3
- package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
- package/dist/surfaces/cli/commands/wipe.mjs +38 -4
- package/dist/surfaces/cli/commands/wipe.mjs.map +1 -1
- package/dist/surfaces/cli/errors.mjs +1 -1
- package/dist/surfaces/cli/errors.mjs.map +1 -1
- package/dist/surfaces/cli/flags.mjs +0 -1
- package/dist/surfaces/cli/flags.mjs.map +1 -1
- package/dist/surfaces/cli/index.mjs +98 -18
- package/dist/surfaces/cli/index.mjs.map +1 -1
- package/dist/surfaces/cli/output.mjs +1 -0
- package/dist/surfaces/cli/output.mjs.map +1 -1
- package/dist/surfaces/cli/sysexits.mjs +2 -1
- package/dist/surfaces/cli/sysexits.mjs.map +1 -1
- package/dist/surfaces/tui/app.mjs +25 -3
- package/dist/surfaces/tui/app.mjs.map +1 -1
- package/dist/surfaces/tui/display-derivation.mjs +23 -74
- package/dist/surfaces/tui/display-derivation.mjs.map +1 -1
- package/dist/surfaces/tui/event-log.mjs +35 -23
- package/dist/surfaces/tui/event-log.mjs.map +1 -1
- package/dist/surfaces/tui/input.mjs +5 -1
- package/dist/surfaces/tui/input.mjs.map +1 -1
- package/dist/surfaces/tui/plain-renderer.mjs +70 -34
- package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
- package/package.json +24 -4
- package/dist/contracts/liveness-classifier.d.mts +0 -19
- package/dist/contracts/network-resolver.d.mts +0 -15
- package/dist/contracts/network-resolver.mjs +0 -7
- package/dist/contracts/network-resolver.mjs.map +0 -1
- package/dist/orchestrators/codegen/extras.mjs +0 -16
- package/dist/orchestrators/codegen/extras.mjs.map +0 -1
- package/dist/plugins/deepbook/routable.mjs +0 -21
- package/dist/plugins/deepbook/routable.mjs.map +0 -1
- package/dist/plugins/faucet/service.d.mts +0 -20
- package/dist/plugins/faucet/strategies/sui-local.d.mts +0 -23
- package/dist/plugins/faucet/strategies/sui-local.mjs.map +0 -1
- package/dist/plugins/sui/seed-objects.d.mts +0 -18
- package/dist/plugins/sui/seed-objects.mjs +0 -25
- package/dist/plugins/sui/seed-objects.mjs.map +0 -1
- package/dist/runtime/built-in-plugin-layers.mjs.map +0 -1
- package/dist/substrate/runtime/context-helpers.mjs +0 -32
- package/dist/substrate/runtime/context-helpers.mjs.map +0 -1
- package/dist/substrate/runtime/run.mjs.map +0 -1
- package/dist/substrate/runtime/strategy-registry/faucet-capability-for.mjs +0 -30
- package/dist/substrate/runtime/strategy-registry/faucet-capability-for.mjs.map +0 -1
- package/dist/substrate/runtime/supervisor.d.mts +0 -1
- package/dist/substrate/runtime/supervisor.mjs +0 -831
- package/dist/substrate/runtime/supervisor.mjs.map +0 -1
- package/dist/surfaces/cli/commands/doctor-probes.mjs.map +0 -1
- /package/dist/{substrate/runtime/strategy-registry/faucet-capability-for.d.mts → plugins/faucet/http.d.mts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.mjs","names":["nodeHostname"],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/channel.ts"],"sourcesContent":["// High-level cross-process command channel.\n//\n// Two halves built on the same `commands.ndjson` + `events.ndjson` pair:\n//\n// - Publisher side (CLI / TUI / programmable API caller): appends to\n// `commands.ndjson`; tails `events.ndjson` so it can correlate\n// ack/error replies AND surface incoming engine events (for `logs`\n// and `status --watch`-style verbs).\n//\n// - Supervisor side: tails `commands.ndjson` to receive incoming\n// intents; appends to `events.ndjson` to broadcast engine events\n// and ack/error replies.\n//\n// Both halves are scope-bound — the underlying poll fiber is forked\n// into the surrounding Scope and stops when the Scope closes.\n\nimport { hostname as nodeHostname } from 'node:os';\nimport { randomUUID } from 'node:crypto';\n\nimport { Effect, Option, Ref, Stream, Scope } from 'effect';\n\nimport type { EngineCommand } from '../../../events.ts';\nimport { decodeUnknownSync } from '../../runtime-decode.ts';\nimport { type CommandChannelError, appendRecord, ensureFile, tailRecords } from './file-channel.ts';\nimport {\n\tCOMMAND_CHANNEL_PROTOCOL_VERSION,\n\tCommandRecordSchema,\n\tEventRecordSchema,\n\ttype CommandRecord,\n\ttype EventRecord,\n} from './protocol.ts';\n\nexport interface CommandChannelPaths {\n\treadonly commandsFile: string;\n\treadonly eventsFile: string;\n}\n\nexport const COMMAND_CHANNEL_COMMANDS_FILE_NAME = 'commands.ndjson';\nexport const COMMAND_CHANNEL_EVENTS_FILE_NAME = 'events.ndjson';\n\n/** Build a `command-channel` paths bundle from a stackRoot dir. The\n * files live next to the existing cross-process artifacts. */\nexport const commandChannelPaths = (stackRoot: string): CommandChannelPaths => ({\n\tcommandsFile: `${stackRoot}/${COMMAND_CHANNEL_COMMANDS_FILE_NAME}`,\n\teventsFile: `${stackRoot}/${COMMAND_CHANNEL_EVENTS_FILE_NAME}`,\n});\n\n// -----------------------------------------------------------------------------\n// Publisher side\n// -----------------------------------------------------------------------------\n\nexport interface PublishedCommand {\n\t/** Per-record id used to correlate `ack` / `error` replies. */\n\treadonly id: string;\n}\n\n/**\n * Publisher half: append a typed command, optionally await its\n * corresponding ack/error reply.\n *\n * `publish(cmd)` writes one record and returns the id; the caller can\n * then `awaitCompletion(id)` to block until the supervisor either acks\n * or errors. `awaitCompletion` is a separate seam so verbs that don't\n * need correlation (TUI-style fire-and-forget) skip the wait.\n */\nexport interface CommandChannelPublisher {\n\treadonly publish: (\n\t\tcommand: EngineCommand,\n\t) => Effect.Effect<PublishedCommand, CommandChannelError>;\n\treadonly awaitCompletion: (\n\t\tid: string,\n\t\toptions?: { readonly timeoutMillis?: number },\n\t) => Effect.Effect<\n\t\t{ readonly ok: true } | { readonly ok: false; readonly message: string },\n\t\tCommandChannelError\n\t>;\n\treadonly events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope>;\n}\n\ninterface PublisherState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSeq = (state: PublisherState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/**\n * Build the publisher half. The events Stream is scope-bound:\n * pass it through `Stream.runDrain` inside a `Scope.use` or fork it\n * with `Effect.forkScoped` so the tail polling cleans up.\n */\nexport const makeCommandChannelPublisher = (\n\tpaths: CommandChannelPaths,\n): Effect.Effect<CommandChannelPublisher, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: PublisherState = { seq: yield* Ref.make(0) };\n\t\tconst pid = process.pid;\n\t\tconst host = nodeHostname();\n\n\t\tconst publish = (\n\t\t\tcommand: EngineCommand,\n\t\t): Effect.Effect<PublishedCommand, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSeq(state);\n\t\t\t\tconst id = `${pid}-${seq}-${randomUUID().slice(0, 8)}`;\n\t\t\t\tconst record: CommandRecord = {\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tid,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tpublisherPid: pid,\n\t\t\t\t\tpublisherHostname: host,\n\t\t\t\t\tcommand,\n\t\t\t\t};\n\t\t\t\tyield* appendRecord(paths.commandsFile, record);\n\t\t\t\treturn { id };\n\t\t\t});\n\n\t\tconst decodeEvent = (raw: unknown): EventRecord =>\n\t\t\tdecodeUnknownSync(EventRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel event',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.eventsFile,\n\t\t\t(raw) => decodeEvent(raw),\n\t\t\t{ fromOffset: 'current' },\n\t\t);\n\n\t\tconst findReply = (\n\t\t\tid: string,\n\t\t): Effect.Effect<\n\t\t\tOption.Option<Extract<EventRecord, { kind: 'ack' | 'error' }>>,\n\t\t\tCommandChannelError,\n\t\t\tScope.Scope\n\t\t> =>\n\t\t\t// Start at 'start' so awaitCompletion picks up an ack that\n\t\t\t// landed before the await call attached. The filter by id is\n\t\t\t// the actual correlation; replaying old records is cheap.\n\t\t\tStream.runHead(\n\t\t\t\ttailRecords(paths.eventsFile, (raw) => decodeEvent(raw), {\n\t\t\t\t\tfromOffset: 'start',\n\t\t\t\t}).pipe(\n\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t(rec): rec is Extract<EventRecord, { kind: 'ack' | 'error' }> =>\n\t\t\t\t\t\t\t(rec.kind === 'ack' || rec.kind === 'error') && rec.correlatesTo === id,\n\t\t\t\t\t),\n\t\t\t\t\tStream.take(1),\n\t\t\t\t),\n\t\t\t);\n\n\t\tconst awaitCompletion = (\n\t\t\tid: string,\n\t\t\toptions: { readonly timeoutMillis?: number } = {},\n\t\t): Effect.Effect<\n\t\t\t{ readonly ok: true } | { readonly ok: false; readonly message: string },\n\t\t\tCommandChannelError\n\t\t> => {\n\t\t\tconst withScope = Effect.scoped(findReply(id));\n\t\t\tconst final =\n\t\t\t\toptions.timeoutMillis !== undefined\n\t\t\t\t\t? withScope.pipe(\n\t\t\t\t\t\t\tEffect.timeoutOption(`${options.timeoutMillis} millis`),\n\t\t\t\t\t\t\tEffect.map((outer) => Option.flatten(outer)),\n\t\t\t\t\t\t)\n\t\t\t\t\t: withScope;\n\t\t\treturn final.pipe(\n\t\t\t\tEffect.map((reply) => {\n\t\t\t\t\tconst inner = Option.getOrNull(reply);\n\t\t\t\t\tif (inner === null) {\n\t\t\t\t\t\treturn options.timeoutMillis !== undefined\n\t\t\t\t\t\t\t? { ok: false as const, message: 'timed out waiting for ack' }\n\t\t\t\t\t\t\t: { ok: false as const, message: 'no reply' };\n\t\t\t\t\t}\n\t\t\t\t\tif (inner.kind === 'ack') return { ok: true as const };\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false as const,\n\t\t\t\t\t\tmessage: inner.detail ? `${inner.message}: ${inner.detail}` : inner.message,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t);\n\t\t};\n\n\t\treturn { publish, awaitCompletion, events };\n\t});\n\n// -----------------------------------------------------------------------------\n// Supervisor side\n// -----------------------------------------------------------------------------\n\nexport interface CommandChannelSubscriberOptions {\n\t/** From which offset to start reading commands. Defaults to `current`\n\t * so a freshly-booted supervisor doesn't replay queued intents from\n\t * before it was alive. */\n\treadonly fromOffset?: 'start' | 'current';\n\treadonly pollMillis?: number;\n}\n\n/** Tail incoming commands. The supervisor wires its event hub through\n * `publishEvent` so engine events appear on the events file. */\nexport interface CommandChannelSubscriber {\n\treadonly commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope>;\n\treadonly publishEvent: (event: unknown) => Effect.Effect<void, CommandChannelError>;\n\treadonly ack: (correlatesTo: string, detail?: string) => Effect.Effect<void, CommandChannelError>;\n\treadonly fail: (\n\t\tcorrelatesTo: string,\n\t\tmessage: string,\n\t\tdetail?: string,\n\t) => Effect.Effect<void, CommandChannelError>;\n}\n\ninterface SubscriberState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSubSeq = (state: SubscriberState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/** Build the supervisor half. The commands Stream is scope-bound. */\nexport const makeCommandChannelSubscriber = (\n\tpaths: CommandChannelPaths,\n\toptions: CommandChannelSubscriberOptions = {},\n): Effect.Effect<CommandChannelSubscriber, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: SubscriberState = { seq: yield* Ref.make(0) };\n\n\t\tconst decodeCommand = (raw: unknown): CommandRecord =>\n\t\t\tdecodeUnknownSync(CommandRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel command',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.commandsFile,\n\t\t\t(raw) => decodeCommand(raw),\n\t\t\t{\n\t\t\t\tfromOffset: options.fromOffset ?? 'current',\n\t\t\t\tpollMillis: options.pollMillis,\n\t\t\t},\n\t\t);\n\n\t\tconst writeEvent = (record: EventRecord): Effect.Effect<void, CommandChannelError> =>\n\t\t\tappendRecord(paths.eventsFile, record);\n\n\t\tconst publishEvent = (event: unknown): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'engine',\n\t\t\t\t\tevent,\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst ack = (correlatesTo: string, detail?: string): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'ack',\n\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst fail = (\n\t\t\tcorrelatesTo: string,\n\t\t\tmessage: string,\n\t\t\tdetail?: string,\n\t\t): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'error',\n\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\tmessage,\n\t\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t\t});\n\t\t\t});\n\n\t\treturn { commands, publishEvent, ack, fail };\n\t});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"channel.mjs","names":["nodeHostname"],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/channel.ts"],"sourcesContent":["// High-level cross-process command channel.\n//\n// Two halves built on the same `commands.ndjson` + `events.ndjson` pair:\n//\n// - Publisher side (CLI / TUI / programmable API caller): appends to\n// `commands.ndjson`; tails `events.ndjson` so it can correlate\n// ack/error replies AND surface incoming engine events (for `logs`\n// and `status --watch`-style verbs).\n//\n// - Supervisor side: tails `commands.ndjson` to receive incoming\n// intents; appends to `events.ndjson` to broadcast engine events\n// and ack/error replies.\n//\n// Both halves are scope-bound — the underlying poll fiber is forked\n// into the surrounding Scope and stops when the Scope closes.\n\nimport { existsSync, statSync } from 'node:fs';\nimport { hostname as nodeHostname } from 'node:os';\nimport { randomUUID } from 'node:crypto';\n\nimport { Effect, Option, Ref, Stream, Scope } from 'effect';\n\nimport type { EngineCommand } from '../../../events.ts';\nimport { decodeUnknownSync } from '../../runtime-decode.ts';\nimport { selfPid } from '../self-pid.ts';\nimport {\n\ttype CommandChannelError,\n\tCommandChannelIoError,\n\tappendRecord,\n\tensureFile,\n\ttailRecords,\n} from './file-channel.ts';\nimport {\n\tCOMMAND_CHANNEL_PROTOCOL_VERSION,\n\tCommandRecordSchema,\n\tEventRecordSchema,\n\ttype CommandRecord,\n\ttype EventRecord,\n} from './protocol.ts';\n\nexport interface CommandChannelPaths {\n\treadonly commandsFile: string;\n\treadonly eventsFile: string;\n}\n\nexport const COMMAND_CHANNEL_COMMANDS_FILE_NAME = 'commands.ndjson';\nexport const COMMAND_CHANNEL_EVENTS_FILE_NAME = 'events.ndjson';\n\n/** Build a `command-channel` paths bundle from a stackRoot dir. The\n * files live next to the existing cross-process artifacts. */\nexport const commandChannelPaths = (stackRoot: string): CommandChannelPaths => ({\n\tcommandsFile: `${stackRoot}/${COMMAND_CHANNEL_COMMANDS_FILE_NAME}`,\n\teventsFile: `${stackRoot}/${COMMAND_CHANNEL_EVENTS_FILE_NAME}`,\n});\n\n// -----------------------------------------------------------------------------\n// Publisher side\n// -----------------------------------------------------------------------------\n\nexport interface PublishedCommand {\n\t/** Per-record id used to correlate `ack` / `error` replies. */\n\treadonly id: string;\n\t/** Byte offset of the events file at publish time. `awaitCompletion`\n\t * tails from here so each await scans only events appended after the\n\t * command was published, instead of replaying the whole file on every\n\t * call (O(N·total) rescans). A reply can only land at-or-after this\n\t * offset because the supervisor reads the command before acking. */\n\treadonly fromOffset: number;\n}\n\n/** Default bound for `awaitCompletion` when the caller passes no\n * `timeoutMillis`. A correlated ack/error normally lands within tens of\n * ms; an absent reply (supervisor died mid-handle, dropped record) must\n * NOT park the await fiber forever, so the wait is always bounded. */\nexport const DEFAULT_AWAIT_COMPLETION_MILLIS = 30_000;\n\n/**\n * Publisher half: append a typed command, optionally await its\n * corresponding ack/error reply.\n *\n * `publish(cmd)` writes one record and returns the id; the caller can\n * then `awaitCompletion(id)` to block until the supervisor either acks\n * or errors. `awaitCompletion` is a separate seam so verbs that don't\n * need correlation (TUI-style fire-and-forget) skip the wait.\n */\nexport interface CommandChannelPublisher {\n\treadonly publish: (\n\t\tcommand: EngineCommand,\n\t) => Effect.Effect<PublishedCommand, CommandChannelError>;\n\treadonly awaitCompletion: (\n\t\tpublished: PublishedCommand,\n\t\toptions?: { readonly timeoutMillis?: number },\n\t) => Effect.Effect<\n\t\t| { readonly ok: true; readonly payload?: unknown }\n\t\t| { readonly ok: false; readonly message: string; readonly payload?: unknown },\n\t\tCommandChannelError\n\t>;\n\treadonly events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope>;\n}\n\ninterface PublisherState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSeq = (state: PublisherState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/**\n * Build the publisher half. The events Stream is scope-bound:\n * pass it through `Stream.runDrain` inside a `Scope.use` or fork it\n * with `Effect.forkScoped` so the tail polling cleans up.\n */\nexport const makeCommandChannelPublisher = (\n\tpaths: CommandChannelPaths,\n): Effect.Effect<CommandChannelPublisher, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: PublisherState = { seq: yield* Ref.make(0) };\n\t\tconst pid = selfPid();\n\t\tconst host = nodeHostname();\n\n\t\tconst publish = (\n\t\t\tcommand: EngineCommand,\n\t\t): Effect.Effect<PublishedCommand, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSeq(state);\n\t\t\t\tconst id = `${pid}-${seq}-${randomUUID().slice(0, 8)}`;\n\t\t\t\t// Snapshot the events-file size BEFORE appending the command:\n\t\t\t\t// any correlated reply is appended strictly after the\n\t\t\t\t// supervisor observes this command, so a tail from this offset\n\t\t\t\t// cannot miss the reply while skipping the entire backlog.\n\t\t\t\tconst fromOffset = yield* Effect.try({\n\t\t\t\t\ttry: () => (existsSync(paths.eventsFile) ? statSync(paths.eventsFile).size : 0),\n\t\t\t\t\tcatch: (cause) =>\n\t\t\t\t\t\tnew CommandChannelIoError({ path: paths.eventsFile, stage: 'stat', cause }),\n\t\t\t\t});\n\t\t\t\tconst record: CommandRecord = {\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tid,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tpublisherPid: pid,\n\t\t\t\t\tpublisherHostname: host,\n\t\t\t\t\tcommand,\n\t\t\t\t};\n\t\t\t\tyield* appendRecord(paths.commandsFile, record);\n\t\t\t\treturn { id, fromOffset };\n\t\t\t});\n\n\t\tconst decodeEvent = (raw: unknown): EventRecord =>\n\t\t\tdecodeUnknownSync(EventRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel event',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.eventsFile,\n\t\t\t(raw) => decodeEvent(raw),\n\t\t\t{ fromOffset: 'current', onDecodeError: 'skip' },\n\t\t);\n\n\t\tconst findReply = (\n\t\t\tid: string,\n\t\t\tfromOffset: number,\n\t\t): Effect.Effect<\n\t\t\tOption.Option<Extract<EventRecord, { kind: 'ack' | 'error' }>>,\n\t\t\tCommandChannelError,\n\t\t\tScope.Scope\n\t\t> =>\n\t\t\t// Tail from the publish-time offset rather than 'start': the\n\t\t\t// reply is always appended after this point, so scanning the\n\t\t\t// whole file on every await (O(N·total)) is wasteful. The filter\n\t\t\t// by id is the actual correlation.\n\t\t\t// `onDecodeError: 'skip'` keeps the tail alive across truncated /\n\t\t\t// corrupt lines from a peer's mid-flight atomic append, per\n\t\t\t// STYLE_GUIDE §20 NDJSON tolerance rule.\n\t\t\tStream.runHead(\n\t\t\t\ttailRecords(paths.eventsFile, (raw) => decodeEvent(raw), {\n\t\t\t\t\tfromOffset,\n\t\t\t\t\tonDecodeError: 'skip',\n\t\t\t\t}).pipe(\n\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t(rec): rec is Extract<EventRecord, { kind: 'ack' | 'error' }> =>\n\t\t\t\t\t\t\t(rec.kind === 'ack' || rec.kind === 'error') && rec.correlatesTo === id,\n\t\t\t\t\t),\n\t\t\t\t\tStream.take(1),\n\t\t\t\t),\n\t\t\t);\n\n\t\tconst awaitCompletion = (\n\t\t\tpublished: PublishedCommand,\n\t\t\toptions: { readonly timeoutMillis?: number } = {},\n\t\t): Effect.Effect<\n\t\t\t| { readonly ok: true; readonly payload?: unknown }\n\t\t\t| { readonly ok: false; readonly message: string; readonly payload?: unknown },\n\t\t\tCommandChannelError\n\t\t> => {\n\t\t\t// Always bound the wait: a missing reply (supervisor died, dropped\n\t\t\t// record) must NOT park the fiber forever, so an absent\n\t\t\t// `timeoutMillis` falls back to `DEFAULT_AWAIT_COMPLETION_MILLIS`.\n\t\t\tconst timeoutMillis = options.timeoutMillis ?? DEFAULT_AWAIT_COMPLETION_MILLIS;\n\t\t\tconst final = Effect.scoped(findReply(published.id, published.fromOffset)).pipe(\n\t\t\t\tEffect.timeoutOption(`${timeoutMillis} millis`),\n\t\t\t\tEffect.map((outer) => Option.flatten(outer)),\n\t\t\t);\n\t\t\treturn final.pipe(\n\t\t\t\tEffect.map((reply) => {\n\t\t\t\t\tconst inner = Option.getOrNull(reply);\n\t\t\t\t\tif (inner === null) {\n\t\t\t\t\t\treturn { ok: false as const, message: 'timed out waiting for ack' };\n\t\t\t\t\t}\n\t\t\t\t\tif (inner.kind === 'ack') {\n\t\t\t\t\t\treturn inner.payload !== undefined\n\t\t\t\t\t\t\t? { ok: true as const, payload: inner.payload }\n\t\t\t\t\t\t\t: { ok: true as const };\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false as const,\n\t\t\t\t\t\tmessage: inner.detail ? `${inner.message}: ${inner.detail}` : inner.message,\n\t\t\t\t\t\t...(inner.payload !== undefined ? { payload: inner.payload } : {}),\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t);\n\t\t};\n\n\t\treturn { publish, awaitCompletion, events };\n\t});\n\n// -----------------------------------------------------------------------------\n// Supervisor side\n// -----------------------------------------------------------------------------\n\nexport interface CommandChannelSubscriberOptions {\n\t/** From which offset to start reading commands. Defaults to `current`\n\t * so a freshly-booted supervisor doesn't replay queued intents from\n\t * before it was alive. */\n\treadonly fromOffset?: 'start' | 'current';\n\treadonly pollMillis?: number;\n}\n\n/** Tail incoming commands. The supervisor wires its event hub through\n * `publishEvent` so engine events appear on the events file. */\nexport interface CommandChannelSubscriber {\n\treadonly commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope>;\n\treadonly publishEvent: (event: unknown) => Effect.Effect<void, CommandChannelError>;\n\treadonly ack: (\n\t\tcorrelatesTo: string,\n\t\tdetail?: string,\n\t\tpayload?: unknown,\n\t) => Effect.Effect<void, CommandChannelError>;\n\treadonly fail: (\n\t\tcorrelatesTo: string,\n\t\tmessage: string,\n\t\tdetail?: string,\n\t\tpayload?: unknown,\n\t) => Effect.Effect<void, CommandChannelError>;\n}\n\ninterface SubscriberState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSubSeq = (state: SubscriberState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/** Build the supervisor half. The commands Stream is scope-bound. */\nexport const makeCommandChannelSubscriber = (\n\tpaths: CommandChannelPaths,\n\toptions: CommandChannelSubscriberOptions = {},\n): Effect.Effect<CommandChannelSubscriber, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: SubscriberState = { seq: yield* Ref.make(0) };\n\n\t\tconst decodeCommand = (raw: unknown): CommandRecord =>\n\t\t\tdecodeUnknownSync(CommandRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel command',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.commandsFile,\n\t\t\t(raw) => decodeCommand(raw),\n\t\t\t{\n\t\t\t\tfromOffset: options.fromOffset ?? 'current',\n\t\t\t\tpollMillis: options.pollMillis,\n\t\t\t},\n\t\t);\n\n\t\tconst writeEvent = (record: EventRecord): Effect.Effect<void, CommandChannelError> =>\n\t\t\tappendRecord(paths.eventsFile, record);\n\n\t\tconst publishEvent = (event: unknown): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'engine',\n\t\t\t\t\tevent,\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst ack = (\n\t\t\tcorrelatesTo: string,\n\t\t\tdetail?: string,\n\t\t\tpayload?: unknown,\n\t\t): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'ack',\n\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t\t\t...(payload !== undefined ? { payload } : {}),\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst fail = (\n\t\t\tcorrelatesTo: string,\n\t\t\tmessage: string,\n\t\t\tdetail?: string,\n\t\t\tpayload?: unknown,\n\t\t): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'error',\n\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\tmessage,\n\t\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t\t\t...(payload !== undefined ? { payload } : {}),\n\t\t\t\t});\n\t\t\t});\n\n\t\treturn { commands, publishEvent, ack, fail };\n\t});\n"],"mappings":"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"}
|
|
@@ -61,8 +61,8 @@ const decodeLine = (path, line, decode) => parseJsonText(line, {
|
|
|
61
61
|
})));
|
|
62
62
|
/** Read newly-appended bytes from `state.offset` to current EOF. Updates
|
|
63
63
|
* `state` in place. Returns the freshly-completed lines. A trailing
|
|
64
|
-
* partial line (no newline yet) is preserved
|
|
65
|
-
* prepended on the next poll. */
|
|
64
|
+
* partial line (no newline yet) is preserved as raw bytes in
|
|
65
|
+
* `state.partial` and prepended on the next poll. */
|
|
66
66
|
const drainNewLines = (path, state) => Effect.try({
|
|
67
67
|
try: () => {
|
|
68
68
|
if (!existsSync(path)) return [];
|
|
@@ -72,11 +72,18 @@ const drainNewLines = (path, state) => Effect.try({
|
|
|
72
72
|
const fd = openSync(path, "r");
|
|
73
73
|
try {
|
|
74
74
|
const buf = Buffer.alloc(grow);
|
|
75
|
-
readSync(fd, buf, 0, grow, state.offset);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
state.partial
|
|
79
|
-
|
|
75
|
+
const bytesRead = readSync(fd, buf, 0, grow, state.offset);
|
|
76
|
+
if (bytesRead <= 0) return [];
|
|
77
|
+
state.offset += bytesRead;
|
|
78
|
+
const chunk = Buffer.concat([state.partial, buf.subarray(0, bytesRead)]);
|
|
79
|
+
const completedLines = [];
|
|
80
|
+
let lineStart = 0;
|
|
81
|
+
for (let i = 0; i < chunk.length; i++) if (chunk[i] === 10) {
|
|
82
|
+
if (i > lineStart) completedLines.push(chunk.toString("utf8", lineStart, i));
|
|
83
|
+
lineStart = i + 1;
|
|
84
|
+
}
|
|
85
|
+
state.partial = Buffer.from(chunk.subarray(lineStart));
|
|
86
|
+
return completedLines;
|
|
80
87
|
} finally {
|
|
81
88
|
closeSync(fd);
|
|
82
89
|
}
|
|
@@ -94,14 +101,19 @@ const drainNewLines = (path, state) => Effect.try({
|
|
|
94
101
|
* - `fromOffset === 'start'` replays every existing record then tails.
|
|
95
102
|
* - `fromOffset === number` resumes from a known byte offset.
|
|
96
103
|
*
|
|
97
|
-
*
|
|
98
|
-
* line attached so the consumer can pick a retry / skip
|
|
104
|
+
* By default decode errors are surfaced as Stream failures with the
|
|
105
|
+
* offending line attached so the consumer can pick a retry / skip
|
|
106
|
+
* policy. Pass `onDecodeError: 'skip'` to drop the bad line instead —
|
|
107
|
+
* required for NDJSON readers that race atomic-append writers (a
|
|
108
|
+
* truncated/corrupt mid-flight line is normal and MUST NOT kill the
|
|
109
|
+
* stream; see STYLE_GUIDE §20).
|
|
99
110
|
*
|
|
100
111
|
* The poll loop uses `Effect.sleep` between iterations, so cooperative
|
|
101
112
|
* scheduling is preserved (no busy loop).
|
|
102
113
|
*/
|
|
103
114
|
const tailRecords = (path, decode, options = {}) => Stream.unwrap(Effect.gen(function* () {
|
|
104
115
|
const pollMillis = options.pollMillis ?? 50;
|
|
116
|
+
const onDecodeError = options.onDecodeError ?? "fail";
|
|
105
117
|
yield* ensureFile(path);
|
|
106
118
|
const state = {
|
|
107
119
|
offset: yield* Effect.try({
|
|
@@ -116,7 +128,7 @@ const tailRecords = (path, decode, options = {}) => Stream.unwrap(Effect.gen(fun
|
|
|
116
128
|
cause
|
|
117
129
|
})
|
|
118
130
|
}),
|
|
119
|
-
partial:
|
|
131
|
+
partial: Buffer.alloc(0)
|
|
120
132
|
};
|
|
121
133
|
const pollOne = Effect.gen(function* () {
|
|
122
134
|
const lines = yield* drainNewLines(path, state);
|
|
@@ -125,12 +137,18 @@ const tailRecords = (path, decode, options = {}) => Stream.unwrap(Effect.gen(fun
|
|
|
125
137
|
return [];
|
|
126
138
|
}
|
|
127
139
|
const decoded = [];
|
|
128
|
-
for (const line of lines)
|
|
140
|
+
for (const line of lines) if (onDecodeError === "skip") {
|
|
141
|
+
const result = yield* decodeLine(path, line, decode).pipe(Effect.tapError((cause) => Effect.logDebug(`tail-records: skipped malformed line at ${path}`).pipe(Effect.annotateLogs({
|
|
142
|
+
line: cause.line,
|
|
143
|
+
cause: String(cause.cause)
|
|
144
|
+
}))), Effect.option);
|
|
145
|
+
if (result._tag === "Some") decoded.push(result.value);
|
|
146
|
+
} else decoded.push(yield* decodeLine(path, line, decode));
|
|
129
147
|
return decoded;
|
|
130
148
|
});
|
|
131
149
|
return Stream.fromEffectRepeat(pollOne).pipe(Stream.flatMap((records) => Stream.fromIterable(records)));
|
|
132
150
|
}));
|
|
133
151
|
//#endregion
|
|
134
|
-
export { appendRecord, ensureFile, tailRecords };
|
|
152
|
+
export { CommandChannelDecodeError, CommandChannelIoError, appendRecord, ensureFile, tailRecords };
|
|
135
153
|
|
|
136
154
|
//# sourceMappingURL=file-channel.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-channel.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/file-channel.ts"],"sourcesContent":["// Append-only NDJSON channel.\n//\n// One-record-per-line, atomic append via `fs.appendFileSync` with\n// `flag: 'a'` (POSIX guarantees the write is atomic for buffers under\n// PIPE_BUF; one JSON line is comfortably under that on every platform\n// we run). The tail-reader polls via offset bookkeeping — `fs.watch`\n// is unreliable across platforms for \"file grew\" notifications\n// (especially over NFS, which the cross-process protocol must remain\n// safe on per architecture § Cross-process safety protocol).\n//\n// Records are framed by a literal newline. A partial trailing line\n// (writer mid-flight when reader observed) is buffered until the next\n// poll iteration completes the line.\n\nimport {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { Data, Effect, Scope, Stream } from 'effect';\n\nimport { parseJsonText } from '../../runtime-decode.ts';\nimport { acquireStackLock } from '../stack-lock.ts';\nimport { runtimeControlLockPathForChannelFile } from './runtime-control-lock.ts';\n\n/** Tagged failure for any I/O error during append. */\nexport class CommandChannelIoError extends Data.TaggedError('CommandChannelIoError')<{\n\treadonly path: string;\n\treadonly stage: 'append' | 'open' | 'read' | 'stat' | 'init';\n\treadonly cause: unknown;\n}> {}\n\n/** Tagged failure when a tailed line cannot be decoded against the\n * record schema. The malformed line is preserved so consumers can\n * surface it for debugging without spamming retries. */\nexport class CommandChannelDecodeError extends Data.TaggedError('CommandChannelDecodeError')<{\n\treadonly path: string;\n\treadonly line: string;\n\treadonly cause: unknown;\n}> {}\n\nexport type CommandChannelError = CommandChannelIoError | CommandChannelDecodeError;\n\n/** Default tail poll interval. Chosen to be small enough for\n * interactive CLI responsiveness (a `down` ack returns within ~50ms)\n * while staying out of the way of process schedulers. */\nexport const DEFAULT_TAIL_POLL_MILLIS = 50;\n\nconst withRuntimeControlLock = <A>(\n\tpath: string,\n\tstage: CommandChannelIoError['stage'],\n\teffect: Effect.Effect<A, CommandChannelIoError>,\n): Effect.Effect<A, CommandChannelIoError> =>\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tyield* acquireStackLock(runtimeControlLockPathForChannelFile(path)).pipe(\n\t\t\t\tEffect.mapError((cause) => new CommandChannelIoError({ path, stage, cause })),\n\t\t\t);\n\t\t\treturn yield* effect;\n\t\t}),\n\t);\n\n/** Ensure the file exists. Idempotent. Creates parent dir if needed. */\nexport const ensureFile = (path: string): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'init',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tif (!existsSync(path)) {\n\t\t\t\t\twriteFileSync(path, '', { flag: 'a' });\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'init', cause }),\n\t\t}),\n\t);\n\n/** Append one record as a single NDJSON line. The trailing newline\n * ensures the next writer's append starts a fresh record. */\nexport const appendRecord = (\n\tpath: string,\n\trecord: unknown,\n): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'append',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tappendFileSync(path, `${JSON.stringify(record)}\\n`, { flag: 'a' });\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'append', cause }),\n\t\t}),\n\t);\n\n/** A line decoder. The channel I/O layer stays schema-agnostic so\n * callers can pick their own wire schema without this generic\n * carrying a `Schema.Decoder<unknown>` constraint. */\nexport type LineDecoder<A> = (raw: unknown) => A;\n\nconst decodeLine = <A>(\n\tpath: string,\n\tline: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<A, CommandChannelDecodeError> =>\n\tparseJsonText(line, {\n\t\tsource: path,\n\t\tmkError: (issue) => new CommandChannelDecodeError({ path, line, cause: issue.cause ?? issue }),\n\t}).pipe(\n\t\tEffect.flatMap((parsed) =>\n\t\t\tEffect.try({\n\t\t\t\ttry: () => decode(parsed),\n\t\t\t\tcatch: (cause) => new CommandChannelDecodeError({ path, line, cause }),\n\t\t\t}),\n\t\t),\n\t);\n\n/** Read the entire current contents into structured records. Useful at\n * startup to backfill state, and as a test seam. */\nexport const readAllRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<ReadonlyArray<A>, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tif (!existsSync(path)) return [];\n\t\tconst raw = yield* Effect.try({\n\t\t\ttry: () => {\n\t\t\t\tconst fd = openSync(path, 'r');\n\t\t\t\ttry {\n\t\t\t\t\tconst { size } = statSync(path);\n\t\t\t\t\tconst buf = Buffer.alloc(size);\n\t\t\t\t\treadSync(fd, buf, 0, size, 0);\n\t\t\t\t\treturn buf.toString('utf8');\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t\t});\n\t\tconst lines = raw.split('\\n').filter((l) => l.length > 0);\n\t\tconst out: A[] = [];\n\t\tfor (const line of lines) {\n\t\t\tout.push(yield* decodeLine(path, line, decode));\n\t\t}\n\t\treturn out;\n\t});\n\ninterface TailState {\n\toffset: number;\n\tpartial: string;\n}\n\n/** Read newly-appended bytes from `state.offset` to current EOF. Updates\n * `state` in place. Returns the freshly-completed lines. A trailing\n * partial line (no newline yet) is preserved in `state.partial` and\n * prepended on the next poll. */\nconst drainNewLines = (\n\tpath: string,\n\tstate: TailState,\n): Effect.Effect<ReadonlyArray<string>, CommandChannelIoError> =>\n\tEffect.try({\n\t\ttry: () => {\n\t\t\tif (!existsSync(path)) return [];\n\t\t\tconst stat = statSync(path);\n\t\t\tif (stat.size <= state.offset) return [];\n\t\t\tconst grow = stat.size - state.offset;\n\t\t\tconst fd = openSync(path, 'r');\n\t\t\ttry {\n\t\t\t\tconst buf = Buffer.alloc(grow);\n\t\t\t\treadSync(fd, buf, 0, grow, state.offset);\n\t\t\t\tstate.offset = stat.size;\n\t\t\t\tconst chunk = state.partial + buf.toString('utf8');\n\t\t\t\tconst parts = chunk.split('\\n');\n\t\t\t\tstate.partial = parts.pop() ?? '';\n\t\t\t\treturn parts.filter((l) => l.length > 0);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t},\n\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t});\n\n/** Tail a file as a Stream of decoded records. The Stream is bound to\n * the surrounding Scope: when the Scope closes, polling stops.\n *\n * - `fromOffset === 'current'` skips existing content; starts at EOF.\n * - `fromOffset === 'start'` replays every existing record then tails.\n * - `fromOffset === number` resumes from a known byte offset.\n *\n * Decode errors are surfaced as Stream failures with the offending\n * line attached so the consumer can pick a retry / skip policy.\n *\n * The poll loop uses `Effect.sleep` between iterations, so cooperative\n * scheduling is preserved (no busy loop).\n */\nexport const tailRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n\toptions: {\n\t\treadonly fromOffset?: 'start' | 'current' | number;\n\t\treadonly pollMillis?: number;\n\t} = {},\n): Stream.Stream<A, CommandChannelError, Scope.Scope> =>\n\tStream.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst pollMillis = options.pollMillis ?? DEFAULT_TAIL_POLL_MILLIS;\n\t\t\tyield* ensureFile(path);\n\t\t\tconst initialOffset = yield* Effect.try({\n\t\t\t\ttry: () => {\n\t\t\t\t\tif (options.fromOffset === 'start' || options.fromOffset === undefined) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (options.fromOffset === 'current') {\n\t\t\t\t\t\treturn existsSync(path) ? statSync(path).size : 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn options.fromOffset;\n\t\t\t\t},\n\t\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'stat', cause }),\n\t\t\t});\n\t\t\tconst state: TailState = { offset: initialOffset, partial: '' };\n\t\t\tconst pollOne: Effect.Effect<ReadonlyArray<A>, CommandChannelError> = Effect.gen(\n\t\t\t\tfunction* () {\n\t\t\t\t\tconst lines = yield* drainNewLines(path, state);\n\t\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\t\tyield* Effect.sleep(`${pollMillis} millis`);\n\t\t\t\t\t\treturn [] as ReadonlyArray<A>;\n\t\t\t\t\t}\n\t\t\t\t\tconst decoded: A[] = [];\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tdecoded.push(yield* decodeLine(path, line, decode));\n\t\t\t\t\t}\n\t\t\t\t\treturn decoded as ReadonlyArray<A>;\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn Stream.fromEffectRepeat(pollOne).pipe(\n\t\t\t\tStream.flatMap((records) => Stream.fromIterable(records)),\n\t\t\t);\n\t\t}),\n\t);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-channel.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/file-channel.ts"],"sourcesContent":["// Append-only NDJSON channel.\n//\n// One-record-per-line. Each append is `fs.appendFileSync` with\n// `flag: 'a'`, serialized cross-process by the runtime-control lock\n// (`withRuntimeControlLock`, an O_EXCL create/unlink). The LOCK — not\n// PIPE_BUF — is what guarantees a record never interleaves with a\n// concurrent append: a JSON record can exceed PIPE_BUF (the POSIX\n// atomic-append bound is only ~512 bytes on macOS, 4096 on Linux), so\n// relying on PIPE_BUF alone would let a large record tear under\n// contention. The tail-reader polls via offset bookkeeping — `fs.watch`\n// is unreliable across platforms for \"file grew\" notifications\n// (especially over NFS, which the cross-process protocol must remain\n// safe on per architecture § Cross-process safety protocol).\n//\n// Records are framed by a literal newline. A partial trailing line\n// (writer mid-flight when reader observed, or a short read bisecting a\n// record) is buffered as raw bytes until a later poll completes the\n// line — see `drainNewLines` for why the buffer is bytes, not a string.\n\nimport {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { Data, Effect, Scope, Stream } from 'effect';\n\nimport { parseJsonText } from '../../runtime-decode.ts';\nimport { acquireStackLock } from '../stack-lock.ts';\nimport { runtimeControlLockPathForChannelFile } from './runtime-control-lock.ts';\n\n/** Tagged failure for any I/O error during append. */\nexport class CommandChannelIoError extends Data.TaggedError('CommandChannelIoError')<{\n\treadonly path: string;\n\treadonly stage: 'append' | 'open' | 'read' | 'stat' | 'init';\n\treadonly cause: unknown;\n}> {}\n\n/** Tagged failure when a tailed line cannot be decoded against the\n * record schema. The malformed line is preserved so consumers can\n * surface it for debugging without spamming retries. */\nexport class CommandChannelDecodeError extends Data.TaggedError('CommandChannelDecodeError')<{\n\treadonly path: string;\n\treadonly line: string;\n\treadonly cause: unknown;\n}> {}\n\nexport type CommandChannelError = CommandChannelIoError | CommandChannelDecodeError;\n\n/** Default tail poll interval. Chosen to be small enough for\n * interactive CLI responsiveness (a `down` ack returns within ~50ms)\n * while staying out of the way of process schedulers. */\nexport const DEFAULT_TAIL_POLL_MILLIS = 50;\n\nconst withRuntimeControlLock = <A>(\n\tpath: string,\n\tstage: CommandChannelIoError['stage'],\n\teffect: Effect.Effect<A, CommandChannelIoError>,\n): Effect.Effect<A, CommandChannelIoError> =>\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tyield* acquireStackLock(runtimeControlLockPathForChannelFile(path)).pipe(\n\t\t\t\tEffect.mapError((cause) => new CommandChannelIoError({ path, stage, cause })),\n\t\t\t);\n\t\t\treturn yield* effect;\n\t\t}),\n\t);\n\n/** Ensure the file exists. Idempotent. Creates parent dir if needed. */\nexport const ensureFile = (path: string): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'init',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tif (!existsSync(path)) {\n\t\t\t\t\twriteFileSync(path, '', { flag: 'a' });\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'init', cause }),\n\t\t}),\n\t);\n\n/** Append one record as a single NDJSON line. The trailing newline\n * ensures the next writer's append starts a fresh record. */\nexport const appendRecord = (\n\tpath: string,\n\trecord: unknown,\n): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'append',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tappendFileSync(path, `${JSON.stringify(record)}\\n`, { flag: 'a' });\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'append', cause }),\n\t\t}),\n\t);\n\n/** A line decoder. The channel I/O layer stays schema-agnostic so\n * callers can pick their own wire schema without this generic\n * carrying a `Schema.Decoder<unknown>` constraint. */\nexport type LineDecoder<A> = (raw: unknown) => A;\n\nconst decodeLine = <A>(\n\tpath: string,\n\tline: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<A, CommandChannelDecodeError> =>\n\tparseJsonText(line, {\n\t\tsource: path,\n\t\tmkError: (issue) => new CommandChannelDecodeError({ path, line, cause: issue.cause ?? issue }),\n\t}).pipe(\n\t\tEffect.flatMap((parsed) =>\n\t\t\tEffect.try({\n\t\t\t\ttry: () => decode(parsed),\n\t\t\t\tcatch: (cause) => new CommandChannelDecodeError({ path, line, cause }),\n\t\t\t}),\n\t\t),\n\t);\n\n/** Read the entire current contents into structured records. Useful at\n * startup to backfill state, and as a test seam. */\nexport const readAllRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<ReadonlyArray<A>, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tif (!existsSync(path)) return [];\n\t\tconst raw = yield* Effect.try({\n\t\t\ttry: () => {\n\t\t\t\tconst fd = openSync(path, 'r');\n\t\t\t\ttry {\n\t\t\t\t\tconst { size } = statSync(path);\n\t\t\t\t\tconst buf = Buffer.alloc(size);\n\t\t\t\t\treadSync(fd, buf, 0, size, 0);\n\t\t\t\t\treturn buf.toString('utf8');\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t\t});\n\t\tconst lines = raw.split('\\n').filter((l) => l.length > 0);\n\t\tconst out: A[] = [];\n\t\tfor (const line of lines) {\n\t\t\tout.push(yield* decodeLine(path, line, decode));\n\t\t}\n\t\treturn out;\n\t});\n\ninterface TailState {\n\toffset: number;\n\t/** Raw, not-yet-decoded bytes of the trailing partial line. Held as a\n\t * Buffer rather than a decoded string so an incomplete UTF-8\n\t * multibyte sequence straddling a short-read boundary survives intact\n\t * until its continuation bytes arrive on the next poll. Decoding\n\t * per-read (the old behaviour) emitted U+FFFD for the split sequence\n\t * and lost the continuation bytes, which could corrupt a non-ASCII\n\t * record into invalid JSON that is then silently dropped. */\n\tpartial: Buffer;\n}\n\n/** Read newly-appended bytes from `state.offset` to current EOF. Updates\n * `state` in place. Returns the freshly-completed lines. A trailing\n * partial line (no newline yet) is preserved as raw bytes in\n * `state.partial` and prepended on the next poll. */\nconst drainNewLines = (\n\tpath: string,\n\tstate: TailState,\n): Effect.Effect<ReadonlyArray<string>, CommandChannelIoError> =>\n\tEffect.try({\n\t\ttry: () => {\n\t\t\tif (!existsSync(path)) return [];\n\t\t\tconst stat = statSync(path);\n\t\t\tif (stat.size <= state.offset) return [];\n\t\t\tconst grow = stat.size - state.offset;\n\t\t\tconst fd = openSync(path, 'r');\n\t\t\ttry {\n\t\t\t\tconst buf = Buffer.alloc(grow);\n\t\t\t\t// POSIX `read(2)` may short-return (especially across\n\t\t\t\t// filesystems like NFS); advance the offset by what was\n\t\t\t\t// ACTUALLY read, not what we asked for. A short read just\n\t\t\t\t// means the rest will arrive on the next poll iteration.\n\t\t\t\tconst bytesRead = readSync(fd, buf, 0, grow, state.offset);\n\t\t\t\tif (bytesRead <= 0) return [];\n\t\t\t\tstate.offset += bytesRead;\n\t\t\t\t// Split on the newline BYTE (0x0A) and decode only complete\n\t\t\t\t// lines. A short read can bisect a multibyte codepoint; by\n\t\t\t\t// keeping the trailing fragment as raw bytes we never decode\n\t\t\t\t// a half codepoint, so non-ASCII records reassemble byte-for-\n\t\t\t\t// byte across poll cycles. 0x0A never appears as a\n\t\t\t\t// continuation byte in UTF-8, so byte-level framing is sound.\n\t\t\t\tconst chunk = Buffer.concat([state.partial, buf.subarray(0, bytesRead)]);\n\t\t\t\tconst completedLines: string[] = [];\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tfor (let i = 0; i < chunk.length; i++) {\n\t\t\t\t\tif (chunk[i] === 0x0a) {\n\t\t\t\t\t\tif (i > lineStart) {\n\t\t\t\t\t\t\tcompletedLines.push(chunk.toString('utf8', lineStart, i));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlineStart = i + 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Retain the unterminated remainder (possibly an incomplete\n\t\t\t\t// multibyte sequence) as raw bytes for the next poll.\n\t\t\t\tstate.partial = Buffer.from(chunk.subarray(lineStart));\n\t\t\t\treturn completedLines;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t},\n\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t});\n\n/** Tail a file as a Stream of decoded records. The Stream is bound to\n * the surrounding Scope: when the Scope closes, polling stops.\n *\n * - `fromOffset === 'current'` skips existing content; starts at EOF.\n * - `fromOffset === 'start'` replays every existing record then tails.\n * - `fromOffset === number` resumes from a known byte offset.\n *\n * By default decode errors are surfaced as Stream failures with the\n * offending line attached so the consumer can pick a retry / skip\n * policy. Pass `onDecodeError: 'skip'` to drop the bad line instead —\n * required for NDJSON readers that race atomic-append writers (a\n * truncated/corrupt mid-flight line is normal and MUST NOT kill the\n * stream; see STYLE_GUIDE §20).\n *\n * The poll loop uses `Effect.sleep` between iterations, so cooperative\n * scheduling is preserved (no busy loop).\n */\nexport const tailRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n\toptions: {\n\t\treadonly fromOffset?: 'start' | 'current' | number;\n\t\treadonly pollMillis?: number;\n\t\treadonly onDecodeError?: 'fail' | 'skip';\n\t} = {},\n): Stream.Stream<A, CommandChannelError, Scope.Scope> =>\n\tStream.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst pollMillis = options.pollMillis ?? DEFAULT_TAIL_POLL_MILLIS;\n\t\t\tconst onDecodeError = options.onDecodeError ?? 'fail';\n\t\t\tyield* ensureFile(path);\n\t\t\tconst initialOffset = yield* Effect.try({\n\t\t\t\ttry: () => {\n\t\t\t\t\tif (options.fromOffset === 'start' || options.fromOffset === undefined) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (options.fromOffset === 'current') {\n\t\t\t\t\t\treturn existsSync(path) ? statSync(path).size : 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn options.fromOffset;\n\t\t\t\t},\n\t\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'stat', cause }),\n\t\t\t});\n\t\t\tconst state: TailState = { offset: initialOffset, partial: Buffer.alloc(0) };\n\t\t\tconst pollOne: Effect.Effect<ReadonlyArray<A>, CommandChannelError> = Effect.gen(\n\t\t\t\tfunction* () {\n\t\t\t\t\tconst lines = yield* drainNewLines(path, state);\n\t\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\t\tyield* Effect.sleep(`${pollMillis} millis`);\n\t\t\t\t\t\treturn [] as ReadonlyArray<A>;\n\t\t\t\t\t}\n\t\t\t\t\tconst decoded: A[] = [];\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tif (onDecodeError === 'skip') {\n\t\t\t\t\t\t\tconst result = yield* decodeLine(path, line, decode).pipe(\n\t\t\t\t\t\t\t\tEffect.tapError((cause) =>\n\t\t\t\t\t\t\t\t\tEffect.logDebug(`tail-records: skipped malformed line at ${path}`).pipe(\n\t\t\t\t\t\t\t\t\t\tEffect.annotateLogs({ line: cause.line, cause: String(cause.cause) }),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tEffect.option,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (result._tag === 'Some') {\n\t\t\t\t\t\t\t\tdecoded.push(result.value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdecoded.push(yield* decodeLine(path, line, decode));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn decoded as ReadonlyArray<A>;\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn Stream.fromEffectRepeat(pollOne).pipe(\n\t\t\t\tStream.flatMap((records) => Stream.fromIterable(records)),\n\t\t\t);\n\t\t}),\n\t);\n"],"mappings":"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"}
|
|
@@ -31,7 +31,12 @@ const EventRecordSchema = Schema.Union([
|
|
|
31
31
|
at: Schema.Number,
|
|
32
32
|
kind: Schema.Literal("ack"),
|
|
33
33
|
correlatesTo: Schema.String,
|
|
34
|
-
detail: Schema.optional(Schema.String)
|
|
34
|
+
detail: Schema.optional(Schema.String),
|
|
35
|
+
/** Optional structured payload (e.g. snapshot.capture's captured
|
|
36
|
+
* metadata). Lets the publisher's `awaitCompletion` carry a
|
|
37
|
+
* typed result through to the caller without the caller having
|
|
38
|
+
* to tail the engine event stream for completion. */
|
|
39
|
+
payload: Schema.optional(Schema.Unknown)
|
|
35
40
|
}),
|
|
36
41
|
Schema.Struct({
|
|
37
42
|
protocol: Schema.Literal(1),
|
|
@@ -40,7 +45,11 @@ const EventRecordSchema = Schema.Union([
|
|
|
40
45
|
kind: Schema.Literal("error"),
|
|
41
46
|
correlatesTo: Schema.String,
|
|
42
47
|
message: Schema.String,
|
|
43
|
-
detail: Schema.optional(Schema.String)
|
|
48
|
+
detail: Schema.optional(Schema.String),
|
|
49
|
+
/** Optional structured payload (e.g. snapshot.captureFailed's
|
|
50
|
+
* summary, snapshot.captureSkipped's reason). Mirrors the `ack`
|
|
51
|
+
* payload contract — see comment above. */
|
|
52
|
+
payload: Schema.optional(Schema.Unknown)
|
|
44
53
|
})
|
|
45
54
|
]);
|
|
46
55
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/protocol.ts"],"sourcesContent":["// Cross-process command channel — wire protocol.\n//\n// Architecture § Cross-process safety protocol § Command channel:\n// The CLI / TUI / programmable API publish typed commands to a\n// running supervisor; the supervisor publishes typed events back. The\n// channel is filesystem-backed (append-only NDJSON) so a peer\n// process can attach without IPC handshakes.\n//\n// Two files live under `<stackRoot>/`:\n// - `commands.ndjson` — caller appends; supervisor tails.\n// - `events.ndjson` — supervisor appends; callers tail.\n//\n// Each record is one JSON object per line, schema-versioned for forward\n// compatibility. Records carry a monotonically increasing sequence\n// number (per file) plus a per-record id that the publisher uses to\n// correlate event replies (e.g. `down` publishes a command, then waits\n// for an `event` carrying `correlatesTo: <same id>`).\n//\n// `EngineCommand` / `EngineEvent` payloads are NOT re-decoded on the\n// wire: the publisher API accepts typed commands, and both payload\n// kinds round-trip faithfully through JSON (closed string-tag-\n// discriminated unions with JSON-safe payloads). The channel validates\n// the envelope (`protocol`, `seq`, `id`, `correlatesTo`) and treats\n// the inner payload as `Schema.Unknown`. A future hardening pass can\n// promote the inner payloads to typed schemas without changing this\n// file's contract.\n\nimport { Schema } from 'effect';\n\n/** Protocol version. Bump on incompatible record-shape changes. */\nexport const COMMAND_CHANNEL_PROTOCOL_VERSION = 1 as const;\n\n/** Per-record envelope written to `commands.ndjson`. */\nexport const CommandRecordSchema = Schema.Struct({\n\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\tseq: Schema.Number,\n\tid: Schema.String,\n\tat: Schema.Number,\n\tpublisherPid: Schema.Number,\n\tpublisherHostname: Schema.String,\n\tcommand: Schema.Unknown,\n});\n\nexport type CommandRecord = Schema.Schema.Type<typeof CommandRecordSchema>;\n\n/** Per-record envelope written to `events.ndjson`. The supervisor emits\n * one record per `EngineEvent` plus synthetic `ack` / `error`\n * responses when the inner handler reports completion or failure for a\n * correlatable command.\n *\n * `correlatesTo` carries the originating `CommandRecord.id` for `ack`\n * / `error`. Other event kinds (engine events) carry `null`.\n */\nexport const EventRecordSchema = Schema.Union([\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('engine'),\n\t\tevent: Schema.Unknown,\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('ack'),\n\t\tcorrelatesTo: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('error'),\n\t\tcorrelatesTo: Schema.String,\n\t\tmessage: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t}),\n]);\n\nexport type EventRecord = Schema.Schema.Type<typeof EventRecordSchema>;\n"],"mappings":";;AAiCA,MAAa,sBAAsB,OAAO,OAAO;CAChD,UAAU,OAAO,QAAA,EAAyC;CAC1D,KAAK,OAAO;CACZ,IAAI,OAAO;CACX,IAAI,OAAO;CACX,cAAc,OAAO;CACrB,mBAAmB,OAAO;CAC1B,SAAS,OAAO;CAChB,CAAC;;;;;;;;;AAYF,MAAa,oBAAoB,OAAO,MAAM;CAC7C,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,SAAS;EAC9B,OAAO,OAAO;EACd,CAAC;CACF,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,MAAM;EAC3B,cAAc,OAAO;EACrB,QAAQ,OAAO,SAAS,OAAO,OAAO;
|
|
1
|
+
{"version":3,"file":"protocol.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/protocol.ts"],"sourcesContent":["// Cross-process command channel — wire protocol.\n//\n// Architecture § Cross-process safety protocol § Command channel:\n// The CLI / TUI / programmable API publish typed commands to a\n// running supervisor; the supervisor publishes typed events back. The\n// channel is filesystem-backed (append-only NDJSON) so a peer\n// process can attach without IPC handshakes.\n//\n// Two files live under `<stackRoot>/`:\n// - `commands.ndjson` — caller appends; supervisor tails.\n// - `events.ndjson` — supervisor appends; callers tail.\n//\n// Each record is one JSON object per line, schema-versioned for forward\n// compatibility. Records carry a monotonically increasing sequence\n// number (per file) plus a per-record id that the publisher uses to\n// correlate event replies (e.g. `down` publishes a command, then waits\n// for an `event` carrying `correlatesTo: <same id>`).\n//\n// `EngineCommand` / `EngineEvent` payloads are NOT re-decoded on the\n// wire: the publisher API accepts typed commands, and both payload\n// kinds round-trip faithfully through JSON (closed string-tag-\n// discriminated unions with JSON-safe payloads). The channel validates\n// the envelope (`protocol`, `seq`, `id`, `correlatesTo`) and treats\n// the inner payload as `Schema.Unknown`. A future hardening pass can\n// promote the inner payloads to typed schemas without changing this\n// file's contract.\n\nimport { Schema } from 'effect';\n\n/** Protocol version. Bump on incompatible record-shape changes. */\nexport const COMMAND_CHANNEL_PROTOCOL_VERSION = 1 as const;\n\n/** Per-record envelope written to `commands.ndjson`. */\nexport const CommandRecordSchema = Schema.Struct({\n\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\tseq: Schema.Number,\n\tid: Schema.String,\n\tat: Schema.Number,\n\tpublisherPid: Schema.Number,\n\tpublisherHostname: Schema.String,\n\tcommand: Schema.Unknown,\n});\n\nexport type CommandRecord = Schema.Schema.Type<typeof CommandRecordSchema>;\n\n/** Per-record envelope written to `events.ndjson`. The supervisor emits\n * one record per `EngineEvent` plus synthetic `ack` / `error`\n * responses when the inner handler reports completion or failure for a\n * correlatable command.\n *\n * `correlatesTo` carries the originating `CommandRecord.id` for `ack`\n * / `error`. Other event kinds (engine events) carry `null`.\n */\nexport const EventRecordSchema = Schema.Union([\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('engine'),\n\t\tevent: Schema.Unknown,\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('ack'),\n\t\tcorrelatesTo: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t\t/** Optional structured payload (e.g. snapshot.capture's captured\n\t\t * metadata). Lets the publisher's `awaitCompletion` carry a\n\t\t * typed result through to the caller without the caller having\n\t\t * to tail the engine event stream for completion. */\n\t\tpayload: Schema.optional(Schema.Unknown),\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('error'),\n\t\tcorrelatesTo: Schema.String,\n\t\tmessage: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t\t/** Optional structured payload (e.g. snapshot.captureFailed's\n\t\t * summary, snapshot.captureSkipped's reason). Mirrors the `ack`\n\t\t * payload contract — see comment above. */\n\t\tpayload: Schema.optional(Schema.Unknown),\n\t}),\n]);\n\nexport type EventRecord = Schema.Schema.Type<typeof EventRecordSchema>;\n"],"mappings":";;AAiCA,MAAa,sBAAsB,OAAO,OAAO;CAChD,UAAU,OAAO,QAAA,EAAyC;CAC1D,KAAK,OAAO;CACZ,IAAI,OAAO;CACX,IAAI,OAAO;CACX,cAAc,OAAO;CACrB,mBAAmB,OAAO;CAC1B,SAAS,OAAO;CAChB,CAAC;;;;;;;;;AAYF,MAAa,oBAAoB,OAAO,MAAM;CAC7C,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,SAAS;EAC9B,OAAO,OAAO;EACd,CAAC;CACF,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,MAAM;EAC3B,cAAc,OAAO;EACrB,QAAQ,OAAO,SAAS,OAAO,OAAO;;;;;EAKtC,SAAS,OAAO,SAAS,OAAO,QAAQ;EACxC,CAAC;CACF,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,QAAQ;EAC7B,cAAc,OAAO;EACrB,SAAS,OAAO;EAChB,QAAQ,OAAO,SAAS,OAAO,OAAO;;;;EAItC,SAAS,OAAO,SAAS,OAAO,QAAQ;EACxC,CAAC;CACF,CAAC"}
|
|
@@ -4,6 +4,6 @@ const RUNTIME_CONTROL_LOCK_FILE_SUFFIX = ".runtime-control.lock";
|
|
|
4
4
|
const runtimeControlLockPathForStackRoot = (stackRoot) => `${stackRoot}${RUNTIME_CONTROL_LOCK_FILE_SUFFIX}`;
|
|
5
5
|
const runtimeControlLockPathForChannelFile = (path) => runtimeControlLockPathForStackRoot(dirname(path));
|
|
6
6
|
//#endregion
|
|
7
|
-
export { runtimeControlLockPathForChannelFile, runtimeControlLockPathForStackRoot };
|
|
7
|
+
export { RUNTIME_CONTROL_LOCK_FILE_SUFFIX, runtimeControlLockPathForChannelFile, runtimeControlLockPathForStackRoot };
|
|
8
8
|
|
|
9
9
|
//# sourceMappingURL=runtime-control-lock.mjs.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./command-channel/protocol.mjs";
|
|
2
|
+
import "./liveness.mjs";
|
|
3
|
+
import "./stack-lock.mjs";
|
|
4
|
+
import "./command-channel/runtime-control-lock.mjs";
|
|
5
|
+
import "./command-channel/file-channel.mjs";
|
|
6
|
+
import "./command-channel/channel.mjs";
|
|
7
|
+
import "./command-channel/index.mjs";
|
|
8
|
+
import "./roster.mjs";
|
|
9
|
+
import "./snapshot-reservation.mjs";
|
|
10
|
+
import "./lock.mjs";
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Clock, Duration, Effect } from "effect";
|
|
2
|
+
//#region src/substrate/runtime/cross-process/live-clock.ts
|
|
3
|
+
/** Live system Clock instance. Used by the cross-process lock and the
|
|
4
|
+
* on-disk `stack.lock` acquire loop so the lock's wall-time invariants
|
|
5
|
+
* (acquire-timeout, exponential backoff, holder-liveness probes) hold
|
|
6
|
+
* even when the surrounding fiber has a `TestClock` installed. */
|
|
7
|
+
const LIVE_CLOCK = {
|
|
8
|
+
currentTimeMillis: Effect.sync(() => Date.now()),
|
|
9
|
+
currentTimeMillisUnsafe: () => Date.now(),
|
|
10
|
+
currentTimeNanos: Effect.sync(() => BigInt(Date.now()) * 1000000n),
|
|
11
|
+
currentTimeNanosUnsafe: () => BigInt(Date.now()) * 1000000n,
|
|
12
|
+
sleep: (duration) => Effect.callback((resume) => {
|
|
13
|
+
const ms = Duration.toMillis(duration);
|
|
14
|
+
if (ms <= 0) {
|
|
15
|
+
resume(Effect.void);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const handle = setTimeout(() => resume(Effect.void), ms);
|
|
19
|
+
return Effect.sync(() => clearTimeout(handle));
|
|
20
|
+
})
|
|
21
|
+
};
|
|
22
|
+
/** Provide the live Clock for `effect`, overriding any inherited
|
|
23
|
+
* `TestClock` for the duration of the wrapped effect.
|
|
24
|
+
*
|
|
25
|
+
* SCOPE: applied ONLY to the acquire/release infrastructure (OS-advisory
|
|
26
|
+
* lock retry backoff, holder-liveness PID probes). Callers MUST NOT
|
|
27
|
+
* wrap user-supplied body effects — those should inherit the caller's
|
|
28
|
+
* clock so TestClock-driven tests can adjust virtual time inside
|
|
29
|
+
* critical sections. See `cross-process/lock.ts` for the discipline. */
|
|
30
|
+
const underLiveClock = (effect) => Effect.provideService(effect, Clock.Clock, LIVE_CLOCK);
|
|
31
|
+
//#endregion
|
|
32
|
+
export { underLiveClock };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=live-clock.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-clock.mjs","names":[],"sources":["../../../../src/substrate/runtime/cross-process/live-clock.ts"],"sourcesContent":["// Live wall-clock instance — shared between the cross-process lock and\n// the on-disk `stack.lock` acquire loop.\n//\n// Cross-process safety is fundamentally a wall-time property: two OS\n// processes can't share a virtual test clock, and the holder-liveness\n// PID/start-time probes that reclaim stale locks measure real time. The\n// `underLiveClock` wrapper pins acquire/release infrastructure (lock\n// retry backoff, holder-liveness probes) to this clock so the lock\n// primitive's semantics hold even when callers install a `TestClock`.\n//\n// SCOPE: applied ONLY to acquire/release infrastructure. The user body\n// inherits the caller's clock so TestClock-driven tests can virtualize\n// `Effect.sleep` inside lock-protected sections without being forced to\n// wall time. The lock primitives themselves still get the live clock\n// for their wall-time invariants. The SHORT-CRITICAL-SECTION discipline\n// from the architecture is enforced by code review rather than by\n// forcing wall time on every body.\n\nimport { Clock, Duration, Effect } from 'effect';\n\n/** Live system Clock instance. Used by the cross-process lock and the\n * on-disk `stack.lock` acquire loop so the lock's wall-time invariants\n * (acquire-timeout, exponential backoff, holder-liveness probes) hold\n * even when the surrounding fiber has a `TestClock` installed. */\nexport const LIVE_CLOCK: Clock.Clock = {\n\tcurrentTimeMillis: Effect.sync(() => Date.now()),\n\tcurrentTimeMillisUnsafe: () => Date.now(),\n\tcurrentTimeNanos: Effect.sync(() => BigInt(Date.now()) * 1_000_000n),\n\tcurrentTimeNanosUnsafe: () => BigInt(Date.now()) * 1_000_000n,\n\tsleep: (duration: Duration.Duration) =>\n\t\tEffect.callback<void>((resume) => {\n\t\t\tconst ms = Duration.toMillis(duration);\n\t\t\tif (ms <= 0) {\n\t\t\t\tresume(Effect.void);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst handle = setTimeout(() => resume(Effect.void), ms);\n\t\t\treturn Effect.sync(() => clearTimeout(handle));\n\t\t}),\n};\n\n/** Provide the live Clock for `effect`, overriding any inherited\n * `TestClock` for the duration of the wrapped effect.\n *\n * SCOPE: applied ONLY to the acquire/release infrastructure (OS-advisory\n * lock retry backoff, holder-liveness PID probes). Callers MUST NOT\n * wrap user-supplied body effects — those should inherit the caller's\n * clock so TestClock-driven tests can adjust virtual time inside\n * critical sections. See `cross-process/lock.ts` for the discipline. */\nexport const underLiveClock = <A, E, R>(\n\teffect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, E, R> => Effect.provideService(effect, Clock.Clock, LIVE_CLOCK);\n"],"mappings":";;;;;;AAwBA,MAAa,aAA0B;CACtC,mBAAmB,OAAO,WAAW,KAAK,KAAK,CAAC;CAChD,+BAA+B,KAAK,KAAK;CACzC,kBAAkB,OAAO,WAAW,OAAO,KAAK,KAAK,CAAC,GAAG,SAAW;CACpE,8BAA8B,OAAO,KAAK,KAAK,CAAC,GAAG;CACnD,QAAQ,aACP,OAAO,UAAgB,WAAW;EACjC,MAAM,KAAK,SAAS,SAAS,SAAS;AACtC,MAAI,MAAM,GAAG;AACZ,UAAO,OAAO,KAAK;AACnB;;EAED,MAAM,SAAS,iBAAiB,OAAO,OAAO,KAAK,EAAE,GAAG;AACxD,SAAO,OAAO,WAAW,aAAa,OAAO,CAAC;GAC7C;CACH;;;;;;;;;AAUD,MAAa,kBACZ,WAC4B,OAAO,eAAe,QAAQ,MAAM,OAAO,WAAW"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selfPid } from "./self-pid.mjs";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
3
|
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { hostname } from "node:os";
|
|
4
|
-
|
|
5
|
+
//#region src/substrate/runtime/cross-process/liveness.ts
|
|
5
6
|
/** Cheap "send signal 0" pid liveness. ESRCH → dead; EPERM → alive
|
|
6
7
|
* (foreign-user pid on shared dev hosts). Any other errno is
|
|
7
8
|
* conservatively treated as dead so we don't refuse cleanup on exotic
|
|
@@ -21,9 +22,28 @@ const isPidAlive = (pid) => {
|
|
|
21
22
|
* a previously-recorded sibling stamp.
|
|
22
23
|
*
|
|
23
24
|
* Architecture § Cross-process safety protocol §3 — `ps -o lstart` on
|
|
24
|
-
* macOS/Linux, `tasklist` confirmation on Windows.
|
|
25
|
-
|
|
25
|
+
* macOS/Linux, `tasklist` confirmation on Windows.
|
|
26
|
+
*
|
|
27
|
+
* Pass `cache` to reuse a previously-probed result across a sweep
|
|
28
|
+
* pass — same `pid` only forks the underlying utility once. */
|
|
29
|
+
const processStartTime = (pid, cache) => {
|
|
26
30
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
31
|
+
if (pid === selfPid()) {
|
|
32
|
+
if (ownStartTimeCache !== UNSET) return ownStartTimeCache;
|
|
33
|
+
const probed = probeStartTimeUncached(pid);
|
|
34
|
+
if (probed !== null) ownStartTimeCache = probed;
|
|
35
|
+
return probed;
|
|
36
|
+
}
|
|
37
|
+
if (cache?.has(pid)) return cache.get(pid) ?? null;
|
|
38
|
+
const probed = probeStartTimeUncached(pid);
|
|
39
|
+
cache?.set(pid, probed);
|
|
40
|
+
return probed;
|
|
41
|
+
};
|
|
42
|
+
const UNSET = Symbol("UNSET");
|
|
43
|
+
let ownStartTimeCache = UNSET;
|
|
44
|
+
/** Inner probe — always forks the platform utility. Split out so the
|
|
45
|
+
* cache branch in `processStartTime` stays a single read/write. */
|
|
46
|
+
const probeStartTimeUncached = (pid) => {
|
|
27
47
|
if (process.platform === "win32") try {
|
|
28
48
|
const out = execFileSync("tasklist", [
|
|
29
49
|
"/fi",
|
|
@@ -88,32 +108,54 @@ const hashStartTimeStamp = (stamp) => {
|
|
|
88
108
|
*
|
|
89
109
|
* Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers
|
|
90
110
|
* compose under spans. */
|
|
91
|
-
const checkHolderLiveness = Effect.fn("cross-process.liveness.checkHolderLiveness")(function* (holder, ownHost = hostname()) {
|
|
111
|
+
const checkHolderLiveness = Effect.fn("cross-process.liveness.checkHolderLiveness")(function* (holder, ownHost = hostname(), cache) {
|
|
92
112
|
if (holder.hostname !== ownHost) return "alive";
|
|
93
113
|
yield* Effect.annotateCurrentSpan({
|
|
94
114
|
"devstack.holder.pid": holder.pid,
|
|
95
115
|
"devstack.holder.host": holder.hostname
|
|
96
116
|
});
|
|
97
117
|
if (!isPidAlive(holder.pid)) return "dead";
|
|
98
|
-
const probedStart = processStartTime(holder.pid);
|
|
118
|
+
const probedStart = processStartTime(holder.pid, cache);
|
|
99
119
|
if (probedStart === null) return "alive";
|
|
120
|
+
if (holder.startTime === null) return "alive";
|
|
100
121
|
return probedStart === holder.startTime ? "alive" : "dead";
|
|
101
122
|
});
|
|
102
123
|
/** Build a holder snapshot for THIS process. The intent defaults to
|
|
103
124
|
* `'normal'`; the snapshot-reservation flow flips it to `'snapshot'`
|
|
104
|
-
* under the stack lock and back when the reservation releases.
|
|
125
|
+
* under the stack lock and back when the reservation releases.
|
|
126
|
+
*
|
|
127
|
+
* A `null` `startTime` propagates verbatim — readers (`isOwnEntry`
|
|
128
|
+
* in `roster.ts`, `checkHolderLiveness` above) honor the null-
|
|
129
|
+
* conservative branch. Writing `0` for "unprobable" was the prior
|
|
130
|
+
* shape and caused a false-dead harvest: a subsequent probe yielding
|
|
131
|
+
* a real stamp would mismatch the recorded `0` and the process could
|
|
132
|
+
* no longer recognize its own entry. */
|
|
105
133
|
const ownHolder = (intent = "normal") => {
|
|
106
|
-
const pid =
|
|
134
|
+
const pid = selfPid();
|
|
107
135
|
return {
|
|
108
136
|
pid,
|
|
109
|
-
startTime: processStartTime(pid)
|
|
137
|
+
startTime: processStartTime(pid),
|
|
110
138
|
hostname: hostname(),
|
|
111
139
|
claimedAt: Date.now(),
|
|
112
140
|
heartbeatAt: Date.now(),
|
|
113
141
|
intent
|
|
114
142
|
};
|
|
115
143
|
};
|
|
144
|
+
/** A per-sweep liveness scope. Callers `yield* LivenessProbeScope`
|
|
145
|
+
* once per sweep (typically inside an `Effect.scoped` block that
|
|
146
|
+
* wraps the loop) and call `probeStartTime` / `probeHolderLiveness`
|
|
147
|
+
* on each holder without threading the Map manually. */
|
|
148
|
+
var LivenessProbeScope = class extends Context.Service()("@devstack/substrate/cross-process/LivenessProbeScope") {};
|
|
149
|
+
/** Construct a fresh `LivenessProbeScope` whose underlying cache is
|
|
150
|
+
* private to this layer — every yielding scope gets its own Map. */
|
|
151
|
+
const layerLivenessProbeScope = Layer.effect(LivenessProbeScope, Effect.sync(() => {
|
|
152
|
+
const cache = /* @__PURE__ */ new Map();
|
|
153
|
+
return LivenessProbeScope.of({
|
|
154
|
+
probeStartTime: (pid) => processStartTime(pid, cache),
|
|
155
|
+
probeHolderLiveness: (holder, ownHost = hostname()) => checkHolderLiveness(holder, ownHost, cache)
|
|
156
|
+
});
|
|
157
|
+
}));
|
|
116
158
|
//#endregion
|
|
117
|
-
export { checkHolderLiveness, isPidAlive, ownHolder, processStartTime };
|
|
159
|
+
export { LivenessProbeScope, checkHolderLiveness, isPidAlive, layerLivenessProbeScope, ownHolder, processStartTime };
|
|
118
160
|
|
|
119
161
|
//# sourceMappingURL=liveness.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"liveness.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/liveness.ts"],"sourcesContent":["// PID + start-time liveness check.\n//\n// Architecture § Cross-process safety protocol § Claim protocol step 3:\n// \"process exists AND its start-time (as read from `/proc/<pid>/stat` on\n// Linux, `ps -o lstart` on macOS, or equivalent) matches the recorded\n// `startTime`.\"\n//\n// Foreign-host entries are treated as alive (NFS-safe conservative\n// default). On the same host, two checks combine: `kill(pid, 0)`\n// determines pid-in-use; `ps -o lstart` confirms it's the SAME process\n// (defending against pid reuse on long-uptime machines).\n//\n// This module is the ONLY place in the substrate that calls into\n// `process.kill` and shells out for start times. Roster/snapshot\n// reservation/stack-lock all consult it through the typed predicates.\n\nimport { execFileSync } from 'node:child_process';\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Data, Effect } from 'effect';\n\nimport type { RosterHolder } from '../../cross-process.ts';\n\n/** Tagged failure when the start-time probe itself errored unexpectedly\n * (timeout, missing utility, etc.). Distinguished from \"pid absent\",\n * which is signaled by `processStartTime` returning `null`. */\nexport class StartTimeProbeError extends Data.TaggedError('StartTimeProbeError')<{\n\treadonly pid: number;\n\treadonly cause: unknown;\n}> {}\n\n/** Cheap \"send signal 0\" pid liveness. ESRCH → dead; EPERM → alive\n * (foreign-user pid on shared dev hosts). Any other errno is\n * conservatively treated as dead so we don't refuse cleanup on exotic\n * platforms. */\nexport const isPidAlive = (pid: number): boolean => {\n\tif (!Number.isFinite(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn code === 'EPERM';\n\t}\n};\n\n/** Best-effort start-time stamp for `pid`. Returns `null` when the\n * process is gone OR the platform can't produce a time. The stamp\n * itself is opaque text — its only contract is bytewise equality with\n * a previously-recorded sibling stamp.\n *\n * Architecture § Cross-process safety protocol §3 — `ps -o lstart` on\n * macOS/Linux, `tasklist` confirmation on Windows. */\nexport const processStartTime = (pid: number): number | null => {\n\tif (!Number.isFinite(pid) || pid <= 0) return null;\n\tif (process.platform === 'win32') {\n\t\ttry {\n\t\t\tconst out = execFileSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\ttimeout: 2000,\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t});\n\t\t\t// Windows path: just hash the line. Windows PID reuse on uptime\n\t\t\t// is a known v1 trade-off; the architecture says the protocol\n\t\t\t// stays POSIX-first.\n\t\t\treturn out.trim().startsWith('\"') ? hashStartTimeStamp(out.trim()) : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\ttry {\n\t\tconst out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 2000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? hashStartTimeStamp(trimmed) : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Stable hash of a start-time string. Reduces the textual `ps -o\n * lstart=` output to a single number so the roster carries a fixed-\n * width integer (the architecture's `startTime` field is `number`).\n *\n * FNV-1a 32-bit; collisions are negligible at the (pid, host, second)\n * granularity we ever compare. */\nconst hashStartTimeStamp = (stamp: string): number => {\n\tlet h = 2166136261;\n\tfor (let i = 0; i < stamp.length; i++) {\n\t\th ^= stamp.charCodeAt(i);\n\t\th = (h * 16777619) >>> 0;\n\t}\n\t// Stay inside Number.MAX_SAFE_INTEGER's positive 32-bit range so the\n\t// number round-trips through JSON without precision loss.\n\treturn h >>> 0;\n};\n\n/** Liveness probe for a roster holder. Used by the claim-protocol\n * sweep AND the snapshot-reservation orphan check.\n *\n * Discipline:\n * - Foreign-host (`hostname` differs from our own) → ALWAYS alive\n * (NFS-safe default; cross-host pid comparisons are meaningless).\n * - Same-host: pid must be live AND start-time must match.\n *\n * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers\n * compose under spans. */\nexport const checkHolderLiveness = Effect.fn('cross-process.liveness.checkHolderLiveness')(\n\tfunction* (holder: RosterHolder, ownHost: string = nodeHostname()) {\n\t\t// Foreign-host: NFS-safe — we can't verify, assume alive.\n\t\tif (holder.hostname !== ownHost) {\n\t\t\treturn 'alive' as const;\n\t\t}\n\t\tyield* Effect.annotateCurrentSpan({\n\t\t\t'devstack.holder.pid': holder.pid,\n\t\t\t'devstack.holder.host': holder.hostname,\n\t\t});\n\t\tif (!isPidAlive(holder.pid)) return 'dead' as const;\n\t\tconst probedStart = processStartTime(holder.pid);\n\t\t// pid alive but no stamp probable → conservative: ALIVE\n\t\t// (we have nothing to dispute the recorded startTime with).\n\t\tif (probedStart === null) return 'alive' as const;\n\t\treturn probedStart === holder.startTime ? ('alive' as const) : ('dead' as const);\n\t},\n);\n\n/** Build a holder snapshot for THIS process. The intent defaults to\n * `'normal'`; the snapshot-reservation flow flips it to `'snapshot'`\n * under the stack lock and back when the reservation releases. */\nexport const ownHolder = (intent: 'normal' | 'snapshot' = 'normal'): RosterHolder => {\n\tconst pid = process.pid;\n\tconst startTime = processStartTime(pid) ?? 0;\n\treturn {\n\t\tpid,\n\t\tstartTime,\n\t\thostname: nodeHostname(),\n\t\tclaimedAt: Date.now(),\n\t\theartbeatAt: Date.now(),\n\t\tintent,\n\t};\n};\n"],"mappings":";;;AA0ByC,KAAK,YAAY,sBAAsB;;;;;AAShF,MAAa,cAAc,QAAyB;AACnD,KAAI,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,EAAG,QAAO;AAC9C,KAAI;AACH,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACC,KAAK;AAEb,SADc,IAA8B,SAC5B;;;;;;;;;;AAWlB,MAAa,oBAAoB,QAA+B;AAC/D,KAAI,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,EAAG,QAAO;AAC9C,KAAI,QAAQ,aAAa,QACxB,KAAI;EACH,MAAM,MAAM,aAAa,YAAY;GAAC;GAAO,UAAU;GAAO;GAAO;GAAO;GAAM,EAAE;GACnF,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,CAAC;AAIF,SAAO,IAAI,MAAM,CAAC,WAAW,KAAI,GAAG,mBAAmB,IAAI,MAAM,CAAC,GAAG;SAC9D;AACP,SAAO;;AAGT,KAAI;EAMH,MAAM,UALM,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,IAAI;GAAC,EAAE;GACpE,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,CACkB,CAAC,MAAM;AAC1B,SAAO,QAAQ,SAAS,IAAI,mBAAmB,QAAQ,GAAG;SACnD;AACP,SAAO;;;;;;;;;AAUT,MAAM,sBAAsB,UAA0B;CACrD,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACtC,OAAK,MAAM,WAAW,EAAE;AACxB,MAAK,IAAI,aAAc;;AAIxB,QAAO,MAAM;;;;;;;;;;;;AAad,MAAa,sBAAsB,OAAO,GAAG,6CAA6C,CACzF,WAAW,QAAsB,UAAkBA,UAAc,EAAE;AAElE,KAAI,OAAO,aAAa,QACvB,QAAO;AAER,QAAO,OAAO,oBAAoB;EACjC,uBAAuB,OAAO;EAC9B,wBAAwB,OAAO;EAC/B,CAAC;AACF,KAAI,CAAC,WAAW,OAAO,IAAI,CAAE,QAAO;CACpC,MAAM,cAAc,iBAAiB,OAAO,IAAI;AAGhD,KAAI,gBAAgB,KAAM,QAAO;AACjC,QAAO,gBAAgB,OAAO,YAAa,UAAqB;EAEjE;;;;AAKD,MAAa,aAAa,SAAgC,aAA2B;CACpF,MAAM,MAAM,QAAQ;AAEpB,QAAO;EACN;EACA,WAHiB,iBAAiB,IAAI,IAAI;EAI1C,UAAUA,UAAc;EACxB,WAAW,KAAK,KAAK;EACrB,aAAa,KAAK,KAAK;EACvB;EACA"}
|
|
1
|
+
{"version":3,"file":"liveness.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/liveness.ts"],"sourcesContent":["// PID + start-time liveness check.\n//\n// Architecture § Cross-process safety protocol § Claim protocol step 3:\n// \"process exists AND its start-time (as read from `/proc/<pid>/stat` on\n// Linux, `ps -o lstart` on macOS, or equivalent) matches the recorded\n// `startTime`.\"\n//\n// Foreign-host entries are treated as alive (NFS-safe conservative\n// default). On the same host, two checks combine: `kill(pid, 0)`\n// determines pid-in-use; `ps -o lstart` confirms it's the SAME process\n// (defending against pid reuse on long-uptime machines).\n//\n// This module is the ONLY place in the substrate that calls into\n// `process.kill` and shells out for start times. Roster/snapshot\n// reservation/stack-lock all consult it through the typed predicates.\n\nimport { execFileSync } from 'node:child_process';\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { RosterHolder } from '../../cross-process.ts';\nimport { selfPid } from './self-pid.ts';\n\n/** Cheap \"send signal 0\" pid liveness. ESRCH → dead; EPERM → alive\n * (foreign-user pid on shared dev hosts). Any other errno is\n * conservatively treated as dead so we don't refuse cleanup on exotic\n * platforms. */\nexport const isPidAlive = (pid: number): boolean => {\n\tif (!Number.isFinite(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn code === 'EPERM';\n\t}\n};\n\n/** Per-sweep cache mapping `pid → start-time stamp` (or `null` when the\n * pid is gone/unprobable). Callers that issue many probes in a single\n * sweep (roster step-3 stale-eviction, container-claim ledger prune,\n * etc.) instantiate one cache per pass and thread it through every\n * probe call so the same pid forks `ps`/`tasklist` AT MOST once per\n * sweep. Single-shot callers omit the cache and get the no-cache\n * behavior.\n *\n * Discriminator is `Map.has(pid)` — a cached `null` is a real result\n * (the pid is missing) and skipping it would re-fork pointlessly. */\nexport type LivenessCache = Map<number, number | null>;\n\n/** Best-effort start-time stamp for `pid`. Returns `null` when the\n * process is gone OR the platform can't produce a time. The stamp\n * itself is opaque text — its only contract is bytewise equality with\n * a previously-recorded sibling stamp.\n *\n * Architecture § Cross-process safety protocol §3 — `ps -o lstart` on\n * macOS/Linux, `tasklist` confirmation on Windows.\n *\n * Pass `cache` to reuse a previously-probed result across a sweep\n * pass — same `pid` only forks the underlying utility once. */\nexport const processStartTime = (pid: number, cache?: LivenessCache): number | null => {\n\tif (!Number.isFinite(pid) || pid <= 0) return null;\n\t// Our own pid's startTime never changes during the process\n\t// lifetime — cache the first SUCCESSFUL probe forever. Eliminates\n\t// the `ps` spawn under high in-process contention (e.g. N fibers\n\t// fighting over a single cross-process lock all probing each\n\t// other's shared pid). Without this cache, 8 concurrent fibers\n\t// each fork `ps -o lstart` with a 2s timeout, compounding into\n\t// seconds of latency that exhaust the claim budget (review fix\n\t// phase 22f reclaim-stress reproducer caught it).\n\t//\n\t// Only a non-null result is memoized: a `null` here means the\n\t// probe transiently FAILED (spawn hiccup / 2s timeout under load),\n\t// not that the pid is gone — it's our own pid, which is alive by\n\t// definition. Caching that null forever would surrender this\n\t// process's PID-reuse protection (it would write `startTime: null`\n\t// to disk for its whole lifetime) on a single flaky fork. Leaving\n\t// the cache UNSET re-probes on the next call; an occasional extra\n\t// `ps` on a previously-failing probe is negligible versus losing\n\t// reuse protection for the lifetime.\n\tif (pid === selfPid()) {\n\t\tif (ownStartTimeCache !== UNSET) return ownStartTimeCache;\n\t\tconst probed = probeStartTimeUncached(pid);\n\t\tif (probed !== null) ownStartTimeCache = probed;\n\t\treturn probed;\n\t}\n\tif (cache?.has(pid)) {\n\t\t// Map.get is `T | undefined` — but `has` is true, so the value\n\t\t// is one of the cached results (a `number` or `null`).\n\t\treturn cache.get(pid) ?? null;\n\t}\n\tconst probed = probeStartTimeUncached(pid);\n\tcache?.set(pid, probed);\n\treturn probed;\n};\n\nconst UNSET: unique symbol = Symbol('UNSET');\n// Only a successful (non-null) self-pid probe is ever stored here; a\n// failed probe leaves this UNSET so the next call re-probes.\nlet ownStartTimeCache: number | typeof UNSET = UNSET;\n\n/** Inner probe — always forks the platform utility. Split out so the\n * cache branch in `processStartTime` stays a single read/write. */\nconst probeStartTimeUncached = (pid: number): number | null => {\n\tif (process.platform === 'win32') {\n\t\ttry {\n\t\t\tconst out = execFileSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\ttimeout: 2000,\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t});\n\t\t\t// Windows path: just hash the line. Windows PID reuse on uptime\n\t\t\t// is a known v1 trade-off; the architecture says the protocol\n\t\t\t// stays POSIX-first.\n\t\t\treturn out.trim().startsWith('\"') ? hashStartTimeStamp(out.trim()) : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\ttry {\n\t\tconst out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 2000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? hashStartTimeStamp(trimmed) : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Stable hash of a start-time string. Reduces the textual `ps -o\n * lstart=` output to a single number so the roster carries a fixed-\n * width integer (the architecture's `startTime` field is `number`).\n *\n * FNV-1a 32-bit; collisions are negligible at the (pid, host, second)\n * granularity we ever compare. */\nconst hashStartTimeStamp = (stamp: string): number => {\n\tlet h = 2166136261;\n\tfor (let i = 0; i < stamp.length; i++) {\n\t\th ^= stamp.charCodeAt(i);\n\t\th = (h * 16777619) >>> 0;\n\t}\n\t// Stay inside Number.MAX_SAFE_INTEGER's positive 32-bit range so the\n\t// number round-trips through JSON without precision loss.\n\treturn h >>> 0;\n};\n\n/** Liveness probe for a roster holder. Used by the claim-protocol\n * sweep AND the snapshot-reservation orphan check.\n *\n * Discipline:\n * - Foreign-host (`hostname` differs from our own) → ALWAYS alive\n * (NFS-safe default; cross-host pid comparisons are meaningless).\n * - Same-host: pid must be live AND start-time must match.\n *\n * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers\n * compose under spans. */\nexport const checkHolderLiveness = Effect.fn('cross-process.liveness.checkHolderLiveness')(\n\tfunction* (holder: RosterHolder, ownHost: string = nodeHostname(), cache?: LivenessCache) {\n\t\t// Foreign-host: NFS-safe — we can't verify, assume alive.\n\t\tif (holder.hostname !== ownHost) {\n\t\t\treturn 'alive' as const;\n\t\t}\n\t\tyield* Effect.annotateCurrentSpan({\n\t\t\t'devstack.holder.pid': holder.pid,\n\t\t\t'devstack.holder.host': holder.hostname,\n\t\t});\n\t\tif (!isPidAlive(holder.pid)) return 'dead' as const;\n\t\tconst probedStart = processStartTime(holder.pid, cache);\n\t\t// pid alive but no stamp probable → conservative: ALIVE\n\t\t// (we have nothing to dispute the recorded startTime with).\n\t\tif (probedStart === null) return 'alive' as const;\n\t\t// Holder recorded a `null` startTime (writer's platform couldn't\n\t\t// probe at the time). The (pid, hostname) pair carries the\n\t\t// identity; same conservative policy as the probedStart-null\n\t\t// branch. Mismatching a real probed stamp against a recorded\n\t\t// `null` would otherwise harvest live holders as \"dead\".\n\t\tif (holder.startTime === null) return 'alive' as const;\n\t\treturn probedStart === holder.startTime ? ('alive' as const) : ('dead' as const);\n\t},\n);\n\n/** Build a holder snapshot for THIS process. The intent defaults to\n * `'normal'`; the snapshot-reservation flow flips it to `'snapshot'`\n * under the stack lock and back when the reservation releases.\n *\n * A `null` `startTime` propagates verbatim — readers (`isOwnEntry`\n * in `roster.ts`, `checkHolderLiveness` above) honor the null-\n * conservative branch. Writing `0` for \"unprobable\" was the prior\n * shape and caused a false-dead harvest: a subsequent probe yielding\n * a real stamp would mismatch the recorded `0` and the process could\n * no longer recognize its own entry. */\nexport const ownHolder = (intent: 'normal' | 'snapshot' = 'normal'): RosterHolder => {\n\tconst pid = selfPid();\n\tconst startTime = processStartTime(pid);\n\treturn {\n\t\tpid,\n\t\tstartTime,\n\t\thostname: nodeHostname(),\n\t\tclaimedAt: Date.now(),\n\t\theartbeatAt: Date.now(),\n\t\tintent,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// LivenessProbeScope — per-sweep cache, lifted out of the Map parameter.\n// -----------------------------------------------------------------------------\n//\n// The bare `LivenessCache = Map<pid, stamp | null>` was threaded through\n// `roster.sweepStaleHolders` and `liveContainerClaims` as an explicit\n// parameter, which is fine for two sites but doesn't scale to the three\n// other sweep loops (dispatch routes, doctor probes, lifecycle-prune)\n// that haven't migrated yet. Promoting the cache into a service lets a\n// sweep loop `yield* LivenessProbeScope` once and call the probe\n// methods without re-passing the Map. The optional-Map params on\n// `processStartTime` / `checkHolderLiveness` stay so unmigrated callers\n// can keep working from outside an Effect.\n\n/** Methods a per-sweep scope exposes — the captured cache backs both\n * so the same `pid` forks `ps`/`tasklist` at most once per scope. */\nexport interface LivenessProbeScopeShape {\n\treadonly probeStartTime: (pid: number) => number | null;\n\treadonly probeHolderLiveness: (\n\t\tholder: RosterHolder,\n\t\townHost?: string,\n\t) => Effect.Effect<'alive' | 'dead'>;\n}\n\n/** A per-sweep liveness scope. Callers `yield* LivenessProbeScope`\n * once per sweep (typically inside an `Effect.scoped` block that\n * wraps the loop) and call `probeStartTime` / `probeHolderLiveness`\n * on each holder without threading the Map manually. */\nexport class LivenessProbeScope extends Context.Service<\n\tLivenessProbeScope,\n\tLivenessProbeScopeShape\n>()('@devstack/substrate/cross-process/LivenessProbeScope') {}\n\n/** Construct a fresh `LivenessProbeScope` whose underlying cache is\n * private to this layer — every yielding scope gets its own Map. */\nexport const layerLivenessProbeScope: Layer.Layer<LivenessProbeScope> = Layer.effect(\n\tLivenessProbeScope,\n\tEffect.sync(() => {\n\t\tconst cache: LivenessCache = new Map();\n\t\treturn LivenessProbeScope.of({\n\t\t\tprobeStartTime: (pid) => processStartTime(pid, cache),\n\t\t\tprobeHolderLiveness: (holder, ownHost = nodeHostname()) =>\n\t\t\t\tcheckHolderLiveness(holder, ownHost, cache),\n\t\t});\n\t}),\n);\n"],"mappings":";;;;;;;;;AA4BA,MAAa,cAAc,QAAyB;AACnD,KAAI,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,EAAG,QAAO;AAC9C,KAAI;AACH,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACC,KAAK;AAEb,SADc,IAA8B,SAC5B;;;;;;;;;;;;;AA0BlB,MAAa,oBAAoB,KAAa,UAAyC;AACtF,KAAI,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,EAAG,QAAO;AAmB9C,KAAI,QAAQ,SAAS,EAAE;AACtB,MAAI,sBAAsB,MAAO,QAAO;EACxC,MAAM,SAAS,uBAAuB,IAAI;AAC1C,MAAI,WAAW,KAAM,qBAAoB;AACzC,SAAO;;AAER,KAAI,OAAO,IAAI,IAAI,CAGlB,QAAO,MAAM,IAAI,IAAI,IAAI;CAE1B,MAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAO,IAAI,KAAK,OAAO;AACvB,QAAO;;AAGR,MAAM,QAAuB,OAAO,QAAQ;AAG5C,IAAI,oBAA2C;;;AAI/C,MAAM,0BAA0B,QAA+B;AAC9D,KAAI,QAAQ,aAAa,QACxB,KAAI;EACH,MAAM,MAAM,aAAa,YAAY;GAAC;GAAO,UAAU;GAAO;GAAO;GAAO;GAAM,EAAE;GACnF,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,CAAC;AAIF,SAAO,IAAI,MAAM,CAAC,WAAW,KAAI,GAAG,mBAAmB,IAAI,MAAM,CAAC,GAAG;SAC9D;AACP,SAAO;;AAGT,KAAI;EAMH,MAAM,UALM,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,IAAI;GAAC,EAAE;GACpE,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,CACkB,CAAC,MAAM;AAC1B,SAAO,QAAQ,SAAS,IAAI,mBAAmB,QAAQ,GAAG;SACnD;AACP,SAAO;;;;;;;;;AAUT,MAAM,sBAAsB,UAA0B;CACrD,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACtC,OAAK,MAAM,WAAW,EAAE;AACxB,MAAK,IAAI,aAAc;;AAIxB,QAAO,MAAM;;;;;;;;;;;;AAad,MAAa,sBAAsB,OAAO,GAAG,6CAA6C,CACzF,WAAW,QAAsB,UAAkBA,UAAc,EAAE,OAAuB;AAEzF,KAAI,OAAO,aAAa,QACvB,QAAO;AAER,QAAO,OAAO,oBAAoB;EACjC,uBAAuB,OAAO;EAC9B,wBAAwB,OAAO;EAC/B,CAAC;AACF,KAAI,CAAC,WAAW,OAAO,IAAI,CAAE,QAAO;CACpC,MAAM,cAAc,iBAAiB,OAAO,KAAK,MAAM;AAGvD,KAAI,gBAAgB,KAAM,QAAO;AAMjC,KAAI,OAAO,cAAc,KAAM,QAAO;AACtC,QAAO,gBAAgB,OAAO,YAAa,UAAqB;EAEjE;;;;;;;;;;;AAYD,MAAa,aAAa,SAAgC,aAA2B;CACpF,MAAM,MAAM,SAAS;AAErB,QAAO;EACN;EACA,WAHiB,iBAAiB,IAGzB;EACT,UAAUA,UAAc;EACxB,WAAW,KAAK,KAAK;EACrB,aAAa,KAAK,KAAK;EACvB;EACA;;;;;;AA+BF,IAAa,qBAAb,cAAwC,QAAQ,SAG7C,CAAC,uDAAuD,CAAC;;;AAI5D,MAAa,0BAA2D,MAAM,OAC7E,oBACA,OAAO,WAAW;CACjB,MAAM,wBAAuB,IAAI,KAAK;AACtC,QAAO,mBAAmB,GAAG;EAC5B,iBAAiB,QAAQ,iBAAiB,KAAK,MAAM;EACrD,sBAAsB,QAAQ,UAAUA,UAAc,KACrD,oBAAoB,QAAQ,SAAS,MAAM;EAC5C,CAAC;EACD,CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StackPathsService } from "../paths.mjs";
|
|
2
|
+
import { underLiveClock } from "./live-clock.mjs";
|
|
3
|
+
import { acquireStackLock } from "./stack-lock.mjs";
|
|
4
|
+
import { Context, Effect, Layer, Semaphore } from "effect";
|
|
5
|
+
//#region src/substrate/runtime/cross-process/lock.ts
|
|
6
|
+
var CrossProcessLock = class extends Context.Service()("@devstack/substrate/CrossProcessLock") {};
|
|
7
|
+
Layer.effect(CrossProcessLock, Effect.gen(function* () {
|
|
8
|
+
const paths = yield* StackPathsService;
|
|
9
|
+
const semaphore = yield* Semaphore.make(1);
|
|
10
|
+
const lockPath = paths.stackLockFile;
|
|
11
|
+
return CrossProcessLock.of({ withLock: (effect) => semaphore.withPermits(1)(Effect.scoped(Effect.gen(function* () {
|
|
12
|
+
yield* underLiveClock(acquireStackLock(lockPath));
|
|
13
|
+
return yield* effect;
|
|
14
|
+
}))) });
|
|
15
|
+
}));
|
|
16
|
+
Layer.effect(CrossProcessLock, Effect.gen(function* () {
|
|
17
|
+
const semaphore = yield* Semaphore.make(1);
|
|
18
|
+
return CrossProcessLock.of({ withLock: (effect) => semaphore.withPermits(1)(effect) });
|
|
19
|
+
}));
|
|
20
|
+
//#endregion
|
|
21
|
+
export { CrossProcessLock };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=lock.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.mjs","names":[],"sources":["../../../../src/substrate/runtime/cross-process/lock.ts"],"sourcesContent":["// Cross-process advisory lock — typed service + two Layers.\n//\n// Architecture § \"What's collapsed\" — three different locks in the\n// old codebase consolidate to ONE lock primitive. The on-disk\n// implementation (`stack.lock` via O_EXCL + PID/start-time liveness)\n// lives at `./stack-lock.ts` as a free function; THIS module wraps\n// it in a Layer satisfying the typed `CrossProcessLock` service so\n// consumers (state-store, cache, etc.) yield ONE name and let\n// wiring decide whether they get the OS-advisory implementation or\n// the in-process semaphore.\n//\n// The state-store uses this for read-modify-write critical sections\n// where two processes might race to mutate the JSON. The lock is\n// SHORT-CRITICAL-SECTION: hold across read+modify+atomic-write,\n// release immediately. Long lifetimes (whole-stack lifecycle) belong\n// to a separate lease, not this lock.\n\nimport { Context, Effect, Layer, Semaphore } from 'effect';\n\nimport { StackPathsService } from '../paths.ts';\nimport { underLiveClock } from './live-clock.ts';\nimport {\n\tacquireStackLock,\n\ttype StackLockIoError,\n\ttype StackLockTimeoutError,\n} from './stack-lock.ts';\n\n/**\n * Cross-process exclusive lock. `withLock(effect)` runs `effect`\n * with the lock held — semantically `Semaphore.withPermits(1)` plus\n * cross-process fanout via the OS.\n *\n * Acquisition is cancellable: an interrupt while waiting for the\n * lock unwinds without acquiring. Acquisition while another holder\n * dies (PID cleanup) is the lock primitive's job, not the\n * state-store's.\n *\n * Acquisition surfaces `StackLockTimeoutError` (peer contention\n * exceeded the acquire window) and `StackLockIoError` (disk failure\n * during acquire) in the `E` channel alongside the body's own errors.\n * Consumers MUST handle these typed failures — either degrade\n * gracefully via `Effect.catchTag` or widen their own `E` channel.\n * Surfacing them as defects (the prior `Effect.orDie` shape) was a\n * bug: a busy peer should not crash the supervisor's fiber. The\n * in-process Layer's typed `E` channel matches `never` for the\n * acquire surface so test wiring stays interchangeable with prod\n * (the union absorbs `never` cleanly).\n */\nexport interface CrossProcessLockShape {\n\treadonly withLock: <A, E, R>(\n\t\teffect: Effect.Effect<A, E, R>,\n\t) => Effect.Effect<A, E | StackLockTimeoutError | StackLockIoError, R>;\n}\n\nexport class CrossProcessLock extends Context.Service<CrossProcessLock, CrossProcessLockShape>()(\n\t'@devstack/substrate/CrossProcessLock',\n) {}\n\n/**\n * Production Layer — backed by `stack.lock` (O_EXCL + PID/start-time\n * liveness) per architecture § Cross-process safety protocol. Yields\n * `StackPathsService` to know which on-disk lock file represents\n * this stack; every `withLock` runs the body inside an\n * `Effect.scoped` that acquires + releases the OS-advisory lock.\n *\n * An in-process `Semaphore` sits in front of the disk acquire so\n * SAME-process concurrent yielders of `CrossProcessLock` serialize\n * before competing for the disk artifact. Without this, two fibers\n * in the same process race each other to `O_EXCL`-write the same\n * path; one wins, the other times out unnecessarily after backoff.\n *\n * Acquisition surfaces `StackLockTimeoutError` / `StackLockIoError`\n * in the `E` channel per `CrossProcessLockShape`; consumers catch\n * via `Effect.catchTag` or widen their own surface. The earlier\n * `Effect.orDie` here converted legitimate peer-contention timeouts\n * into supervisor defects — that was wrong; busy peers must be\n * recoverable through the typed channel.\n */\nexport const layerCrossProcessLockFlock: Layer.Layer<CrossProcessLock, never, StackPathsService> =\n\tLayer.effect(\n\t\tCrossProcessLock,\n\t\tEffect.gen(function* () {\n\t\t\tconst paths = yield* StackPathsService;\n\t\t\tconst semaphore = yield* Semaphore.make(1);\n\t\t\tconst lockPath = paths.stackLockFile;\n\t\t\treturn CrossProcessLock.of({\n\t\t\t\twithLock: <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n\t\t\t\t\t// Acquire/release infrastructure runs under live clock\n\t\t\t\t\t// (OS-advisory lock retry backoff needs wall time — see\n\t\t\t\t\t// LIVE_CLOCK). The user body inherits the caller's\n\t\t\t\t\t// clock so TestClock-driven tests can virtualize\n\t\t\t\t\t// `Effect.sleep` inside the critical section.\n\t\t\t\t\t//\n\t\t\t\t\t// `semaphore.withPermits(1)` keeps SAME-process fibers\n\t\t\t\t\t// serialized before they race for the on-disk artifact;\n\t\t\t\t\t// permit release fires on body completion, failure, AND\n\t\t\t\t\t// interrupt via the semaphore's own finalizer.\n\t\t\t\t\t// `Effect.scoped` owns the on-disk acquire so its\n\t\t\t\t\t// finalizer (registered by `acquireStackLock`) unlinks\n\t\t\t\t\t// `stack.lock` on the same lifecycle.\n\t\t\t\t\tsemaphore.withPermits(1)(\n\t\t\t\t\t\tEffect.scoped(\n\t\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\t\tyield* underLiveClock(acquireStackLock(lockPath));\n\t\t\t\t\t\t\t\treturn yield* effect;\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t});\n\t\t}),\n\t);\n\n/**\n * In-process-only fallback. Useful for tests and single-process dev\n * loops where no other OS process can interleave with this\n * supervisor. Production wiring uses `layerCrossProcessLockFlock`.\n *\n * Documented in the architecture as acceptable for \"no cross-process\n * safety needed\" use cases (one-shot CLI invocations, unit tests\n * with a tmpdir per-test, etc.). NOT safe under `pnpm dev` where\n * A dev server and devstack share a runtime root.\n */\nexport const layerCrossProcessLockInProcess: Layer.Layer<CrossProcessLock> = Layer.effect(\n\tCrossProcessLock,\n\tEffect.gen(function* () {\n\t\tconst semaphore = yield* Semaphore.make(1);\n\t\treturn CrossProcessLock.of({\n\t\t\t// No on-disk acquire here, so there's no wall-time backoff to\n\t\t\t// pin — `semaphore.withPermits(1)` parks on a queue without\n\t\t\t// `Effect.sleep`, so it tolerates TestClock natively. The\n\t\t\t// body inherits the caller's clock by construction; no\n\t\t\t// `underLiveClock` wrap.\n\t\t\twithLock: (effect) => semaphore.withPermits(1)(effect),\n\t\t});\n\t}),\n);\n"],"mappings":";;;;;AAsDA,IAAa,mBAAb,cAAsC,QAAQ,SAAkD,CAC/F,uCACA,CAAC;AAuBD,MAAM,OACL,kBACA,OAAO,IAAI,aAAa;CACvB,MAAM,QAAQ,OAAO;CACrB,MAAM,YAAY,OAAO,UAAU,KAAK,EAAE;CAC1C,MAAM,WAAW,MAAM;AACvB,QAAO,iBAAiB,GAAG,EAC1B,WAAoB,WAcnB,UAAU,YAAY,EAAE,CACvB,OAAO,OACN,OAAO,IAAI,aAAa;AACvB,SAAO,eAAe,iBAAiB,SAAS,CAAC;AACjD,SAAO,OAAO;GACb,CACF,CACD,EACF,CAAC;EACD,CACF;AAY2E,MAAM,OAClF,kBACA,OAAO,IAAI,aAAa;CACvB,MAAM,YAAY,OAAO,UAAU,KAAK,EAAE;AAC1C,QAAO,iBAAiB,GAAG,EAM1B,WAAW,WAAW,UAAU,YAAY,EAAE,CAAC,OAAO,EACtD,CAAC;EACD,CACF"}
|