@mysten-incubation/devstack 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dashboard-ui/assets/grpc-a4usE3Nk.js +3 -0
- package/dashboard-ui/assets/index-B82Bh84P.css +1 -0
- package/dashboard-ui/assets/index-CRYJ4pru.js +1277 -0
- package/dashboard-ui/index.html +2 -2
- package/dist/api/define-capabilities.d.mts +1 -20
- package/dist/api/define-capabilities.mjs +15 -22
- package/dist/api/define-capabilities.mjs.map +1 -1
- package/dist/api/define-devstack-with.d.mts +7 -1
- package/dist/api/define-devstack-with.mjs.map +1 -1
- package/dist/api/define-devstack.mjs.map +1 -1
- package/dist/api/define-plugin.d.mts +2 -1
- package/dist/api/inference-network.d.mts +31 -17
- package/dist/api/inference-network.mjs +40 -43
- package/dist/api/inference-network.mjs.map +1 -1
- package/dist/api/load-config.mjs +88 -0
- package/dist/api/load-config.mjs.map +1 -0
- package/dist/api/mode-narrowed-factory.d.mts +1 -1
- package/dist/api/mode-narrowed-factory.mjs.map +1 -1
- package/dist/api/run-stack-internal.mjs +202 -0
- package/dist/api/run-stack-internal.mjs.map +1 -0
- package/dist/api/run-stack.d.mts +82 -31
- package/dist/api/run-stack.mjs +22 -141
- package/dist/api/run-stack.mjs.map +1 -1
- package/dist/build-integrations/dapp-kit/index.d.mts +61 -0
- package/dist/build-integrations/dapp-kit/index.mjs +58 -0
- package/dist/build-integrations/dapp-kit/index.mjs.map +1 -0
- package/dist/build-integrations/playwright/config.d.mts +1 -30
- package/dist/build-integrations/playwright/config.mjs +1 -27
- package/dist/build-integrations/playwright/config.mjs.map +1 -1
- package/dist/build-integrations/playwright/errors.d.mts +4 -7
- package/dist/build-integrations/playwright/errors.mjs.map +1 -1
- package/dist/build-integrations/playwright/global-setup.d.mts +42 -71
- package/dist/build-integrations/playwright/global-setup.mjs +142 -218
- package/dist/build-integrations/playwright/global-setup.mjs.map +1 -1
- package/dist/build-integrations/playwright/index.d.mts +2 -2
- package/dist/build-integrations/playwright/index.mjs +2 -2
- package/dist/build-integrations/playwright/stack-context.d.mts +3 -1
- package/dist/build-integrations/playwright/stack-context.mjs +4 -2
- package/dist/build-integrations/playwright/stack-context.mjs.map +1 -1
- package/dist/build-integrations/playwright/wallet-context.mjs +44 -19
- package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/cold-start-url.mjs.map +1 -1
- package/dist/build-integrations/runtime/conventional-routes.mjs.map +1 -1
- package/dist/build-integrations/runtime/dapp-kit-slot.mjs.map +1 -1
- package/dist/build-integrations/runtime/discover.d.mts +6 -4
- package/dist/build-integrations/runtime/discover.mjs +15 -4
- package/dist/build-integrations/runtime/discover.mjs.map +1 -1
- package/dist/build-integrations/runtime/endpoint-registry.mjs.map +1 -1
- package/dist/build-integrations/runtime/errors.mjs.map +1 -1
- package/dist/build-integrations/runtime/read-stack-context.mjs +3 -3
- package/dist/build-integrations/runtime/read-stack-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/resolve-discovery-env.d.mts +23 -7
- package/dist/build-integrations/runtime/resolve-discovery-env.mjs +25 -12
- package/dist/build-integrations/runtime/resolve-discovery-env.mjs.map +1 -1
- package/dist/build-integrations/runtime/stack-context.d.mts +2 -2
- package/dist/build-integrations/vite/index.d.mts +84 -19
- package/dist/build-integrations/vite/index.mjs +203 -28
- package/dist/build-integrations/vite/index.mjs.map +1 -1
- package/dist/build-integrations/vitest/config.d.mts +20 -2
- package/dist/build-integrations/vitest/config.mjs +16 -4
- package/dist/build-integrations/vitest/config.mjs.map +1 -1
- package/dist/build-integrations/vitest/env.d.mts +18 -10
- package/dist/build-integrations/vitest/env.mjs +11 -10
- package/dist/build-integrations/vitest/env.mjs.map +1 -1
- package/dist/build-integrations/vitest/errors.d.mts +7 -11
- package/dist/build-integrations/vitest/errors.mjs +3 -4
- package/dist/build-integrations/vitest/errors.mjs.map +1 -1
- package/dist/build-integrations/vitest/global-setup.d.mts +35 -0
- package/dist/build-integrations/vitest/global-setup.mjs +156 -0
- package/dist/build-integrations/vitest/global-setup.mjs.map +1 -0
- package/dist/build-integrations/vitest/index.d.mts +3 -2
- package/dist/build-integrations/vitest/index.mjs +3 -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 +5 -3
- package/dist/build-integrations/vitest/stack-context.mjs +3 -2
- package/dist/build-integrations/vitest/stack-context.mjs.map +1 -1
- package/dist/cli/bail.mjs.map +1 -1
- package/dist/cli/doctor-probes.mjs +1 -3
- package/dist/cli/doctor-probes.mjs.map +1 -1
- package/dist/cli/main.d.mts +23 -1
- package/dist/cli/main.mjs +63 -8
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/prune-direct.mjs.map +1 -1
- package/dist/cli/snapshot-reader.mjs.map +1 -1
- package/dist/cli/up-lifecycle.mjs.map +1 -1
- package/dist/cli/wirings/apply.mjs +28 -21
- package/dist/cli/wirings/apply.mjs.map +1 -1
- package/dist/cli/wirings/codegen.mjs +108 -0
- package/dist/cli/wirings/codegen.mjs.map +1 -0
- package/dist/cli/wirings/config-loader.mjs +12 -62
- package/dist/cli/wirings/config-loader.mjs.map +1 -1
- package/dist/cli/wirings/dump-ids.mjs +86 -0
- package/dist/cli/wirings/dump-ids.mjs.map +1 -0
- package/dist/cli/wirings/engine-command.mjs +1 -1
- package/dist/cli/wirings/engine-command.mjs.map +1 -1
- package/dist/cli/wirings/identity.mjs +2 -2
- package/dist/cli/wirings/identity.mjs.map +1 -1
- package/dist/cli/wirings/provide-file-system.mjs.map +1 -1
- package/dist/cli/wirings/read-devstack-version.mjs +17 -0
- package/dist/cli/wirings/read-devstack-version.mjs.map +1 -0
- package/dist/cli/wirings/snapshot.mjs +27 -34
- package/dist/cli/wirings/snapshot.mjs.map +1 -1
- package/dist/cli/wirings/up-ipc.mjs +147 -0
- package/dist/cli/wirings/up-ipc.mjs.map +1 -0
- package/dist/cli/wirings/up.mjs +218 -198
- package/dist/cli/wirings/up.mjs.map +1 -1
- package/dist/cli/wirings/wipe.mjs +1 -1
- package/dist/cli/wirings/wipe.mjs.map +1 -1
- package/dist/contracts/chain-probe.d.mts +1 -1
- package/dist/contracts/chain-probe.mjs +1 -1
- package/dist/contracts/chain-probe.mjs.map +1 -1
- package/dist/contracts/codegenable.d.mts +60 -6
- package/dist/contracts/codegenable.mjs +28 -0
- package/dist/contracts/codegenable.mjs.map +1 -0
- package/dist/contracts/config-bindings.mjs +162 -0
- package/dist/contracts/config-bindings.mjs.map +1 -0
- package/dist/contracts/container-runtime.d.mts +23 -17
- package/dist/contracts/plugin-expander.mjs.map +1 -1
- package/dist/contracts/snapshotable.d.mts +5 -7
- package/dist/contracts/wallet-protocol.d.mts +2 -5
- package/dist/contracts/wallet-protocol.mjs +2 -5
- package/dist/contracts/wallet-protocol.mjs.map +1 -1
- package/dist/index.d.mts +21 -26
- package/dist/index.mjs +8 -9
- package/dist/orchestrators/boot.d.mts +28 -0
- package/dist/orchestrators/boot.mjs +440 -0
- package/dist/orchestrators/boot.mjs.map +1 -0
- package/dist/orchestrators/codegen/bindings.d.mts +13 -1
- package/dist/orchestrators/codegen/bindings.mjs +23 -9
- package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
- package/dist/orchestrators/codegen/config-runtime.mjs +208 -0
- package/dist/orchestrators/codegen/config-runtime.mjs.map +1 -0
- package/dist/orchestrators/codegen/emit.mjs +1 -1
- package/dist/orchestrators/codegen/emit.mjs.map +1 -1
- package/dist/orchestrators/codegen/errors.mjs +21 -1
- package/dist/orchestrators/codegen/errors.mjs.map +1 -1
- package/dist/orchestrators/codegen/format.mjs +2 -0
- package/dist/orchestrators/codegen/format.mjs.map +1 -1
- package/dist/orchestrators/codegen/gitignore.mjs +30 -11
- package/dist/orchestrators/codegen/gitignore.mjs.map +1 -1
- package/dist/orchestrators/codegen/id-config.mjs +107 -0
- package/dist/orchestrators/codegen/id-config.mjs.map +1 -0
- package/dist/orchestrators/codegen/output-location.mjs +23 -32
- package/dist/orchestrators/codegen/output-location.mjs.map +1 -1
- package/dist/orchestrators/codegen/paths.mjs +8 -1
- package/dist/orchestrators/codegen/paths.mjs.map +1 -1
- package/dist/orchestrators/codegen/permissions.mjs.map +1 -1
- package/dist/orchestrators/codegen/service.mjs +286 -58
- package/dist/orchestrators/codegen/service.mjs.map +1 -1
- package/dist/orchestrators/layers.mjs +31 -0
- package/dist/orchestrators/layers.mjs.map +1 -0
- package/dist/orchestrators/lifecycle-prune/errors.mjs.map +1 -1
- package/dist/orchestrators/lifecycle-prune/index.mjs +8 -10
- package/dist/orchestrators/lifecycle-prune/index.mjs.map +1 -1
- package/dist/orchestrators/network-options.d.mts +52 -0
- package/dist/orchestrators/network-options.mjs +43 -0
- package/dist/orchestrators/network-options.mjs.map +1 -0
- package/dist/orchestrators/router/cleanup.mjs.map +1 -1
- package/dist/orchestrators/router/cors.mjs.map +1 -1
- package/dist/orchestrators/router/entrypoints.mjs.map +1 -1
- package/dist/orchestrators/router/errors.mjs.map +1 -1
- package/dist/orchestrators/router/file-provider.mjs.map +1 -1
- package/dist/orchestrators/router/hostname.mjs.map +1 -1
- package/dist/orchestrators/router/profile.mjs +1 -1
- package/dist/orchestrators/router/profile.mjs.map +1 -1
- package/dist/orchestrators/router/service.d.mts +2 -2
- package/dist/orchestrators/router/service.mjs +12 -27
- package/dist/orchestrators/router/service.mjs.map +1 -1
- package/dist/orchestrators/router/traefik-container.mjs +1 -1
- package/dist/orchestrators/router/traefik-container.mjs.map +1 -1
- package/dist/orchestrators/snapshot/capture.mjs +171 -195
- package/dist/orchestrators/snapshot/capture.mjs.map +1 -1
- package/dist/orchestrators/snapshot/descriptor.d.mts +1 -1
- package/dist/orchestrators/snapshot/descriptor.mjs +53 -11
- package/dist/orchestrators/snapshot/descriptor.mjs.map +1 -1
- package/dist/orchestrators/snapshot/identity-guard.mjs +3 -5
- package/dist/orchestrators/snapshot/identity-guard.mjs.map +1 -1
- package/dist/orchestrators/snapshot/image-bundle-tags.mjs +32 -101
- package/dist/orchestrators/snapshot/image-bundle-tags.mjs.map +1 -1
- package/dist/orchestrators/snapshot/index.mjs +1 -4
- package/dist/orchestrators/snapshot/integrity.mjs.map +1 -1
- package/dist/orchestrators/snapshot/interrupted-restore.mjs +116 -0
- package/dist/orchestrators/snapshot/interrupted-restore.mjs.map +1 -0
- package/dist/orchestrators/snapshot/phase-error.mjs.map +1 -1
- package/dist/orchestrators/snapshot/prune.mjs +40 -31
- package/dist/orchestrators/snapshot/prune.mjs.map +1 -1
- package/dist/orchestrators/snapshot/restore.mjs +221 -196
- package/dist/orchestrators/snapshot/restore.mjs.map +1 -1
- package/dist/orchestrators/snapshot/service.mjs +72 -62
- package/dist/orchestrators/snapshot/service.mjs.map +1 -1
- package/dist/orchestrators/snapshot/wipe.mjs +67 -56
- package/dist/orchestrators/snapshot/wipe.mjs.map +1 -1
- package/dist/plugins/account/codegen.mjs +3 -0
- package/dist/plugins/account/codegen.mjs.map +1 -1
- package/dist/plugins/account/errors.d.mts +2 -2
- package/dist/plugins/account/errors.mjs +1 -4
- package/dist/plugins/account/errors.mjs.map +1 -1
- package/dist/plugins/account/funding.mjs +3 -27
- package/dist/plugins/account/funding.mjs.map +1 -1
- package/dist/plugins/account/index.d.mts +3 -8
- package/dist/plugins/account/index.mjs +71 -35
- package/dist/plugins/account/index.mjs.map +1 -1
- package/dist/plugins/account/keypair.d.mts +8 -7
- package/dist/plugins/account/keypair.mjs +5 -18
- package/dist/plugins/account/keypair.mjs.map +1 -1
- package/dist/plugins/account/lease.mjs.map +1 -1
- package/dist/plugins/account/registry.d.mts +1 -46
- package/dist/plugins/account/registry.mjs.map +1 -1
- package/dist/plugins/account/service.d.mts +43 -34
- package/dist/plugins/account/service.mjs +9 -54
- package/dist/plugins/account/service.mjs.map +1 -1
- package/dist/plugins/account/snapshot.mjs.map +1 -1
- package/dist/plugins/account/variants/ephemeral.mjs.map +1 -1
- package/dist/plugins/account/variants/impersonate.mjs.map +1 -1
- package/dist/plugins/account/variants/signer.mjs.map +1 -1
- package/dist/plugins/action/discriminator.mjs.map +1 -1
- package/dist/plugins/action/errors.mjs +1 -4
- package/dist/plugins/action/errors.mjs.map +1 -1
- package/dist/plugins/action/execute.mjs +3 -4
- package/dist/plugins/action/execute.mjs.map +1 -1
- package/dist/plugins/action/index.d.mts +2 -2
- package/dist/plugins/action/index.mjs +7 -11
- package/dist/plugins/action/index.mjs.map +1 -1
- package/dist/plugins/action/service.d.mts +5 -7
- package/dist/plugins/action/service.mjs +2 -16
- package/dist/plugins/action/service.mjs.map +1 -1
- package/dist/plugins/coin/address-resolution.mjs.map +1 -1
- package/dist/plugins/coin/codegen.mjs +97 -16
- package/dist/plugins/coin/codegen.mjs.map +1 -1
- package/dist/plugins/coin/discovery.mjs.map +1 -1
- package/dist/plugins/coin/errors.mjs +1 -4
- package/dist/plugins/coin/errors.mjs.map +1 -1
- package/dist/plugins/coin/index.d.mts +4 -34
- package/dist/plugins/coin/index.mjs +47 -28
- package/dist/plugins/coin/index.mjs.map +1 -1
- package/dist/plugins/coin/metadata.d.mts +1 -0
- package/dist/plugins/coin/metadata.mjs +8 -9
- package/dist/plugins/coin/metadata.mjs.map +1 -1
- package/dist/plugins/coin/mint.mjs +7 -21
- package/dist/plugins/coin/mint.mjs.map +1 -1
- package/dist/plugins/coin/registry.mjs +33 -20
- package/dist/plugins/coin/registry.mjs.map +1 -1
- package/dist/plugins/coin/service.d.mts +1 -1
- package/dist/plugins/coin/service.mjs +1 -8
- package/dist/plugins/coin/service.mjs.map +1 -1
- package/dist/plugins/coin/snapshot.mjs.map +1 -1
- package/dist/plugins/coin/type-strings.mjs.map +1 -1
- package/dist/plugins/dashboard/domain.mjs +106 -167
- package/dist/plugins/dashboard/domain.mjs.map +1 -1
- package/dist/plugins/dashboard/index.mjs +13 -13
- package/dist/plugins/dashboard/index.mjs.map +1 -1
- package/dist/plugins/dashboard/origin-policy.mjs +3 -3
- package/dist/plugins/dashboard/origin-policy.mjs.map +1 -1
- package/dist/plugins/dashboard/routable.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/builder.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/enums.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/root.mjs +23 -44
- package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/types.mjs +37 -91
- package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
- package/dist/plugins/dashboard/schema.mjs.map +1 -1
- package/dist/plugins/dashboard/server.mjs.map +1 -1
- package/dist/plugins/deepbook/codegen.d.mts +12 -11
- package/dist/plugins/deepbook/codegen.mjs +108 -11
- package/dist/plugins/deepbook/codegen.mjs.map +1 -1
- package/dist/plugins/deepbook/deploy.mjs +7 -14
- package/dist/plugins/deepbook/deploy.mjs.map +1 -1
- package/dist/plugins/deepbook/errors.mjs +4 -2
- package/dist/plugins/deepbook/errors.mjs.map +1 -1
- package/dist/plugins/deepbook/faucet-strategy.mjs +4 -10
- package/dist/plugins/deepbook/faucet-strategy.mjs.map +1 -1
- package/dist/plugins/deepbook/hash.mjs.map +1 -1
- package/dist/plugins/deepbook/index.d.mts +38 -34
- package/dist/plugins/deepbook/index.mjs +133 -64
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/deepbook/plugin-key.mjs.map +1 -1
- package/dist/plugins/deepbook/pyth/index.mjs +4 -8
- package/dist/plugins/deepbook/pyth/index.mjs.map +1 -1
- package/dist/plugins/deepbook/snapshot.mjs +0 -1
- package/dist/plugins/deepbook/snapshot.mjs.map +1 -1
- package/dist/plugins/deepbook/type-strings.mjs.map +1 -1
- package/dist/plugins/deepbook/types.d.mts +2 -1
- package/dist/plugins/deepbook/types.mjs.map +1 -1
- package/dist/plugins/faucet/dispatcher.d.mts +3 -3
- package/dist/plugins/faucet/dispatcher.mjs.map +1 -1
- package/dist/plugins/faucet/errors.mjs.map +1 -1
- package/dist/plugins/faucet/http.mjs +2 -10
- package/dist/plugins/faucet/http.mjs.map +1 -1
- package/dist/plugins/faucet/index.mjs +0 -1
- package/dist/plugins/faucet/index.mjs.map +1 -1
- package/dist/plugins/host-service/errors.d.mts +1 -3
- package/dist/plugins/host-service/errors.mjs +1 -2
- package/dist/plugins/host-service/errors.mjs.map +1 -1
- package/dist/plugins/host-service/index.d.mts +1 -2
- package/dist/plugins/host-service/index.mjs +11 -11
- package/dist/plugins/host-service/index.mjs.map +1 -1
- package/dist/plugins/host-service/routable.mjs.map +1 -1
- package/dist/plugins/host-service/service.mjs +13 -13
- package/dist/plugins/host-service/service.mjs.map +1 -1
- package/dist/plugins/internal/acquire-on-chain-artifact.mjs.map +1 -1
- package/dist/plugins/internal/codegen-helpers.mjs +2 -0
- package/dist/plugins/internal/codegen-helpers.mjs.map +1 -1
- package/dist/plugins/internal/funding-failure-error.mjs.map +1 -1
- package/dist/plugins/{postgres → internal/postgres-sidecar}/connection.mjs +1 -1
- package/dist/plugins/internal/postgres-sidecar/connection.mjs.map +1 -0
- package/dist/plugins/{postgres → internal/postgres-sidecar}/db-ensure.mjs +30 -16
- package/dist/plugins/internal/postgres-sidecar/db-ensure.mjs.map +1 -0
- package/dist/plugins/internal/postgres-sidecar/errors.mjs +21 -0
- package/dist/plugins/internal/postgres-sidecar/errors.mjs.map +1 -0
- package/dist/{substrate/runtime/scoped-multimap → plugins/internal/postgres-sidecar}/index.mjs +1 -0
- package/dist/plugins/internal/postgres-sidecar/service.mjs +121 -0
- package/dist/plugins/internal/postgres-sidecar/service.mjs.map +1 -0
- package/dist/plugins/package/build.mjs +2 -2
- package/dist/plugins/package/build.mjs.map +1 -1
- package/dist/plugins/package/codegen.d.mts +7 -14
- package/dist/plugins/package/codegen.mjs +187 -59
- package/dist/plugins/package/codegen.mjs.map +1 -1
- package/dist/plugins/package/dep-resolution.mjs +51 -3
- package/dist/plugins/package/dep-resolution.mjs.map +1 -1
- package/dist/plugins/package/errors.mjs +1 -4
- package/dist/plugins/package/errors.mjs.map +1 -1
- package/dist/plugins/package/git-source.d.mts +18 -0
- package/dist/plugins/package/git-source.mjs +119 -0
- package/dist/plugins/package/git-source.mjs.map +1 -0
- package/dist/plugins/package/index.d.mts +28 -19
- package/dist/plugins/package/index.mjs +107 -32
- package/dist/plugins/package/index.mjs.map +1 -1
- package/dist/plugins/package/mode-known.mjs +2 -2
- package/dist/plugins/package/mode-known.mjs.map +1 -1
- package/dist/plugins/package/mode-local.mjs +18 -35
- package/dist/plugins/package/mode-local.mjs.map +1 -1
- package/dist/plugins/package/publish-executor.mjs +13 -8
- package/dist/plugins/package/publish-executor.mjs.map +1 -1
- package/dist/plugins/package/publish-output.d.mts +1 -11
- package/dist/plugins/package/publish-output.mjs +1 -9
- package/dist/plugins/package/publish-output.mjs.map +1 -1
- package/dist/plugins/package/registry.d.mts +1 -1
- package/dist/plugins/package/registry.mjs +26 -14
- package/dist/plugins/package/registry.mjs.map +1 -1
- package/dist/plugins/package/service.mjs.map +1 -1
- package/dist/plugins/package/snapshot.mjs.map +1 -1
- package/dist/plugins/router-entrypoints.mjs +1 -3
- package/dist/plugins/router-entrypoints.mjs.map +1 -1
- package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs +1 -2
- package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs.map +1 -1
- package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs +1 -6
- package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs.map +1 -1
- package/dist/plugins/seal/codegen.d.mts +1 -5
- package/dist/plugins/seal/codegen.mjs +83 -12
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/config-render.mjs +2 -6
- package/dist/plugins/seal/config-render.mjs.map +1 -1
- package/dist/plugins/seal/deploy.mjs +18 -29
- package/dist/plugins/seal/deploy.mjs.map +1 -1
- package/dist/plugins/seal/errors.d.mts +4 -5
- package/dist/plugins/seal/errors.mjs +1 -8
- package/dist/plugins/seal/errors.mjs.map +1 -1
- package/dist/plugins/seal/index.d.mts +7 -28
- package/dist/plugins/seal/index.mjs +69 -47
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/seal/key-manager.mjs.map +1 -1
- package/dist/plugins/seal/key-server.mjs +1 -6
- package/dist/plugins/seal/key-server.mjs.map +1 -1
- package/dist/plugins/seal/keygen.mjs +2 -3
- package/dist/plugins/seal/keygen.mjs.map +1 -1
- package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
- package/dist/plugins/seal/mode/live.mjs +1 -1
- package/dist/plugins/seal/mode/live.mjs.map +1 -1
- package/dist/plugins/seal/mode/local-keygen.mjs +4 -10
- package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
- package/dist/plugins/seal/plugin-key.mjs.map +1 -1
- 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/snapshot.mjs +1 -2
- package/dist/plugins/seal/snapshot.mjs.map +1 -1
- package/dist/plugins/sui/auto-tick.mjs +5 -5
- package/dist/plugins/sui/auto-tick.mjs.map +1 -1
- package/dist/plugins/sui/chain-build-container.mjs +2 -2
- package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
- package/dist/plugins/sui/chain-probe.mjs +4 -4
- package/dist/plugins/sui/chain-probe.mjs.map +1 -1
- package/dist/plugins/sui/codegen.d.mts +9 -7
- package/dist/plugins/sui/codegen.mjs +49 -46
- package/dist/plugins/sui/codegen.mjs.map +1 -1
- package/dist/plugins/sui/errors.d.mts +1 -1
- package/dist/plugins/sui/errors.mjs +1 -9
- package/dist/plugins/sui/errors.mjs.map +1 -1
- package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/index.d.mts +1 -1
- package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/index.mjs +3 -7
- package/dist/plugins/sui/exec/index.mjs.map +1 -0
- package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/sign-and-dispatch.mjs +1 -1
- package/dist/plugins/sui/exec/sign-and-dispatch.mjs.map +1 -0
- package/dist/plugins/sui/fork-faucet-strategy.mjs +2 -6
- package/dist/plugins/sui/fork-faucet-strategy.mjs.map +1 -1
- package/dist/plugins/sui/fork-orchestration.mjs +13 -23
- package/dist/plugins/sui/fork-orchestration.mjs.map +1 -1
- package/dist/plugins/sui/fork-transaction.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +34 -83
- package/dist/plugins/sui/index.mjs +200 -68
- package/dist/plugins/sui/index.mjs.map +1 -1
- package/dist/{substrate/runtime/sui-ledger → plugins/sui/ledger}/object-ref.mjs +1 -1
- package/dist/plugins/sui/ledger/object-ref.mjs.map +1 -0
- package/dist/plugins/sui/local-faucet-strategy.mjs +1 -5
- package/dist/plugins/sui/local-faucet-strategy.mjs.map +1 -1
- package/dist/plugins/sui/log-attrs.mjs +11 -0
- package/dist/plugins/sui/log-attrs.mjs.map +1 -0
- package/dist/plugins/sui/mode/external.mjs +4 -12
- package/dist/plugins/sui/mode/external.mjs.map +1 -1
- package/dist/plugins/sui/mode/fork.mjs +19 -16
- package/dist/plugins/sui/mode/fork.mjs.map +1 -1
- package/dist/plugins/sui/mode/live.mjs +4 -17
- package/dist/plugins/sui/mode/live.mjs.map +1 -1
- package/dist/plugins/sui/mode/local.d.mts +1 -0
- package/dist/plugins/sui/mode/local.mjs +203 -61
- package/dist/plugins/sui/mode/local.mjs.map +1 -1
- package/dist/plugins/sui/mode/shared-boot.mjs +18 -23
- package/dist/plugins/sui/mode/shared-boot.mjs.map +1 -1
- package/dist/plugins/sui/mode/shared.d.mts +6 -7
- package/dist/plugins/sui/mode/shared.mjs.map +1 -1
- package/dist/plugins/sui/mode/spec.d.mts +23 -6
- package/dist/{substrate/runtime/sui-move-build → plugins/sui/move}/index.mjs +28 -30
- package/dist/plugins/sui/move/index.mjs.map +1 -0
- package/dist/plugins/sui/move-summary-runner.mjs +8 -4
- package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
- package/dist/{substrate/network.d.mts → plugins/sui/network-config.d.mts} +11 -10
- package/dist/plugins/sui/network-resolver.d.mts +6 -6
- package/dist/plugins/sui/routable.mjs.map +1 -1
- package/dist/plugins/sui/service.mjs +10 -6
- package/dist/plugins/sui/service.mjs.map +1 -1
- package/dist/plugins/sui/snapshot.mjs +10 -5
- package/dist/plugins/sui/snapshot.mjs.map +1 -1
- package/dist/plugins/wallet/codegen.d.mts +8 -6
- package/dist/plugins/wallet/codegen.mjs +4 -10
- package/dist/plugins/wallet/codegen.mjs.map +1 -1
- package/dist/plugins/wallet/errors.mjs +1 -4
- package/dist/plugins/wallet/errors.mjs.map +1 -1
- package/dist/plugins/wallet/index.d.mts +3 -6
- package/dist/plugins/wallet/index.mjs +16 -21
- package/dist/plugins/wallet/index.mjs.map +1 -1
- package/dist/plugins/wallet/origin-policy.mjs +3 -3
- package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
- package/dist/plugins/wallet/pairing.mjs +6 -24
- package/dist/plugins/wallet/pairing.mjs.map +1 -1
- package/dist/plugins/wallet/protocol.mjs.map +1 -1
- package/dist/plugins/wallet/routable.mjs.map +1 -1
- package/dist/plugins/wallet/server.mjs +17 -24
- package/dist/plugins/wallet/server.mjs.map +1 -1
- package/dist/plugins/wallet/service.d.mts +2 -2
- package/dist/plugins/wallet/service.mjs +4 -15
- package/dist/plugins/wallet/service.mjs.map +1 -1
- package/dist/plugins/wallet/snapshot.mjs.map +1 -1
- package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs +1 -5
- package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs.map +1 -1
- package/dist/plugins/walrus/codegen.d.mts +2 -13
- package/dist/plugins/walrus/codegen.mjs +93 -28
- package/dist/plugins/walrus/codegen.mjs.map +1 -1
- package/dist/plugins/walrus/deploy-paths.mjs +2 -3
- package/dist/plugins/walrus/deploy-paths.mjs.map +1 -1
- package/dist/plugins/walrus/deploy.mjs +26 -19
- package/dist/plugins/walrus/deploy.mjs.map +1 -1
- package/dist/plugins/walrus/errors.mjs +1 -8
- package/dist/plugins/walrus/errors.mjs.map +1 -1
- package/dist/plugins/walrus/faucet-strategy.mjs +15 -1
- package/dist/plugins/walrus/faucet-strategy.mjs.map +1 -1
- package/dist/plugins/walrus/index.d.mts +13 -44
- package/dist/plugins/walrus/index.mjs +119 -122
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.mjs +6 -6
- package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
- package/dist/plugins/walrus/mode/local-cluster.d.mts +2 -2
- package/dist/plugins/walrus/mode/local-cluster.mjs +2 -10
- package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
- package/dist/plugins/walrus/plugin-key.mjs.map +1 -1
- package/dist/plugins/walrus/registry-publish.mjs.map +1 -1
- package/dist/plugins/walrus/routable.mjs.map +1 -1
- package/dist/plugins/walrus/service.mjs.map +1 -1
- package/dist/plugins/walrus/snapshot.mjs +3 -3
- package/dist/plugins/walrus/snapshot.mjs.map +1 -1
- package/dist/plugins/walrus/storage-nodes.mjs +53 -14
- package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
- package/dist/plugins/walrus/wal-swap.mjs +4 -8
- package/dist/plugins/walrus/wal-swap.mjs.map +1 -1
- package/dist/primitives/artifact-publisher.mjs.map +1 -1
- package/dist/primitives/cache.d.mts +1 -1
- package/dist/runtime/docker/client.mjs +47 -3
- package/dist/runtime/docker/client.mjs.map +1 -1
- package/dist/runtime/docker/container.mjs +37 -57
- package/dist/runtime/docker/container.mjs.map +1 -1
- package/dist/runtime/docker/errors.mjs.map +1 -1
- package/dist/runtime/docker/exec.mjs +2 -2
- package/dist/runtime/docker/exec.mjs.map +1 -1
- package/dist/runtime/docker/image.mjs +10 -11
- package/dist/runtime/docker/image.mjs.map +1 -1
- package/dist/runtime/docker/index.mjs +0 -1
- package/dist/runtime/docker/inspect-and-decode.mjs.map +1 -1
- package/dist/runtime/docker/inventory.mjs +9 -9
- package/dist/runtime/docker/inventory.mjs.map +1 -1
- package/dist/runtime/docker/labels.mjs +12 -1
- package/dist/runtime/docker/labels.mjs.map +1 -1
- package/dist/runtime/docker/network.mjs +6 -6
- package/dist/runtime/docker/network.mjs.map +1 -1
- package/dist/runtime/docker/render-run-args.mjs.map +1 -1
- package/dist/runtime/docker/service.mjs +31 -45
- package/dist/runtime/docker/service.mjs.map +1 -1
- package/dist/runtime/docker/sweep.mjs +14 -64
- package/dist/runtime/docker/sweep.mjs.map +1 -1
- package/dist/runtime/docker/volume.mjs +1 -1
- package/dist/runtime/docker/volume.mjs.map +1 -1
- package/dist/runtime/docker/wrap.mjs +14 -2
- package/dist/runtime/docker/wrap.mjs.map +1 -1
- package/dist/substrate/brand.d.mts +2 -5
- package/dist/substrate/brand.mjs +3 -2
- package/dist/substrate/brand.mjs.map +1 -1
- package/dist/substrate/cross-process.mjs +2 -16
- package/dist/substrate/cross-process.mjs.map +1 -1
- package/dist/substrate/event-time.mjs +0 -2
- package/dist/substrate/event-time.mjs.map +1 -1
- package/dist/substrate/events.d.mts +46 -16
- package/dist/substrate/identity.d.mts +11 -5
- package/dist/substrate/manifest.d.mts +33 -26
- package/dist/substrate/manifest.mjs +8 -3
- package/dist/substrate/manifest.mjs.map +1 -1
- package/dist/substrate/options.d.mts +33 -4
- package/dist/substrate/plugin-ctx.d.mts +70 -0
- package/dist/substrate/plugin-ctx.mjs +66 -0
- package/dist/substrate/plugin-ctx.mjs.map +1 -0
- package/dist/substrate/plugin.d.mts +46 -38
- package/dist/substrate/plugin.mjs +13 -9
- package/dist/substrate/plugin.mjs.map +1 -1
- package/dist/substrate/projection.d.mts +1 -1
- package/dist/substrate/runtime/atomic-write.mjs +3 -4
- package/dist/substrate/runtime/atomic-write.mjs.map +1 -1
- package/dist/substrate/runtime/cache/schema.mjs.map +1 -1
- package/dist/substrate/runtime/cache/service.mjs +50 -20
- package/dist/substrate/runtime/cache/service.mjs.map +1 -1
- package/dist/substrate/runtime/config-validation.mjs.map +1 -1
- package/dist/substrate/runtime/control-plane/domain.mjs +34 -12
- package/dist/substrate/runtime/control-plane/domain.mjs.map +1 -1
- package/dist/substrate/runtime/control-plane/service.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/channel.mjs +14 -17
- package/dist/substrate/runtime/cross-process/command-channel/channel.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/file-channel.mjs.map +1 -1
- 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.map +1 -1
- package/dist/substrate/runtime/cross-process/index.mjs +0 -2
- package/dist/substrate/runtime/cross-process/live-clock.mjs +1 -1
- package/dist/substrate/runtime/cross-process/live-clock.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/liveness.mjs +11 -18
- package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs +2 -2
- package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/roster.mjs +17 -127
- package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/self-pid.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/stack-lock.mjs +29 -28
- package/dist/substrate/runtime/cross-process/stack-lock.mjs.map +1 -1
- package/dist/substrate/runtime/current-plugin.mjs.map +1 -1
- package/dist/substrate/runtime/errors.mjs +1 -13
- package/dist/substrate/runtime/errors.mjs.map +1 -1
- package/dist/substrate/runtime/format-unknown-error.mjs.map +1 -1
- package/dist/substrate/runtime/host-bind-mount-owner.mjs.map +1 -1
- package/dist/substrate/runtime/host-gateway.mjs.map +1 -1
- package/dist/substrate/runtime/host-tree-tar/index.mjs +15 -163
- package/dist/substrate/runtime/host-tree-tar/index.mjs.map +1 -1
- package/dist/substrate/runtime/http-probe.d.mts +1 -3
- package/dist/substrate/runtime/http-probe.mjs.map +1 -1
- package/dist/substrate/runtime/index.mjs +1 -6
- package/dist/substrate/runtime/lease-broker/service.mjs +5 -23
- package/dist/substrate/runtime/lease-broker/service.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/dep-graph.mjs +3 -3
- package/dist/substrate/runtime/lifecycle/dep-graph.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/graph-input-id.d.mts +1 -0
- package/dist/substrate/runtime/lifecycle/graph-input-id.mjs +133 -0
- package/dist/substrate/runtime/lifecycle/graph-input-id.mjs.map +1 -0
- package/dist/substrate/runtime/lifecycle/index.mjs +2 -2
- package/dist/substrate/runtime/lifecycle/plugin-registry.d.mts +1 -1
- package/dist/substrate/runtime/lifecycle/plugin-registry.mjs +22 -1
- package/dist/substrate/runtime/lifecycle/plugin-registry.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/ready-gate.mjs +2 -15
- package/dist/substrate/runtime/lifecycle/ready-gate.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/selective-restart.mjs +29 -25
- package/dist/substrate/runtime/lifecycle/selective-restart.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/signals.mjs +1 -1
- package/dist/substrate/runtime/lifecycle/signals.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/state-machine.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/watch-attribution.d.mts +1 -1
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +0 -1
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
- package/dist/substrate/runtime/managed-container.d.mts +5 -1
- package/dist/substrate/runtime/managed-container.mjs +8 -10
- package/dist/substrate/runtime/managed-container.mjs.map +1 -1
- package/dist/substrate/runtime/manifest/manifest.mjs +8 -30
- package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
- package/dist/substrate/runtime/mode-errors.d.mts +1 -2
- package/dist/substrate/runtime/mode-errors.mjs.map +1 -1
- package/dist/substrate/runtime/observability/cascade-formatter.mjs +1 -11
- package/dist/substrate/runtime/observability/cascade-formatter.mjs.map +1 -1
- package/dist/substrate/runtime/observability/ignore-with-log.mjs.map +1 -1
- package/dist/substrate/runtime/observability/index.d.mts +1 -1
- package/dist/substrate/runtime/observability/index.mjs +4 -6
- package/dist/substrate/runtime/observability/log-attrs.mjs +46 -0
- package/dist/substrate/runtime/observability/log-attrs.mjs.map +1 -0
- package/dist/substrate/runtime/observability/log-store.mjs.map +1 -1
- package/dist/substrate/runtime/observability/logger.mjs +3 -3
- package/dist/substrate/runtime/observability/logger.mjs.map +1 -1
- package/dist/substrate/runtime/observability/output-truncate.mjs.map +1 -1
- package/dist/substrate/runtime/observability/pretty-error.mjs.map +1 -1
- package/dist/substrate/runtime/observability/process-lines.mjs.map +1 -1
- package/dist/substrate/runtime/observability/redaction.mjs.map +1 -1
- package/dist/substrate/runtime/observability/subprocess-capture.mjs +2 -2
- package/dist/substrate/runtime/observability/subprocess-capture.mjs.map +1 -1
- package/dist/substrate/runtime/passthrough-or-wrap.mjs.map +1 -1
- package/dist/substrate/runtime/paths.mjs +27 -9
- package/dist/substrate/runtime/paths.mjs.map +1 -1
- package/dist/substrate/runtime/phase-preserving-produce.mjs.map +1 -1
- package/dist/substrate/runtime/port-broker/service.mjs +10 -27
- package/dist/substrate/runtime/port-broker/service.mjs.map +1 -1
- package/dist/substrate/runtime/post-acquire-tasks.mjs.map +1 -1
- package/dist/substrate/runtime/probes.d.mts +2 -3
- package/dist/substrate/runtime/probes.mjs.map +1 -1
- package/dist/substrate/runtime/process-supervisor.mjs.map +1 -1
- package/dist/substrate/runtime/projection/operational-endpoints.mjs.map +1 -1
- package/dist/substrate/runtime/projection/state-ref.mjs.map +1 -1
- package/dist/substrate/runtime/projection/update.mjs +87 -11
- package/dist/substrate/runtime/projection/update.mjs.map +1 -1
- package/dist/substrate/runtime/random-suffix.mjs.map +1 -1
- package/dist/substrate/runtime/reconcile/fs-plan.mjs +146 -0
- package/dist/substrate/runtime/reconcile/fs-plan.mjs.map +1 -0
- package/dist/substrate/runtime/reconcile/graph.mjs +19 -0
- package/dist/substrate/runtime/reconcile/graph.mjs.map +1 -0
- package/dist/substrate/runtime/reconcile/index.mjs +5 -0
- package/dist/substrate/runtime/reconcile/label.mjs +41 -0
- package/dist/substrate/runtime/reconcile/label.mjs.map +1 -0
- package/dist/substrate/runtime/reconcile/spec.mjs +15 -0
- package/dist/substrate/runtime/reconcile/spec.mjs.map +1 -0
- package/dist/substrate/runtime/retry-policy.d.mts +2 -3
- package/dist/substrate/runtime/retry-policy.mjs.map +1 -1
- package/dist/substrate/runtime/routed-url.mjs.map +1 -1
- package/dist/substrate/runtime/runtime-decode.mjs.map +1 -1
- package/dist/substrate/runtime/scoped-http-server.mjs.map +1 -1
- package/dist/substrate/runtime/stage-and-swap/index.mjs +49 -12
- package/dist/substrate/runtime/stage-and-swap/index.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/chain-keyed-strategy-for.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/chain-probe-for.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/service.mjs +29 -17
- package/dist/substrate/runtime/strategy-registry/service.mjs.map +1 -1
- package/dist/substrate/runtime/subnet-broker.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/acquire-node.mjs +188 -56
- package/dist/substrate/runtime/supervisor/acquire-node.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/background-tasks.mjs +79 -65
- package/dist/substrate/runtime/supervisor/background-tasks.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/command-loop.mjs +82 -23
- package/dist/substrate/runtime/supervisor/command-loop.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/contribution-dispatcher.mjs +16 -0
- package/dist/substrate/runtime/supervisor/contribution-dispatcher.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/errors.mjs +11 -2
- package/dist/substrate/runtime/supervisor/errors.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/index.mjs +2 -0
- package/dist/substrate/runtime/supervisor/shutdown.mjs +6 -6
- package/dist/substrate/runtime/supervisor/shutdown.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs +60 -70
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/state.d.mts +1 -1
- package/dist/substrate/runtime/supervisor/state.mjs +1 -1
- package/dist/substrate/runtime/supervisor/state.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/teardown.mjs +10 -9
- package/dist/substrate/runtime/supervisor/teardown.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/wiring.mjs +6 -20
- package/dist/substrate/runtime/supervisor/wiring.mjs.map +1 -1
- package/dist/substrate/runtime/tar/reader.mjs +216 -0
- package/dist/substrate/runtime/tar/reader.mjs.map +1 -0
- package/dist/substrate/runtime/typed-env.mjs.map +1 -1
- package/dist/substrate/versioned-doc-schema.mjs.map +1 -1
- package/dist/substrate/versioned-doc-sync.mjs +5 -5
- package/dist/substrate/versioned-doc-sync.mjs.map +1 -1
- package/dist/surfaces/cli/command-tree.mjs +49 -7
- package/dist/surfaces/cli/command-tree.mjs.map +1 -1
- package/dist/surfaces/cli/commands/config.mjs +1 -1
- package/dist/surfaces/cli/commands/config.mjs.map +1 -1
- package/dist/surfaces/cli/commands/confirm-node.mjs.map +1 -1
- package/dist/surfaces/cli/commands/confirm.mjs.map +1 -1
- package/dist/surfaces/cli/commands/doctor.mjs +1 -1
- package/dist/surfaces/cli/commands/doctor.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune-picker-entry.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune-picker.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune.mjs +1 -1
- package/dist/surfaces/cli/commands/prune.mjs.map +1 -1
- package/dist/surfaces/cli/commands/snapshot.mjs +1 -1
- package/dist/surfaces/cli/commands/snapshot.mjs.map +1 -1
- package/dist/surfaces/cli/commands/status.mjs +1 -1
- package/dist/surfaces/cli/commands/status.mjs.map +1 -1
- package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
- package/dist/surfaces/cli/commands/wipe.mjs +3 -4
- package/dist/surfaces/cli/commands/wipe.mjs.map +1 -1
- package/dist/surfaces/cli/envelope.mjs.map +1 -1
- package/dist/surfaces/cli/errors.mjs.map +1 -1
- package/dist/surfaces/cli/flags.mjs.map +1 -1
- package/dist/surfaces/cli/index.mjs +45 -14
- package/dist/surfaces/cli/index.mjs.map +1 -1
- package/dist/surfaces/cli/output.mjs.map +1 -1
- package/dist/surfaces/cli/sysexits.mjs.map +1 -1
- package/dist/surfaces/tui/app.mjs +0 -21
- package/dist/surfaces/tui/app.mjs.map +1 -1
- package/dist/surfaces/tui/dashboard.mjs +0 -48
- package/dist/surfaces/tui/dashboard.mjs.map +1 -1
- package/dist/surfaces/tui/display-derivation.mjs +80 -16
- package/dist/surfaces/tui/display-derivation.mjs.map +1 -1
- package/dist/surfaces/tui/errors.mjs.map +1 -1
- package/dist/surfaces/tui/event-log.mjs +0 -9
- package/dist/surfaces/tui/event-log.mjs.map +1 -1
- package/dist/surfaces/tui/heartbeat.mjs.map +1 -1
- package/dist/surfaces/tui/index.mjs +2 -2
- package/dist/surfaces/tui/index.mjs.map +1 -1
- package/dist/surfaces/tui/input.mjs.map +1 -1
- package/dist/surfaces/tui/mode-detect.mjs.map +1 -1
- package/dist/surfaces/tui/mount-ink.mjs.map +1 -1
- package/dist/surfaces/tui/plain-renderer.mjs +26 -19
- package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
- package/dist/surfaces/tui/resource-table.mjs +8 -10
- package/dist/surfaces/tui/resource-table.mjs.map +1 -1
- package/images/postgres/Dockerfile +0 -1
- package/images/sui/Dockerfile +27 -54
- package/images/sui/entrypoint.sh +17 -179
- package/package.json +23 -15
- package/dashboard-ui/assets/index-Bmi1UtAg.js +0 -1356
- package/dashboard-ui/assets/index-D5EShVt4.js +0 -3
- package/dashboard-ui/assets/index-Deml9drg.css +0 -1
- package/dist/api/plugin-errors.d.mts +0 -7
- package/dist/api/plugin-errors.mjs +0 -10
- package/dist/api/plugin-errors.mjs.map +0 -1
- package/dist/cli/wirings/build-verb-layers.mjs +0 -42
- package/dist/cli/wirings/build-verb-layers.mjs.map +0 -1
- package/dist/contracts/capability-decl.d.mts +0 -40
- package/dist/orchestrators/built-in-plugin-layers.mjs +0 -54
- package/dist/orchestrators/built-in-plugin-layers.mjs.map +0 -1
- package/dist/orchestrators/run.mjs +0 -91
- package/dist/orchestrators/run.mjs.map +0 -1
- package/dist/orchestrators/runtime-composition.d.mts +0 -10
- package/dist/orchestrators/runtime-composition.mjs +0 -252
- package/dist/orchestrators/runtime-composition.mjs.map +0 -1
- package/dist/orchestrators/snapshot/capture-command.d.mts +0 -1
- package/dist/orchestrators/snapshot/capture-command.mjs +0 -25
- package/dist/orchestrators/snapshot/capture-command.mjs.map +0 -1
- package/dist/orchestrators/snapshot/pending-marker.mjs +0 -99
- package/dist/orchestrators/snapshot/pending-marker.mjs.map +0 -1
- package/dist/orchestrators/snapshot/recover-pending.mjs +0 -223
- package/dist/orchestrators/snapshot/recover-pending.mjs.map +0 -1
- package/dist/orchestrators/snapshot/state-document.d.mts +0 -1
- package/dist/orchestrators/snapshot/state-document.mjs +0 -58
- package/dist/orchestrators/snapshot/state-document.mjs.map +0 -1
- package/dist/plugins/account/spans.mjs +0 -16
- package/dist/plugins/account/spans.mjs.map +0 -1
- package/dist/plugins/account/variants/env.mjs +0 -24
- package/dist/plugins/account/variants/env.mjs.map +0 -1
- package/dist/plugins/account/variants/inline.mjs +0 -13
- package/dist/plugins/account/variants/inline.mjs.map +0 -1
- package/dist/plugins/account/variants/keystore.mjs +0 -91
- package/dist/plugins/account/variants/keystore.mjs.map +0 -1
- package/dist/plugins/action/spans.mjs +0 -11
- package/dist/plugins/action/spans.mjs.map +0 -1
- package/dist/plugins/coin/spans.mjs +0 -20
- package/dist/plugins/coin/spans.mjs.map +0 -1
- package/dist/plugins/deepbook/spans.mjs +0 -18
- package/dist/plugins/deepbook/spans.mjs.map +0 -1
- package/dist/plugins/faucet/spans.mjs +0 -12
- package/dist/plugins/faucet/spans.mjs.map +0 -1
- package/dist/plugins/package/spans.mjs +0 -14
- package/dist/plugins/package/spans.mjs.map +0 -1
- package/dist/plugins/postgres/codegen.mjs +0 -42
- package/dist/plugins/postgres/codegen.mjs.map +0 -1
- package/dist/plugins/postgres/connection.d.mts +0 -33
- package/dist/plugins/postgres/connection.mjs.map +0 -1
- package/dist/plugins/postgres/db-ensure.mjs.map +0 -1
- package/dist/plugins/postgres/errors.d.mts +0 -60
- package/dist/plugins/postgres/errors.mjs +0 -29
- package/dist/plugins/postgres/errors.mjs.map +0 -1
- package/dist/plugins/postgres/index.d.mts +0 -37
- package/dist/plugins/postgres/index.mjs +0 -68
- package/dist/plugins/postgres/index.mjs.map +0 -1
- package/dist/plugins/postgres/routable.mjs +0 -33
- package/dist/plugins/postgres/routable.mjs.map +0 -1
- package/dist/plugins/postgres/service.d.mts +0 -50
- package/dist/plugins/postgres/service.mjs +0 -179
- package/dist/plugins/postgres/service.mjs.map +0 -1
- package/dist/plugins/postgres/snapshot.mjs +0 -32
- package/dist/plugins/postgres/snapshot.mjs.map +0 -1
- package/dist/plugins/postgres/spans.mjs +0 -11
- package/dist/plugins/postgres/spans.mjs.map +0 -1
- package/dist/plugins/seal/spans.mjs +0 -18
- package/dist/plugins/seal/spans.mjs.map +0 -1
- package/dist/plugins/sui/spans.mjs +0 -17
- package/dist/plugins/sui/spans.mjs.map +0 -1
- package/dist/plugins/wallet/spans.mjs +0 -22
- package/dist/plugins/wallet/spans.mjs.map +0 -1
- package/dist/plugins/walrus/registry-publish.d.mts +0 -24
- package/dist/plugins/walrus/spans.mjs +0 -18
- package/dist/plugins/walrus/spans.mjs.map +0 -1
- package/dist/runtime/docker/logs.d.mts +0 -1
- package/dist/runtime/docker/logs.mjs +0 -34
- package/dist/runtime/docker/logs.mjs.map +0 -1
- package/dist/substrate/runtime/artifact-publisher/index.mjs +0 -86
- package/dist/substrate/runtime/artifact-publisher/index.mjs.map +0 -1
- package/dist/substrate/runtime/capability-sinks/index.d.mts +0 -1
- package/dist/substrate/runtime/capability-sinks/index.mjs +0 -3
- package/dist/substrate/runtime/capability-sinks/layer.d.mts +0 -1
- package/dist/substrate/runtime/capability-sinks/layer.mjs +0 -31
- package/dist/substrate/runtime/capability-sinks/layer.mjs.map +0 -1
- package/dist/substrate/runtime/capability-sinks/service.d.mts +0 -95
- package/dist/substrate/runtime/capability-sinks/service.mjs +0 -69
- package/dist/substrate/runtime/capability-sinks/service.mjs.map +0 -1
- package/dist/substrate/runtime/cross-process/lock.d.mts +0 -1
- package/dist/substrate/runtime/cross-process/lock.mjs +0 -23
- package/dist/substrate/runtime/cross-process/lock.mjs.map +0 -1
- package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs +0 -113
- package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs.map +0 -1
- package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs +0 -31
- package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs.map +0 -1
- package/dist/substrate/runtime/observability/formatter-registry.d.mts +0 -1
- package/dist/substrate/runtime/observability/formatter-registry.mjs +0 -48
- package/dist/substrate/runtime/observability/formatter-registry.mjs.map +0 -1
- package/dist/substrate/runtime/observability/span-store.d.mts +0 -1
- package/dist/substrate/runtime/observability/span-store.mjs +0 -110
- package/dist/substrate/runtime/observability/span-store.mjs.map +0 -1
- package/dist/substrate/runtime/observability/spans.d.mts +0 -1
- package/dist/substrate/runtime/observability/spans.mjs +0 -87
- package/dist/substrate/runtime/observability/spans.mjs.map +0 -1
- package/dist/substrate/runtime/projection/index.mjs +0 -4
- package/dist/substrate/runtime/projection/persisted.mjs +0 -213
- package/dist/substrate/runtime/projection/persisted.mjs.map +0 -1
- package/dist/substrate/runtime/scoped-multimap/service.mjs +0 -52
- package/dist/substrate/runtime/scoped-multimap/service.mjs.map +0 -1
- package/dist/substrate/runtime/scoped-ref-map/index.mjs +0 -2
- package/dist/substrate/runtime/scoped-ref-map/service.mjs +0 -83
- package/dist/substrate/runtime/scoped-ref-map/service.mjs.map +0 -1
- package/dist/substrate/runtime/state-store/index.mjs +0 -3
- package/dist/substrate/runtime/state-store/schema.d.mts +0 -1
- package/dist/substrate/runtime/state-store/schema.mjs +0 -41
- package/dist/substrate/runtime/state-store/schema.mjs.map +0 -1
- package/dist/substrate/runtime/state-store/service.d.mts +0 -1
- package/dist/substrate/runtime/state-store/service.mjs +0 -145
- package/dist/substrate/runtime/state-store/service.mjs.map +0 -1
- package/dist/substrate/runtime/sui-execute/index.mjs.map +0 -1
- package/dist/substrate/runtime/sui-execute/sign-and-dispatch.mjs.map +0 -1
- package/dist/substrate/runtime/sui-ledger/object-ref.mjs.map +0 -1
- package/dist/substrate/runtime/sui-move-build/index.mjs.map +0 -1
- package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs +0 -151
- package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs.map +0 -1
- package/dist/substrate/state-store.d.mts +0 -1
- /package/dist/orchestrators/{snapshot/pending-marker.d.mts → codegen/id-config.d.mts} +0 -0
- /package/dist/orchestrators/snapshot/{recover-pending.d.mts → interrupted-restore.d.mts} +0 -0
- /package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/sign-and-dispatch.d.mts +0 -0
- /package/dist/plugins/{postgres/snapshot.d.mts → sui/ledger/object-ref.d.mts} +0 -0
- /package/dist/{substrate/runtime/sui-move-build → plugins/sui/move}/index.d.mts +0 -0
- /package/dist/substrate/runtime/{state-store/index.d.mts → reconcile/graph.d.mts} +0 -0
- /package/dist/substrate/runtime/{cross-process/snapshot-reservation.d.mts → supervisor/contribution-dispatcher.d.mts} +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeJsonTextSync } from "../runtime-decode.mjs";
|
|
2
2
|
import { versionedDocSchema } from "../../versioned-doc-schema.mjs";
|
|
3
|
+
import { RosterHolderSchema } from "../../cross-process.mjs";
|
|
3
4
|
import { selfPid } from "../cross-process/self-pid.mjs";
|
|
4
5
|
import { PortBrokerError } from "../errors.mjs";
|
|
5
6
|
import { atomicWriteFileExclusiveSync } from "../atomic-write.mjs";
|
|
6
|
-
import { decodeJsonTextSync } from "../runtime-decode.mjs";
|
|
7
|
-
import { RosterHolderSchema } from "../../cross-process.mjs";
|
|
8
7
|
import { checkHolderLiveness, ownHolder } from "../cross-process/liveness.mjs";
|
|
8
|
+
import { RuntimeRoot } from "../paths.mjs";
|
|
9
9
|
import { Context, Effect, Layer, Ref, Schema } from "effect";
|
|
10
10
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
12
11
|
import { join } from "node:path";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { createServer } from "node:net";
|
|
14
14
|
//#region src/substrate/runtime/port-broker/service.ts
|
|
15
15
|
/** Default scan window. Sized for "tens of plugins per stack" with
|
|
@@ -172,11 +172,10 @@ const layerPortBroker = Layer.effect(PortBrokerService, Effect.gen(function* ()
|
|
|
172
172
|
* interrupt, typed failure, or a non-`kept` success outcome.
|
|
173
173
|
* Without this, an interrupt landing between the reservation write
|
|
174
174
|
* (in `acquirePortReservation`) and the finalizer arm (in
|
|
175
|
-
* `finishAllocation`)
|
|
176
|
-
* only
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* `checkHolderLiveness` report `alive`, marking it busy for the
|
|
175
|
+
* `finishAllocation`) would leave `port-locks/<port>.json` on disk
|
|
176
|
+
* while only dropping the in-process slot. Its `holder` is this
|
|
177
|
+
* still-alive process, so every future `allocate` of that port then
|
|
178
|
+
* sees `checkHolderLiveness` report `alive`, marking it busy for the
|
|
180
179
|
* whole supervisor lifetime.
|
|
181
180
|
*
|
|
182
181
|
* The only path that intentionally KEEPS both the slot and the
|
|
@@ -264,20 +263,10 @@ const layerPortBroker = Layer.effect(PortBrokerService, Effect.gen(function* ()
|
|
|
264
263
|
reason: "no-free-port",
|
|
265
264
|
detail: `no free port found in [${windowStart}, ${windowStart + windowSize}) for owner '${owner}'`
|
|
266
265
|
}));
|
|
267
|
-
})
|
|
268
|
-
owner: opts.owner ?? DEFAULT_OWNER,
|
|
269
|
-
preferredPort: opts.preferredPort ?? -1,
|
|
270
|
-
probeHost: opts.probeHost ?? "127.0.0.1",
|
|
271
|
-
windowStart: opts.windowHint?.start ?? DEFAULT_PORT_WINDOW.start,
|
|
272
|
-
windowSize: opts.windowHint?.size ?? DEFAULT_PORT_WINDOW.size
|
|
273
|
-
} }));
|
|
266
|
+
});
|
|
274
267
|
const finishAllocation = (port, owner, dropFn, reservation) => Effect.gen(function* () {
|
|
275
268
|
const release = Effect.all([dropFn(port), reservation.release], { discard: true });
|
|
276
269
|
yield* Effect.addFinalizer(() => release.pipe(Effect.uninterruptible));
|
|
277
|
-
yield* Effect.annotateCurrentSpan({
|
|
278
|
-
"portBroker.port": port,
|
|
279
|
-
"portBroker.owner": owner
|
|
280
|
-
});
|
|
281
270
|
yield* Effect.logDebug(`port-broker allocated ${port} (owner=${owner})`);
|
|
282
271
|
return {
|
|
283
272
|
port,
|
|
@@ -305,13 +294,7 @@ const probePort = (port, host) => host === "0.0.0.0" ? Effect.gen(function* () {
|
|
|
305
294
|
const loopback = yield* probeSinglePort(port, "127.0.0.1");
|
|
306
295
|
if (loopback._tag !== "ok") return loopback;
|
|
307
296
|
return yield* probeSinglePort(port, host);
|
|
308
|
-
})
|
|
309
|
-
host,
|
|
310
|
-
port
|
|
311
|
-
} })) : probeSinglePort(port, host).pipe(Effect.withSpan("substrate.portBroker.probe", { attributes: {
|
|
312
|
-
host,
|
|
313
|
-
port
|
|
314
|
-
} }));
|
|
297
|
+
}) : probeSinglePort(port, host);
|
|
315
298
|
const probeSinglePort = (port, host) => Effect.callback((resume) => {
|
|
316
299
|
let settled = false;
|
|
317
300
|
let server = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["createNetServer"],"sources":["../../../../src/substrate/runtime/port-broker/service.ts"],"sourcesContent":["// Port broker — substrate-level service.\n//\n// Architecture (`notes/redesign/architecture.md` § \"What's collapsed\"):\n// resource broker for host ports. Plugins that need to bind a host\n// HTTP server or publish a container port on the host yield this\n// service and call `allocate({...})`. The broker:\n//\n// 1. Tracks in-process allocations in a `Ref<Map<port, Holder>>` so\n// two sibling plugins in the same stack don't both pick the same\n// port between bind-probe and `server.listen`.\n// 2. Claims a runtime-root scoped port reservation file before the\n// bind probe. This closes the cross-process probe→listen race:\n// two independent `devstack apply` processes sharing a state dir\n// cannot both choose the same candidate while neither real server\n// is listening yet.\n// 3. Verifies kernel-level availability by binding a transient\n// `net.Server` on the candidate port and closing it.\n// The OS's `EADDRINUSE` remains the final collision check for\n// non-devstack processes and stale reservation gaps.\n// 4. Releases on scope close. The broker hands back a `release`\n// Effect AND installs a scope finalizer; the architecture's\n// \"scope-local, never module-level\" invariant is honoured by\n// construction (the Layer is scope-local, the entries die with\n// the scope).\n//\n// Sequential search: callers may pass `preferredPort` (tried first) and\n// an optional `windowHint: { start, size }` (the scan window if\n// preferred is busy or absent). The default window —\n// `DEFAULT_PORT_WINDOW` below — is intentionally wide enough for any\n// realistic in-process devstack; plugins do NOT freelance per-kind\n// window starts. The substrate stays name-blind: there is no `PortKind`\n// literal; plugins are opaque port-holders from the broker's view.\n//\n// Cross-process safety: the reservation file is scoped to the runtime\n// root, not the stack root, because host ports are machine-global. The\n// stack-scoped supervisor lock serializes acquires of the SAME stack;\n// these reservation files serialize host-port choices across DIFFERENT\n// stacks and apps that share a state dir.\n\nimport { randomUUID } from 'node:crypto';\nimport { readFileSync, unlinkSync } from 'node:fs';\nimport {\n\tcreateServer as createNetServer,\n\ttype AddressInfo,\n\ttype Server as NetServer,\n} from 'node:net';\nimport { join } from 'node:path';\n\nimport { Context, Effect, Layer, Ref, Schema, Scope } from 'effect';\n\nimport { RosterHolderSchema, type RosterHolder } from '../../cross-process.ts';\nimport { atomicWriteFileExclusiveSync } from '../atomic-write.ts';\nimport { checkHolderLiveness, ownHolder } from '../cross-process/liveness.ts';\nimport { selfPid } from '../cross-process/self-pid.ts';\nimport { PortBrokerError } from '../errors.ts';\nimport { RuntimeRoot } from '../paths.ts';\nimport { decodeJsonTextSync } from '../runtime-decode.ts';\nimport { versionedDocSchema } from '../../versioned-doc-schema.ts';\n\n// ----------------------------------------------------------------------\n// Public shape\n// ----------------------------------------------------------------------\n\n/** Host interface used by the transient bind probe. Callers binding a\n * real server on loopback should use the default. Callers that hand\n * the port to Docker's all-interface publish path should pass\n * `'0.0.0.0'` so the probe asks the same kernel question. */\nexport type PortProbeHost = '127.0.0.1' | '0.0.0.0';\n\n/** Optional per-call window override. The broker scans\n * `[start, start + size)` in order when `preferredPort` is busy or\n * absent. Plugins with UX-pinned ports (e.g. a dev wallet whose\n * legacy adapters auto-connect to a specific range) pass this so the\n * fall-back range stays predictable; everyone else gets the default. */\nexport interface PortAllocationWindow {\n\treadonly start: number;\n\treadonly size?: number;\n}\n\n/** Default scan window. Sized for \"tens of plugins per stack\" with\n * parallel-stack overhead headroom. 1000 ports is more than enough for\n * any realistic in-process devstack; the forward-scan only ever\n * reaches the end if EVERY port in the window is taken (effectively\n * never on dev machines). Start chosen to land between common dev\n * defaults (3000-5173, 8000-8080) and the ephemeral range so a\n * sibling Vite/Next process doesn't accidentally collide on the\n * first probe. */\nexport const DEFAULT_PORT_WINDOW: Required<PortAllocationWindow> = {\n\tstart: 39200,\n\tsize: 1000,\n};\n\n/** Input to `allocate`. */\nexport interface AllocateOptions {\n\t/** Optional caller hint. The broker tries this FIRST; if the port is\n\t * already held in-process the call FAILS with\n\t * `reason: 'preferred-busy'` (the caller asked specifically for\n\t * this port and got an obvious collision). If the bind-probe fails\n\t * with EADDRINUSE the broker falls through to the window scan —\n\t * preferred is a HINT for the kernel-collision case, but a HARD\n\t * CHOICE against in-process collision. */\n\treadonly preferredPort?: number;\n\t/** Interface to bind during the kernel probe. Defaults to\n\t * `127.0.0.1`, matching host-loopback servers. Docker\n\t * `-p host:container` publishes on all interfaces, so Docker-backed\n\t * callers pass `0.0.0.0`. */\n\treadonly probeHost?: PortProbeHost;\n\t/** Optional per-call window override; defaults to\n\t * `DEFAULT_PORT_WINDOW`. */\n\treadonly windowHint?: PortAllocationWindow;\n\t/** Optional free-form owner label for diagnostics (`no-free-port`\n\t * error detail). The broker treats this as opaque; pass something\n\t * short and human-readable like `'wallet'` or `'sui:rpc'`. */\n\treadonly owner?: string;\n}\n\n/** Successful allocation. `release` is provided in addition to the\n * scope finalizer so tests / orchestrators can drive release\n * explicitly without unwinding the whole scope. Calling `release`\n * twice is a no-op. */\nexport interface AllocatedPort {\n\treadonly port: number;\n\treadonly release: Effect.Effect<void>;\n}\n\n/** Service shape — what plugins yield from Context. */\nexport interface PortBroker {\n\t/**\n\t * Allocate a port. The returned port is exclusive within THIS\n\t * process (the broker's `Ref` map) and best-effort-exclusive across\n\t * devstack processes that share a runtime root (port reservation\n\t * file), then verified against the kernel bind table on `probeHost`.\n\t *\n\t * Scope-bound release: the broker installs a finalizer on the\n\t * surrounding scope; callers can additionally invoke\n\t * `result.release` explicitly.\n\t *\n\t * Failures (`PortBrokerError`):\n\t * - `preferred-busy` — caller's `preferredPort` is held by another\n\t * in-process allocation on the same stack.\n\t * - `no-free-port` — the scan window was exhausted without a\n\t * probe-pass candidate.\n\t * - `bind-probe-failed` — non-EADDRINUSE error from the OS bind\n\t * probe (EACCES on a privileged port etc.).\n\t * - `reservation-failed` — port reservation file IO failed.\n\t */\n\treadonly allocate: (\n\t\topts?: AllocateOptions,\n\t) => Effect.Effect<AllocatedPort, PortBrokerError, Scope.Scope>;\n}\n\n// ----------------------------------------------------------------------\n// Internal state\n// ----------------------------------------------------------------------\n\nconst DEFAULT_OWNER = 'unknown';\n\n/** In-process holder for a port. Carries the owner string for\n * diagnostics (e.g. surfaced in `no-free-port` errors). */\ninterface Holder {\n\treadonly owner: string;\n}\n\ntype State = ReadonlyMap<number, Holder>;\n\n// Reservation-doc shape. NOTE: this changed incompatibly from an\n// earlier `{ version: 1, kind, ... }` to `{ version: 1, owner, ... }`\n// without bumping the version. A version bump alone would NOT recover\n// a SIGKILL'd prior-version leftover (an old body still fails decode\n// under any new schema and would block its port forever). Instead the\n// recovery is behavioural: `acquirePortReservation` treats an EEXIST\n// file it cannot decode as a stale artifact and self-heals it\n// (best-effort unlink + retry), so a leftover incompatible reservation\n// clears itself on the next allocate of that port.\nconst PORT_RESERVATION_VERSION = 1 as const;\n\nconst PortReservationDocSchema = versionedDocSchema(PORT_RESERVATION_VERSION, {\n\tport: Schema.Number,\n\towner: Schema.String,\n\townerId: Schema.String,\n\tholder: RosterHolderSchema,\n});\n\ntype PortReservationDoc = Schema.Schema.Type<typeof PortReservationDocSchema>;\n\ninterface PortReservation {\n\treadonly path: string;\n\treadonly ownerId: string;\n\treadonly release: Effect.Effect<void>;\n}\n\ntype ReservationAttempt =\n\t| { readonly _tag: 'acquired'; readonly reservation: PortReservation }\n\t| { readonly _tag: 'busy' }\n\t| { readonly _tag: 'failed'; readonly detail: string; readonly cause: unknown };\n\ntype ReservationWriteAttempt =\n\t| { readonly _tag: 'written' }\n\t| { readonly _tag: 'race' }\n\t| { readonly _tag: 'exists'; readonly doc: PortReservationDoc | null }\n\t| { readonly _tag: 'failed'; readonly cause: unknown };\n\nconst portReservationPath = (root: string, port: number): string =>\n\tjoin(root, 'port-locks', `${port}.json`);\n\nconst parseReservationDoc = (raw: string): PortReservationDoc | null => {\n\ttry {\n\t\treturn decodeJsonTextSync(PortReservationDocSchema, raw, {\n\t\t\tsource: 'port reservation',\n\t\t\tmkError: (issue) => issue,\n\t\t});\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst tryWriteReservationSync = (\n\tpath: string,\n\tdoc: PortReservationDoc,\n): ReservationWriteAttempt => {\n\t// `atomicWriteFileExclusiveSync` writes a tempfile under O_EXCL +\n\t// fsync, then `linkSync`s it onto the final path. POSIX `link(2)`\n\t// is atomic AND fails with `EEXIST` if the target exists — that is\n\t// the exclusive-create primitive `rename` lacks (rename clobbers).\n\t// A sibling pid that wins the race becomes the canonical reservation\n\t// at `path`; we observe it via the EEXIST error and surface the\n\t// winner's doc to the caller (`acquirePortReservation`), which then\n\t// decides busy-vs-stale based on the holder's liveness.\n\ttry {\n\t\tatomicWriteFileExclusiveSync(path, `${JSON.stringify(doc)}\\n`, { mode: 0o600 });\n\t\treturn { _tag: 'written' };\n\t} catch (cause) {\n\t\tconst code = (cause as NodeJS.ErrnoException).code;\n\t\tif (code === 'EEXIST') {\n\t\t\ttry {\n\t\t\t\treturn { _tag: 'exists', doc: parseReservationDoc(readFileSync(path, 'utf8')) };\n\t\t\t} catch {\n\t\t\t\t// The winning reservation file was removed between our\n\t\t\t\t// link-EEXIST and our read (a peer raced release). Retry.\n\t\t\t\treturn { _tag: 'race' };\n\t\t\t}\n\t\t}\n\t\treturn { _tag: 'failed', cause };\n\t}\n};\n\nconst unlinkReservationIfOwnerSync = (path: string, ownerId: string): void => {\n\ttry {\n\t\tconst doc = parseReservationDoc(readFileSync(path, 'utf8'));\n\t\tif (doc?.ownerId === ownerId) {\n\t\t\tunlinkSync(path);\n\t\t}\n\t} catch {\n\t\t// Missing or unreadable: release is best-effort. A peer never\n\t\t// reclaims unreadable reservations; it simply skips that port.\n\t}\n};\n\nconst reclaimReservationIfOwnerSync = (path: string, ownerId: string): boolean => {\n\ttry {\n\t\tconst doc = parseReservationDoc(readFileSync(path, 'utf8'));\n\t\tif (doc?.ownerId !== ownerId) return false;\n\t\tunlinkSync(path);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\n/** Best-effort removal of an EXISTING reservation file whose body\n * cannot be decoded (a leftover from an incompatible-version\n * supervisor, or a torn write). Such a file carries no readable\n * `ownerId`/`holder`, so neither `reclaimReservationIfOwnerSync` nor\n * liveness can ever clear it; without this it would skip that one\n * port for every future process for the supervisor's lifetime. We\n * treat an undecodable body as a stale artifact and unlink it so the\n * next exclusive-create can win. Returns whether the unlink ran\n * (false if the file vanished first, i.e. a peer already cleared it)\n * so the caller can bound its retry. */\nconst reclaimUndecodableReservationSync = (path: string): boolean => {\n\ttry {\n\t\tunlinkSync(path);\n\t\treturn true;\n\t} catch {\n\t\t// Already gone (peer reclaimed it) or unremovable; either way\n\t\t// the next loop turn re-evaluates via exclusive-create.\n\t\treturn false;\n\t}\n};\n\nconst acquirePortReservation = (\n\troot: string,\n\tport: number,\n\towner: string,\n): Effect.Effect<ReservationAttempt> =>\n\tEffect.gen(function* () {\n\t\tconst path = portReservationPath(root, port);\n\t\tconst ownerId = `${selfPid()}-${randomUUID().slice(0, 8)}`;\n\t\tconst holder: RosterHolder = ownHolder();\n\t\tconst doc: PortReservationDoc = {\n\t\t\tversion: PORT_RESERVATION_VERSION,\n\t\t\tport,\n\t\t\towner,\n\t\t\townerId,\n\t\t\tholder,\n\t\t};\n\n\t\t// Bounds the self-heal: an undecodable EEXIST body is reclaimed at\n\t\t// most once. If we see another undecodable body after that (a peer\n\t\t// keeps rewriting garbage), give up with `busy` rather than spin.\n\t\tlet reclaimedUndecodable = false;\n\t\twhile (true) {\n\t\t\tconst attempt = tryWriteReservationSync(path, doc);\n\t\t\tif (attempt._tag === 'written') {\n\t\t\t\treturn {\n\t\t\t\t\t_tag: 'acquired',\n\t\t\t\t\treservation: {\n\t\t\t\t\t\tpath,\n\t\t\t\t\t\townerId,\n\t\t\t\t\t\trelease: Effect.sync(() => unlinkReservationIfOwnerSync(path, ownerId)),\n\t\t\t\t\t},\n\t\t\t\t} as const;\n\t\t\t}\n\t\t\tif (attempt._tag === 'race') continue;\n\t\t\tif (attempt._tag === 'failed') {\n\t\t\t\treturn {\n\t\t\t\t\t_tag: 'failed',\n\t\t\t\t\tdetail: `port reservation ${path} could not be written`,\n\t\t\t\t\tcause: attempt.cause,\n\t\t\t\t} as const;\n\t\t\t}\n\t\t\tif (attempt.doc === null) {\n\t\t\t\t// EEXIST file we cannot decode (stale incompatible-version\n\t\t\t\t// leftover or torn write). Self-heal once: unlink it and retry\n\t\t\t\t// the exclusive-create so a live port is not lost permanently.\n\t\t\t\tif (!reclaimedUndecodable && reclaimUndecodableReservationSync(path)) {\n\t\t\t\t\treclaimedUndecodable = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn { _tag: 'busy' } as const;\n\t\t\t}\n\n\t\t\tconst liveness = yield* checkHolderLiveness(attempt.doc.holder).pipe(\n\t\t\t\tEffect.catch(() => Effect.succeed('alive' as const)),\n\t\t\t);\n\t\t\tif (liveness === 'alive') return { _tag: 'busy' } as const;\n\t\t\tif (!reclaimReservationIfOwnerSync(path, attempt.doc.ownerId)) {\n\t\t\t\treturn { _tag: 'busy' } as const;\n\t\t\t}\n\t\t}\n\t});\n\n// ----------------------------------------------------------------------\n// Service tag + Layer\n// ----------------------------------------------------------------------\n\nexport class PortBrokerService extends Context.Service<PortBrokerService, PortBroker>()(\n\t'@devstack/substrate/PortBroker',\n) {}\n\n/**\n * Layer that materializes one `PortBroker` per stack scope.\n *\n * The broker's state lives in a `Ref` captured by the Layer's\n * `Effect.gen`; closing the Layer's scope drops every allocation.\n * Parallel stacks each get their own broker (Layer-driven), and they\n * coordinate cross-process via runtime-root reservation files plus the\n * kernel's bind table — never via shared in-process state.\n */\nexport const layerPortBroker: Layer.Layer<PortBrokerService, never, RuntimeRoot> = Layer.effect(\n\tPortBrokerService,\n\tEffect.gen(function* () {\n\t\tconst runtimeRoot = yield* RuntimeRoot;\n\t\tconst reservationRoot = runtimeRoot.root;\n\t\tconst state = yield* Ref.make<State>(new Map());\n\n\t\tconst tryReserve = (port: number, owner: string): Effect.Effect<boolean> =>\n\t\t\tRef.modify<State, boolean>(state, (current) => {\n\t\t\t\tif (current.has(port)) return [false, current];\n\t\t\t\tconst next = new Map(current);\n\t\t\t\tnext.set(port, { owner });\n\t\t\t\treturn [true, next];\n\t\t\t});\n\n\t\tconst drop = (port: number): Effect.Effect<void> =>\n\t\t\tRef.update(state, (current) => {\n\t\t\t\tif (!current.has(port)) return current;\n\t\t\t\tconst next = new Map(current);\n\t\t\t\tnext.delete(port);\n\t\t\t\treturn next;\n\t\t\t});\n\n\t\t/** Outcome of one candidate-port attempt.\n\t\t *\n\t\t * `kept` means `finishAllocation` armed its scope finalizer; the\n\t\t * in-process slot is now owned by that finalizer and we MUST\n\t\t * NOT drop it on this critical region's exit. Any other outcome\n\t\t * (busy / probe-in-use / probe-failed / reservation-failed)\n\t\t * must release the slot before we hand control back to the\n\t\t * caller. */\n\t\ttype SlotOutcome =\n\t\t\t| { readonly _tag: 'kept'; readonly allocated: AllocatedPort }\n\t\t\t| { readonly _tag: 'busy' }\n\t\t\t| {\n\t\t\t\t\treadonly _tag: 'reservation-failed';\n\t\t\t\t\treadonly detail: string;\n\t\t\t\t\treadonly cause: unknown;\n\t\t\t }\n\t\t\t| { readonly _tag: 'probe-failed'; readonly cause: unknown };\n\n\t\t/** Reserve `port` in the in-process Map, run the probe+reservation\n\t\t * chain through to either (a) `finishAllocation` arming its scope\n\t\t * finalizer or (b) a clean release of BOTH the in-process slot\n\t\t * AND the on-disk reservation file.\n\t\t *\n\t\t * Interrupt safety: the critical region is wrapped in\n\t\t * `Effect.acquireUseRelease`, whose `release` step is\n\t\t * uninterruptible while its `use` step is NOT (Effect v4). The\n\t\t * acquired `PortReservation` is created INSIDE `use` (after the\n\t\t * on-disk file is written), so the `use` body stashes it in\n\t\t * `reservationCell`; the uninterruptible `release` reads that cell\n\t\t * and unlinks the reservation file on ANY non-`kept` exit —\n\t\t * interrupt, typed failure, or a non-`kept` success outcome.\n\t\t * Without this, an interrupt landing between the reservation write\n\t\t * (in `acquirePortReservation`) and the finalizer arm (in\n\t\t * `finishAllocation`) left the file on disk while the old release\n\t\t * only dropped the in-process slot — leaking\n\t\t * `port-locks/<port>.json`. Its `holder` is this still-alive\n\t\t * process, so every future `allocate` of that port then saw\n\t\t * `checkHolderLiveness` report `alive`, marking it busy for the\n\t\t * whole supervisor lifetime.\n\t\t *\n\t\t * The only path that intentionally KEEPS both the slot and the\n\t\t * file is the `kept` outcome, where ownership transfers to the\n\t\t * scope finalizer (see `finishAllocation`); `reservationCell` is\n\t\t * cleared there so `release` is a no-op for it.\n\t\t * (`unlinkReservationIfOwnerSync` is owner-id-guarded and\n\t\t * idempotent, so a stray double-release is harmless — clearing\n\t\t * the cell just avoids the redundant stat.)\n\t\t */\n\t\tconst attemptSlot = (\n\t\t\tport: number,\n\t\t\towner: string,\n\t\t\tprobeHost: PortProbeHost,\n\t\t): Effect.Effect<SlotOutcome | null, never, Scope.Scope> => {\n\t\t\t// Visible to BOTH the `use` body (fills it once the reservation\n\t\t\t// file exists) and the uninterruptible `release` callback\n\t\t\t// (unlinks it on any non-`kept` exit). On-disk analogue of the\n\t\t\t// in-process slot.\n\t\t\tlet reservationCell: PortReservation | null = null;\n\t\t\treturn Effect.acquireUseRelease(\n\t\t\t\ttryReserve(port, owner),\n\t\t\t\t(reserved): Effect.Effect<SlotOutcome | null, never, Scope.Scope> => {\n\t\t\t\t\tif (!reserved) return Effect.succeed(null);\n\t\t\t\t\treturn Effect.gen(function* () {\n\t\t\t\t\t\tconst reservation = yield* acquirePortReservation(reservationRoot, port, owner);\n\t\t\t\t\t\tif (reservation._tag === 'busy') return { _tag: 'busy' as const };\n\t\t\t\t\t\tif (reservation._tag === 'failed') {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t_tag: 'reservation-failed' as const,\n\t\t\t\t\t\t\t\tdetail: reservation.detail,\n\t\t\t\t\t\t\t\tcause: reservation.cause,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// File is now on disk. Hand it to the uninterruptible\n\t\t\t\t\t\t// `release` step so an interrupt at the `probePort` /\n\t\t\t\t\t\t// `finishAllocation` boundary cannot leak it.\n\t\t\t\t\t\treservationCell = reservation.reservation;\n\t\t\t\t\t\tconst ok = yield* probePort(port, probeHost);\n\t\t\t\t\t\tif (ok._tag === 'ok') {\n\t\t\t\t\t\t\tconst allocated = yield* finishAllocation(port, owner, drop, reservation.reservation);\n\t\t\t\t\t\t\t// Ownership transferred to the scope finalizer; the\n\t\t\t\t\t\t\t// `release` step must NOT also unlink the file.\n\t\t\t\t\t\t\treservationCell = null;\n\t\t\t\t\t\t\treturn { _tag: 'kept' as const, allocated };\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Probe missed. The uninterruptible `release` step below\n\t\t\t\t\t\t// unlinks the reservation file (via `reservationCell`) and\n\t\t\t\t\t\t// drops the in-process slot, so a single owner cleans up\n\t\t\t\t\t\t// both even if an interrupt preempts us right here.\n\t\t\t\t\t\tif (ok._tag === 'probe-failed') {\n\t\t\t\t\t\t\treturn { _tag: 'probe-failed' as const, cause: ok.cause };\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { _tag: 'busy' as const };\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t(reserved, exit) => {\n\t\t\t\t\t// Slot + reservation release (uninterruptible). Drop the\n\t\t\t\t\t// in-process entry AND unlink the on-disk reservation\n\t\t\t\t\t// UNLESS we transferred ownership to a scope finalizer\n\t\t\t\t\t// (the `kept` outcome, which cleared `reservationCell`).\n\t\t\t\t\t// Interrupts, typed failures, and non-`kept` success\n\t\t\t\t\t// outcomes all flow through here, closing both the Map\n\t\t\t\t\t// leak and the port-locks/*.json leak (the latter\n\t\t\t\t\t// previously persisted until the supervisor exited).\n\t\t\t\t\tif (!reserved) return Effect.void;\n\t\t\t\t\tconst kept = exit._tag === 'Success' && exit.value?._tag === 'kept';\n\t\t\t\t\tif (kept) return Effect.void;\n\t\t\t\t\tconst releaseReservation =\n\t\t\t\t\t\treservationCell !== null ? reservationCell.release : Effect.void;\n\t\t\t\t\treturn Effect.all([drop(port), releaseReservation], { discard: true });\n\t\t\t\t},\n\t\t\t);\n\t\t};\n\n\t\tconst allocate: PortBroker['allocate'] = (opts = {}) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst owner = opts.owner ?? DEFAULT_OWNER;\n\t\t\t\tconst probeHost = opts.probeHost ?? '127.0.0.1';\n\t\t\t\tconst windowStart = opts.windowHint?.start ?? DEFAULT_PORT_WINDOW.start;\n\t\t\t\tconst windowSize = opts.windowHint?.size ?? DEFAULT_PORT_WINDOW.size;\n\n\t\t\t\t// 1. Preferred-port path. The broker honours `preferredPort`\n\t\t\t\t// as a strong hint, but refuses in-process collisions\n\t\t\t\t// (architecture §6: same-stack siblings MUST NOT trample).\n\t\t\t\tif (opts.preferredPort !== undefined) {\n\t\t\t\t\tconst outcome = yield* attemptSlot(opts.preferredPort, owner, probeHost);\n\t\t\t\t\tif (outcome === null) {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'preferred-busy',\n\t\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\t\t`preferred port ${opts.preferredPort} (owner=${owner}) is ` +\n\t\t\t\t\t\t\t\t\t`already held by another allocation in this stack`,\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\t\tif (outcome._tag === 'kept') return outcome.allocated;\n\t\t\t\t\tif (outcome._tag === 'reservation-failed') {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'reservation-failed',\n\t\t\t\t\t\t\t\tdetail: outcome.detail,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\tif (outcome._tag === 'probe-failed') {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'bind-probe-failed',\n\t\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\t\t`bind probe on preferred port ${opts.preferredPort} ` +\n\t\t\t\t\t\t\t\t\t`(owner=${owner}, host=${probeHost}) failed with non-EADDRINUSE error`,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\t// `busy` (either cross-process reservation held by a\n\t\t\t\t\t// live peer OR kernel-level in-use) — fall through to\n\t\t\t\t\t// the window scan; the caller's preferred was a HINT.\n\t\t\t\t}\n\n\t\t\t\t// 2. Forward-scan the window.\n\t\t\t\tfor (let p = windowStart; p < windowStart + windowSize; p++) {\n\t\t\t\t\tconst outcome = yield* attemptSlot(p, owner, probeHost);\n\t\t\t\t\tif (outcome === null) continue;\n\t\t\t\t\tif (outcome._tag === 'kept') return outcome.allocated;\n\t\t\t\t\tif (outcome._tag === 'reservation-failed') {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'reservation-failed',\n\t\t\t\t\t\t\t\tdetail: outcome.detail,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\tif (outcome._tag === 'probe-failed') {\n\t\t\t\t\t\t// A privileged port (EACCES) in the middle of the\n\t\t\t\t\t\t// window is unrecoverable for this scan. Surface\n\t\t\t\t\t\t// immediately rather than thrash the whole window.\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'bind-probe-failed',\n\t\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\t\t`bind probe on port ${p} (owner=${owner}, host=${probeHost}) failed ` +\n\t\t\t\t\t\t\t\t\t`with non-EADDRINUSE error`,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\t// `busy` — next candidate.\n\t\t\t\t}\n\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\treason: 'no-free-port',\n\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t`no free port found in [${windowStart}, ` +\n\t\t\t\t\t\t\t`${windowStart + windowSize}) for owner '${owner}'`,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}).pipe(\n\t\t\t\tEffect.withSpan('substrate.portBroker.allocate', {\n\t\t\t\t\tattributes: {\n\t\t\t\t\t\towner: opts.owner ?? DEFAULT_OWNER,\n\t\t\t\t\t\tpreferredPort: opts.preferredPort ?? -1,\n\t\t\t\t\t\tprobeHost: opts.probeHost ?? '127.0.0.1',\n\t\t\t\t\t\twindowStart: opts.windowHint?.start ?? DEFAULT_PORT_WINDOW.start,\n\t\t\t\t\t\twindowSize: opts.windowHint?.size ?? DEFAULT_PORT_WINDOW.size,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\n\t\tconst finishAllocation = (\n\t\t\tport: number,\n\t\t\towner: string,\n\t\t\tdropFn: (p: number) => Effect.Effect<void>,\n\t\t\treservation: PortReservation,\n\t\t): Effect.Effect<AllocatedPort, never, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\t// Scope finalizer — drops the entry when the surrounding\n\t\t\t\t// scope closes. Uninterruptible so a Ctrl-C double-tap\n\t\t\t\t// doesn't leave the entry dangling in the broker's Ref.\n\t\t\t\tconst release = Effect.all([dropFn(port), reservation.release], {\n\t\t\t\t\tdiscard: true,\n\t\t\t\t});\n\t\t\t\tyield* Effect.addFinalizer(() => release.pipe(Effect.uninterruptible));\n\t\t\t\tyield* Effect.annotateCurrentSpan({\n\t\t\t\t\t'portBroker.port': port,\n\t\t\t\t\t'portBroker.owner': owner,\n\t\t\t\t});\n\t\t\t\tyield* Effect.logDebug(`port-broker allocated ${port} (owner=${owner})`);\n\t\t\t\treturn {\n\t\t\t\t\tport,\n\t\t\t\t\trelease,\n\t\t\t\t} satisfies AllocatedPort;\n\t\t\t});\n\n\t\treturn PortBrokerService.of({ allocate });\n\t}),\n);\n\n// ----------------------------------------------------------------------\n// Kernel-level bind probe\n// ----------------------------------------------------------------------\n\n/** Probe result. `'in-use'` is the recoverable case (try the next\n * port); `'probe-failed'` is the unrecoverable case (caller's\n * problem — surface). */\ntype ProbeResult =\n\t| { readonly _tag: 'ok' }\n\t| { readonly _tag: 'in-use' }\n\t| { readonly _tag: 'probe-failed'; readonly cause: unknown };\n\n/**\n * Bind a transient `net.Server` on `<host>:<port>` and close it.\n * The OS atomically rejects with `EADDRINUSE` if another process\n * already holds the port — that's our cross-process collision check.\n *\n * The default probe is loopback-only because most servers bind\n * loopback. Docker-backed callers pass `0.0.0.0`; on Docker Desktop,\n * binding a transient Node server on `0.0.0.0` does not always reject\n * ports already held on `127.0.0.1`, even though Docker's publish step\n * will reject them. Wildcard probes therefore check loopback as well.\n *\n * Race window: between the probe-close and the caller's real `listen`,\n * a non-devstack process could still grab the port. Devstack peers are\n * blocked by the reservation file until the allocation scope releases.\n */\nconst probePort = (port: number, host: PortProbeHost): Effect.Effect<ProbeResult> =>\n\thost === '0.0.0.0'\n\t\t? Effect.gen(function* () {\n\t\t\t\tconst loopback = yield* probeSinglePort(port, '127.0.0.1');\n\t\t\t\tif (loopback._tag !== 'ok') return loopback;\n\t\t\t\treturn yield* probeSinglePort(port, host);\n\t\t\t}).pipe(Effect.withSpan('substrate.portBroker.probe', { attributes: { host, port } }))\n\t\t: probeSinglePort(port, host).pipe(\n\t\t\t\tEffect.withSpan('substrate.portBroker.probe', { attributes: { host, port } }),\n\t\t\t);\n\nconst probeSinglePort = (port: number, host: PortProbeHost): Effect.Effect<ProbeResult> =>\n\tEffect.callback<ProbeResult>((resume) => {\n\t\tlet settled = false;\n\t\tlet server: NetServer | null = null;\n\t\tconst settle = (r: ProbeResult): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tresume(Effect.succeed(r));\n\t\t};\n\t\ttry {\n\t\t\tserver = createNetServer();\n\t\t\tserver.unref();\n\t\t\tconst onError = (err: NodeJS.ErrnoException): void => {\n\t\t\t\tconst code = err.code ?? '';\n\t\t\t\tif (code === 'EADDRINUSE' || code === 'EACCES') {\n\t\t\t\t\t// EACCES on these hosts is exotic but treat as in-use\n\t\t\t\t\t// for non-privileged ports (our default window starts\n\t\t\t\t\t// at 39200, well above 1024). Surfaces as probe-failed\n\t\t\t\t\t// only if it persists for every port in the window.\n\t\t\t\t\tsettle({ _tag: 'in-use' });\n\t\t\t\t} else {\n\t\t\t\t\tsettle({ _tag: 'probe-failed', cause: err });\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tserver?.close();\n\t\t\t\t} catch {\n\t\t\t\t\t/* defensive */\n\t\t\t\t}\n\t\t\t};\n\t\t\tserver.once('error', onError);\n\t\t\tserver.listen(port, host, () => {\n\t\t\t\t// Verify the OS actually gave us THIS port (defensive —\n\t\t\t\t// asking for a specific port shouldn't ever surprise\n\t\t\t\t// us, but `address()` is the source of truth).\n\t\t\t\tconst addr = server?.address() as AddressInfo | null;\n\t\t\t\tconst ok = addr !== null && addr.port === port;\n\t\t\t\tserver?.close(() => {\n\t\t\t\t\tsettle(ok ? { _tag: 'ok' } : { _tag: 'in-use' });\n\t\t\t\t});\n\t\t\t});\n\t\t} catch (cause) {\n\t\t\tsettle({ _tag: 'probe-failed', cause });\n\t\t\ttry {\n\t\t\t\tserver?.close();\n\t\t\t} catch {\n\t\t\t\t/* defensive */\n\t\t\t}\n\t\t}\n\t\t// Interrupt mid-`listen()` must close the transient server so the\n\t\t// probed port isn't held open by an orphaned handle.\n\t\treturn Effect.sync(() => {\n\t\t\ttry {\n\t\t\t\tserver?.close();\n\t\t\t} catch {\n\t\t\t\t/* defensive */\n\t\t\t}\n\t\t});\n\t});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuFA,MAAa,sBAAsD;CAClE,OAAO;CACP,MAAM;CACN;AAiED,MAAM,gBAAgB;AAmBtB,MAAM,2BAA2B;AAEjC,MAAM,2BAA2B,mBAAmB,0BAA0B;CAC7E,MAAM,OAAO;CACb,OAAO,OAAO;CACd,SAAS,OAAO;CAChB,QAAQ;CACR,CAAC;AAqBF,MAAM,uBAAuB,MAAc,SAC1C,KAAK,MAAM,cAAc,GAAG,KAAK,OAAO;AAEzC,MAAM,uBAAuB,QAA2C;AACvE,KAAI;AACH,SAAO,mBAAmB,0BAA0B,KAAK;GACxD,QAAQ;GACR,UAAU,UAAU;GACpB,CAAC;SACK;AACP,SAAO;;;AAIT,MAAM,2BACL,MACA,QAC6B;AAS7B,KAAI;AACH,+BAA6B,MAAM,GAAG,KAAK,UAAU,IAAI,CAAC,KAAK,EAAE,MAAM,KAAO,CAAC;AAC/E,SAAO,EAAE,MAAM,WAAW;UAClB,OAAO;AAEf,MADc,MAAgC,SACjC,SACZ,KAAI;AACH,UAAO;IAAE,MAAM;IAAU,KAAK,oBAAoB,aAAa,MAAM,OAAO,CAAC;IAAE;UACxE;AAGP,UAAO,EAAE,MAAM,QAAQ;;AAGzB,SAAO;GAAE,MAAM;GAAU;GAAO;;;AAIlC,MAAM,gCAAgC,MAAc,YAA0B;AAC7E,KAAI;AAEH,MADY,oBAAoB,aAAa,MAAM,OAAO,CACnD,EAAE,YAAY,QACpB,YAAW,KAAK;SAEV;;AAMT,MAAM,iCAAiC,MAAc,YAA6B;AACjF,KAAI;AAEH,MADY,oBAAoB,aAAa,MAAM,OAAO,CACnD,EAAE,YAAY,QAAS,QAAO;AACrC,aAAW,KAAK;AAChB,SAAO;SACA;AACP,SAAO;;;;;;;;;;;;;AAcT,MAAM,qCAAqC,SAA0B;AACpE,KAAI;AACH,aAAW,KAAK;AAChB,SAAO;SACA;AAGP,SAAO;;;AAIT,MAAM,0BACL,MACA,MACA,UAEA,OAAO,IAAI,aAAa;CACvB,MAAM,OAAO,oBAAoB,MAAM,KAAK;CAC5C,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,YAAY,CAAC,MAAM,GAAG,EAAE;CAExD,MAAM,MAA0B;EAC/B,SAAS;EACT;EACA;EACA;EACA,QAN4B,WAMtB;EACN;CAKD,IAAI,uBAAuB;AAC3B,QAAO,MAAM;EACZ,MAAM,UAAU,wBAAwB,MAAM,IAAI;AAClD,MAAI,QAAQ,SAAS,UACpB,QAAO;GACN,MAAM;GACN,aAAa;IACZ;IACA;IACA,SAAS,OAAO,WAAW,6BAA6B,MAAM,QAAQ,CAAC;IACvE;GACD;AAEF,MAAI,QAAQ,SAAS,OAAQ;AAC7B,MAAI,QAAQ,SAAS,SACpB,QAAO;GACN,MAAM;GACN,QAAQ,oBAAoB,KAAK;GACjC,OAAO,QAAQ;GACf;AAEF,MAAI,QAAQ,QAAQ,MAAM;AAIzB,OAAI,CAAC,wBAAwB,kCAAkC,KAAK,EAAE;AACrE,2BAAuB;AACvB;;AAED,UAAO,EAAE,MAAM,QAAQ;;AAMxB,OAAI,OAHoB,oBAAoB,QAAQ,IAAI,OAAO,CAAC,KAC/D,OAAO,YAAY,OAAO,QAAQ,QAAiB,CAAC,CACpD,MACgB,QAAS,QAAO,EAAE,MAAM,QAAQ;AACjD,MAAI,CAAC,8BAA8B,MAAM,QAAQ,IAAI,QAAQ,CAC5D,QAAO,EAAE,MAAM,QAAQ;;EAGxB;AAMH,IAAa,oBAAb,cAAuC,QAAQ,SAAwC,CACtF,iCACA,CAAC;;;;;;;;;;AAWF,MAAa,kBAAsE,MAAM,OACxF,mBACA,OAAO,IAAI,aAAa;CAEvB,MAAM,mBAAkB,OADG,aACS;CACpC,MAAM,QAAQ,OAAO,IAAI,qBAAY,IAAI,KAAK,CAAC;CAE/C,MAAM,cAAc,MAAc,UACjC,IAAI,OAAuB,QAAQ,YAAY;AAC9C,MAAI,QAAQ,IAAI,KAAK,CAAE,QAAO,CAAC,OAAO,QAAQ;EAC9C,MAAM,OAAO,IAAI,IAAI,QAAQ;AAC7B,OAAK,IAAI,MAAM,EAAE,OAAO,CAAC;AACzB,SAAO,CAAC,MAAM,KAAK;GAClB;CAEH,MAAM,QAAQ,SACb,IAAI,OAAO,QAAQ,YAAY;AAC9B,MAAI,CAAC,QAAQ,IAAI,KAAK,CAAE,QAAO;EAC/B,MAAM,OAAO,IAAI,IAAI,QAAQ;AAC7B,OAAK,OAAO,KAAK;AACjB,SAAO;GACN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDH,MAAM,eACL,MACA,OACA,cAC2D;EAK3D,IAAI,kBAA0C;AAC9C,SAAO,OAAO,kBACb,WAAW,MAAM,MAAM,GACtB,aAAoE;AACpE,OAAI,CAAC,SAAU,QAAO,OAAO,QAAQ,KAAK;AAC1C,UAAO,OAAO,IAAI,aAAa;IAC9B,MAAM,cAAc,OAAO,uBAAuB,iBAAiB,MAAM,MAAM;AAC/E,QAAI,YAAY,SAAS,OAAQ,QAAO,EAAE,MAAM,QAAiB;AACjE,QAAI,YAAY,SAAS,SACxB,QAAO;KACN,MAAM;KACN,QAAQ,YAAY;KACpB,OAAO,YAAY;KACnB;AAKF,sBAAkB,YAAY;IAC9B,MAAM,KAAK,OAAO,UAAU,MAAM,UAAU;AAC5C,QAAI,GAAG,SAAS,MAAM;KACrB,MAAM,YAAY,OAAO,iBAAiB,MAAM,OAAO,MAAM,YAAY,YAAY;AAGrF,uBAAkB;AAClB,YAAO;MAAE,MAAM;MAAiB;MAAW;;AAM5C,QAAI,GAAG,SAAS,eACf,QAAO;KAAE,MAAM;KAAyB,OAAO,GAAG;KAAO;AAE1D,WAAO,EAAE,MAAM,QAAiB;KAC/B;MAEF,UAAU,SAAS;AASnB,OAAI,CAAC,SAAU,QAAO,OAAO;AAE7B,OADa,KAAK,SAAS,aAAa,KAAK,OAAO,SAAS,OACnD,QAAO,OAAO;GACxB,MAAM,qBACL,oBAAoB,OAAO,gBAAgB,UAAU,OAAO;AAC7D,UAAO,OAAO,IAAI,CAAC,KAAK,KAAK,EAAE,mBAAmB,EAAE,EAAE,SAAS,MAAM,CAAC;IAEvE;;CAGF,MAAM,YAAoC,OAAO,EAAE,KAClD,OAAO,IAAI,aAAa;EACvB,MAAM,QAAQ,KAAK,SAAS;EAC5B,MAAM,YAAY,KAAK,aAAa;EACpC,MAAM,cAAc,KAAK,YAAY,SAAS,oBAAoB;EAClE,MAAM,aAAa,KAAK,YAAY,QAAQ,oBAAoB;AAKhE,MAAI,KAAK,kBAAkB,KAAA,GAAW;GACrC,MAAM,UAAU,OAAO,YAAY,KAAK,eAAe,OAAO,UAAU;AACxE,OAAI,YAAY,KACf,QAAO,OAAO,OAAO,KACpB,IAAI,gBAAgB;IACnB,QAAQ;IACR,QACC,kBAAkB,KAAK,cAAc,UAAU,MAAM;IAEtD,CAAC,CACF;AAEF,OAAI,QAAQ,SAAS,OAAQ,QAAO,QAAQ;AAC5C,OAAI,QAAQ,SAAS,qBACpB,QAAO,OAAO,OAAO,KACpB,IAAI,gBAAgB;IACnB,QAAQ;IACR,QAAQ,QAAQ;IAChB,OAAO,QAAQ;IACf,CAAC,CACF;AAEF,OAAI,QAAQ,SAAS,eACpB,QAAO,OAAO,OAAO,KACpB,IAAI,gBAAgB;IACnB,QAAQ;IACR,QACC,gCAAgC,KAAK,cAAc,UACzC,MAAM,SAAS,UAAU;IACpC,OAAO,QAAQ;IACf,CAAC,CACF;;AAQH,OAAK,IAAI,IAAI,aAAa,IAAI,cAAc,YAAY,KAAK;GAC5D,MAAM,UAAU,OAAO,YAAY,GAAG,OAAO,UAAU;AACvD,OAAI,YAAY,KAAM;AACtB,OAAI,QAAQ,SAAS,OAAQ,QAAO,QAAQ;AAC5C,OAAI,QAAQ,SAAS,qBACpB,QAAO,OAAO,OAAO,KACpB,IAAI,gBAAgB;IACnB,QAAQ;IACR,QAAQ,QAAQ;IAChB,OAAO,QAAQ;IACf,CAAC,CACF;AAEF,OAAI,QAAQ,SAAS,eAIpB,QAAO,OAAO,OAAO,KACpB,IAAI,gBAAgB;IACnB,QAAQ;IACR,QACC,sBAAsB,EAAE,UAAU,MAAM,SAAS,UAAU;IAE5D,OAAO,QAAQ;IACf,CAAC,CACF;;AAKH,SAAO,OAAO,OAAO,KACpB,IAAI,gBAAgB;GACnB,QAAQ;GACR,QACC,0BAA0B,YAAY,IACnC,cAAc,WAAW,eAAe,MAAM;GAClD,CAAC,CACF;GACA,CAAC,KACF,OAAO,SAAS,iCAAiC,EAChD,YAAY;EACX,OAAO,KAAK,SAAS;EACrB,eAAe,KAAK,iBAAiB;EACrC,WAAW,KAAK,aAAa;EAC7B,aAAa,KAAK,YAAY,SAAS,oBAAoB;EAC3D,YAAY,KAAK,YAAY,QAAQ,oBAAoB;EACzD,EACD,CAAC,CACF;CAEF,MAAM,oBACL,MACA,OACA,QACA,gBAEA,OAAO,IAAI,aAAa;EAIvB,MAAM,UAAU,OAAO,IAAI,CAAC,OAAO,KAAK,EAAE,YAAY,QAAQ,EAAE,EAC/D,SAAS,MACT,CAAC;AACF,SAAO,OAAO,mBAAmB,QAAQ,KAAK,OAAO,gBAAgB,CAAC;AACtE,SAAO,OAAO,oBAAoB;GACjC,mBAAmB;GACnB,oBAAoB;GACpB,CAAC;AACF,SAAO,OAAO,SAAS,yBAAyB,KAAK,UAAU,MAAM,GAAG;AACxE,SAAO;GACN;GACA;GACA;GACA;AAEH,QAAO,kBAAkB,GAAG,EAAE,UAAU,CAAC;EACxC,CACF;;;;;;;;;;;;;;;;AA6BD,MAAM,aAAa,MAAc,SAChC,SAAS,YACN,OAAO,IAAI,aAAa;CACxB,MAAM,WAAW,OAAO,gBAAgB,MAAM,YAAY;AAC1D,KAAI,SAAS,SAAS,KAAM,QAAO;AACnC,QAAO,OAAO,gBAAgB,MAAM,KAAK;EACxC,CAAC,KAAK,OAAO,SAAS,8BAA8B,EAAE,YAAY;CAAE;CAAM;CAAM,EAAE,CAAC,CAAC,GACrF,gBAAgB,MAAM,KAAK,CAAC,KAC5B,OAAO,SAAS,8BAA8B,EAAE,YAAY;CAAE;CAAM;CAAM,EAAE,CAAC,CAC7E;AAEJ,MAAM,mBAAmB,MAAc,SACtC,OAAO,UAAuB,WAAW;CACxC,IAAI,UAAU;CACd,IAAI,SAA2B;CAC/B,MAAM,UAAU,MAAyB;AACxC,MAAI,QAAS;AACb,YAAU;AACV,SAAO,OAAO,QAAQ,EAAE,CAAC;;AAE1B,KAAI;AACH,WAASA,cAAiB;AAC1B,SAAO,OAAO;EACd,MAAM,WAAW,QAAqC;GACrD,MAAM,OAAO,IAAI,QAAQ;AACzB,OAAI,SAAS,gBAAgB,SAAS,SAKrC,QAAO,EAAE,MAAM,UAAU,CAAC;OAE1B,QAAO;IAAE,MAAM;IAAgB,OAAO;IAAK,CAAC;AAE7C,OAAI;AACH,YAAQ,OAAO;WACR;;AAIT,SAAO,KAAK,SAAS,QAAQ;AAC7B,SAAO,OAAO,MAAM,YAAY;GAI/B,MAAM,OAAO,QAAQ,SAAS;GAC9B,MAAM,KAAK,SAAS,QAAQ,KAAK,SAAS;AAC1C,WAAQ,YAAY;AACnB,WAAO,KAAK,EAAE,MAAM,MAAM,GAAG,EAAE,MAAM,UAAU,CAAC;KAC/C;IACD;UACM,OAAO;AACf,SAAO;GAAE,MAAM;GAAgB;GAAO,CAAC;AACvC,MAAI;AACH,WAAQ,OAAO;UACR;;AAMT,QAAO,OAAO,WAAW;AACxB,MAAI;AACH,WAAQ,OAAO;UACR;GAGP;EACD"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["createNetServer"],"sources":["../../../../src/substrate/runtime/port-broker/service.ts"],"sourcesContent":["// Port broker — substrate-level service.\n//\n// Resource broker for host ports. Plugins that need to bind a host HTTP server\n// or publish a container port on the host yield this service and call\n// `allocate({...})`. The broker:\n//\n// 1. Tracks in-process allocations in a `Ref<Map<port, Holder>>` so\n// two sibling plugins in the same stack don't both pick the same\n// port between bind-probe and `server.listen`.\n// 2. Claims a runtime-root scoped port reservation file before the\n// bind probe. This closes the cross-process probe→listen race:\n// two independent `devstack apply` processes sharing a state dir\n// cannot both choose the same candidate while neither real server\n// is listening yet.\n// 3. Verifies kernel-level availability by binding a transient\n// `net.Server` on the candidate port and closing it.\n// The OS's `EADDRINUSE` remains the final collision check for\n// non-devstack processes and stale reservation gaps.\n// 4. Releases on scope close. The broker hands back a `release`\n// Effect AND installs a scope finalizer; the architecture's\n// \"scope-local, never module-level\" invariant is honoured by\n// construction (the Layer is scope-local, the entries die with\n// the scope).\n//\n// Sequential search: callers may pass `preferredPort` (tried first) and\n// an optional `windowHint: { start, size }` (the scan window if\n// preferred is busy or absent). The default window —\n// `DEFAULT_PORT_WINDOW` below — is intentionally wide enough for any\n// realistic in-process devstack; plugins do NOT freelance per-kind\n// window starts. The substrate stays name-blind: there is no `PortKind`\n// literal; plugins are opaque port-holders from the broker's view.\n//\n// Cross-process safety: the reservation file is scoped to the runtime\n// root, not the stack root, because host ports are machine-global. The\n// stack-scoped supervisor lock serializes acquires of the SAME stack;\n// these reservation files serialize host-port choices across DIFFERENT\n// stacks and apps that share a state dir.\n\nimport { randomUUID } from 'node:crypto';\nimport { readFileSync, unlinkSync } from 'node:fs';\nimport {\n\tcreateServer as createNetServer,\n\ttype AddressInfo,\n\ttype Server as NetServer,\n} from 'node:net';\nimport { join } from 'node:path';\n\nimport { Context, Effect, Layer, Ref, Schema, Scope } from 'effect';\n\nimport { RosterHolderSchema, type RosterHolder } from '../../cross-process.ts';\nimport { atomicWriteFileExclusiveSync } from '../atomic-write.ts';\nimport { checkHolderLiveness, ownHolder } from '../cross-process/liveness.ts';\nimport { selfPid } from '../cross-process/self-pid.ts';\nimport { PortBrokerError } from '../errors.ts';\nimport { RuntimeRoot } from '../paths.ts';\nimport { decodeJsonTextSync } from '../runtime-decode.ts';\nimport { versionedDocSchema } from '../../versioned-doc-schema.ts';\n\n// ----------------------------------------------------------------------\n// Public shape\n// ----------------------------------------------------------------------\n\n/** Host interface used by the transient bind probe. Callers binding a\n * real server on loopback should use the default. Callers that hand\n * the port to Docker's all-interface publish path should pass\n * `'0.0.0.0'` so the probe asks the same kernel question. */\nexport type PortProbeHost = '127.0.0.1' | '0.0.0.0';\n\n/** Optional per-call window override. The broker scans\n * `[start, start + size)` in order when `preferredPort` is busy or\n * absent. Plugins with UX-pinned ports (e.g. a dev wallet whose\n * legacy adapters auto-connect to a specific range) pass this so the\n * fall-back range stays predictable; everyone else gets the default. */\nexport interface PortAllocationWindow {\n\treadonly start: number;\n\treadonly size?: number;\n}\n\n/** Default scan window. Sized for \"tens of plugins per stack\" with\n * parallel-stack overhead headroom. 1000 ports is more than enough for\n * any realistic in-process devstack; the forward-scan only ever\n * reaches the end if EVERY port in the window is taken (effectively\n * never on dev machines). Start chosen to land between common dev\n * defaults (3000-5173, 8000-8080) and the ephemeral range so a\n * sibling Vite/Next process doesn't accidentally collide on the\n * first probe. */\nexport const DEFAULT_PORT_WINDOW: Required<PortAllocationWindow> = {\n\tstart: 39200,\n\tsize: 1000,\n};\n\n/** Input to `allocate`. */\nexport interface AllocateOptions {\n\t/** Optional caller hint. The broker tries this FIRST; if the port is\n\t * already held in-process the call FAILS with\n\t * `reason: 'preferred-busy'` (the caller asked specifically for\n\t * this port and got an obvious collision). If the bind-probe fails\n\t * with EADDRINUSE the broker falls through to the window scan —\n\t * preferred is a HINT for the kernel-collision case, but a HARD\n\t * CHOICE against in-process collision. */\n\treadonly preferredPort?: number;\n\t/** Interface to bind during the kernel probe. Defaults to\n\t * `127.0.0.1`, matching host-loopback servers. Docker\n\t * `-p host:container` publishes on all interfaces, so Docker-backed\n\t * callers pass `0.0.0.0`. */\n\treadonly probeHost?: PortProbeHost;\n\t/** Optional per-call window override; defaults to\n\t * `DEFAULT_PORT_WINDOW`. */\n\treadonly windowHint?: PortAllocationWindow;\n\t/** Optional free-form owner label for diagnostics (`no-free-port`\n\t * error detail). The broker treats this as opaque; pass something\n\t * short and human-readable like `'wallet'` or `'sui:rpc'`. */\n\treadonly owner?: string;\n}\n\n/** Successful allocation. `release` is provided in addition to the\n * scope finalizer so tests / orchestrators can drive release\n * explicitly without unwinding the whole scope. Calling `release`\n * twice is a no-op. */\nexport interface AllocatedPort {\n\treadonly port: number;\n\treadonly release: Effect.Effect<void>;\n}\n\n/** Service shape — what plugins yield from Context. */\nexport interface PortBroker {\n\t/**\n\t * Allocate a port. The returned port is exclusive within THIS\n\t * process (the broker's `Ref` map) and best-effort-exclusive across\n\t * devstack processes that share a runtime root (port reservation\n\t * file), then verified against the kernel bind table on `probeHost`.\n\t *\n\t * Scope-bound release: the broker installs a finalizer on the\n\t * surrounding scope; callers can additionally invoke\n\t * `result.release` explicitly.\n\t *\n\t * Failures (`PortBrokerError`):\n\t * - `preferred-busy` — caller's `preferredPort` is held by another\n\t * in-process allocation on the same stack.\n\t * - `no-free-port` — the scan window was exhausted without a\n\t * probe-pass candidate.\n\t * - `bind-probe-failed` — non-EADDRINUSE error from the OS bind\n\t * probe (EACCES on a privileged port etc.).\n\t * - `reservation-failed` — port reservation file IO failed.\n\t */\n\treadonly allocate: (\n\t\topts?: AllocateOptions,\n\t) => Effect.Effect<AllocatedPort, PortBrokerError, Scope.Scope>;\n}\n\n// ----------------------------------------------------------------------\n// Internal state\n// ----------------------------------------------------------------------\n\nconst DEFAULT_OWNER = 'unknown';\n\n/** In-process holder for a port. Carries the owner string for\n * diagnostics (e.g. surfaced in `no-free-port` errors). */\ninterface Holder {\n\treadonly owner: string;\n}\n\ntype State = ReadonlyMap<number, Holder>;\n\n// Reservation-doc shape. Decode failures are treated as stale artifacts:\n// `acquirePortReservation` self-heals them with a best-effort unlink +\n// retry so one corrupt reservation file cannot block a port forever.\nconst PORT_RESERVATION_VERSION = 1 as const;\n\nconst PortReservationDocSchema = versionedDocSchema(PORT_RESERVATION_VERSION, {\n\tport: Schema.Number,\n\towner: Schema.String,\n\townerId: Schema.String,\n\tholder: RosterHolderSchema,\n});\n\ntype PortReservationDoc = Schema.Schema.Type<typeof PortReservationDocSchema>;\n\ninterface PortReservation {\n\treadonly path: string;\n\treadonly ownerId: string;\n\treadonly release: Effect.Effect<void>;\n}\n\ntype ReservationAttempt =\n\t| { readonly _tag: 'acquired'; readonly reservation: PortReservation }\n\t| { readonly _tag: 'busy' }\n\t| { readonly _tag: 'failed'; readonly detail: string; readonly cause: unknown };\n\ntype ReservationWriteAttempt =\n\t| { readonly _tag: 'written' }\n\t| { readonly _tag: 'race' }\n\t| { readonly _tag: 'exists'; readonly doc: PortReservationDoc | null }\n\t| { readonly _tag: 'failed'; readonly cause: unknown };\n\nconst portReservationPath = (root: string, port: number): string =>\n\tjoin(root, 'port-locks', `${port}.json`);\n\nconst parseReservationDoc = (raw: string): PortReservationDoc | null => {\n\ttry {\n\t\treturn decodeJsonTextSync(PortReservationDocSchema, raw, {\n\t\t\tsource: 'port reservation',\n\t\t\tmkError: (issue) => issue,\n\t\t});\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst tryWriteReservationSync = (\n\tpath: string,\n\tdoc: PortReservationDoc,\n): ReservationWriteAttempt => {\n\t// `atomicWriteFileExclusiveSync` writes a tempfile under O_EXCL +\n\t// fsync, then `linkSync`s it onto the final path. POSIX `link(2)`\n\t// is atomic AND fails with `EEXIST` if the target exists — that is\n\t// the exclusive-create primitive `rename` lacks (rename clobbers).\n\t// A sibling pid that wins the race becomes the canonical reservation\n\t// at `path`; we observe it via the EEXIST error and surface the\n\t// winner's doc to the caller (`acquirePortReservation`), which then\n\t// decides busy-vs-stale based on the holder's liveness.\n\ttry {\n\t\tatomicWriteFileExclusiveSync(path, `${JSON.stringify(doc)}\\n`, { mode: 0o600 });\n\t\treturn { _tag: 'written' };\n\t} catch (cause) {\n\t\tconst code = (cause as NodeJS.ErrnoException).code;\n\t\tif (code === 'EEXIST') {\n\t\t\ttry {\n\t\t\t\treturn { _tag: 'exists', doc: parseReservationDoc(readFileSync(path, 'utf8')) };\n\t\t\t} catch {\n\t\t\t\t// The winning reservation file was removed between our\n\t\t\t\t// link-EEXIST and our read (a peer raced release). Retry.\n\t\t\t\treturn { _tag: 'race' };\n\t\t\t}\n\t\t}\n\t\treturn { _tag: 'failed', cause };\n\t}\n};\n\nconst unlinkReservationIfOwnerSync = (path: string, ownerId: string): void => {\n\ttry {\n\t\tconst doc = parseReservationDoc(readFileSync(path, 'utf8'));\n\t\tif (doc?.ownerId === ownerId) {\n\t\t\tunlinkSync(path);\n\t\t}\n\t} catch {\n\t\t// Missing or unreadable: release is best-effort. A peer never\n\t\t// reclaims unreadable reservations; it simply skips that port.\n\t}\n};\n\nconst reclaimReservationIfOwnerSync = (path: string, ownerId: string): boolean => {\n\ttry {\n\t\tconst doc = parseReservationDoc(readFileSync(path, 'utf8'));\n\t\tif (doc?.ownerId !== ownerId) return false;\n\t\tunlinkSync(path);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\n/** Best-effort removal of an EXISTING reservation file whose body\n * cannot be decoded (a leftover from an incompatible-version\n * supervisor, or a torn write). Such a file carries no readable\n * `ownerId`/`holder`, so neither `reclaimReservationIfOwnerSync` nor\n * liveness can ever clear it; without this it would skip that one\n * port for every future process for the supervisor's lifetime. We\n * treat an undecodable body as a stale artifact and unlink it so the\n * next exclusive-create can win. Returns whether the unlink ran\n * (false if the file vanished first, i.e. a peer already cleared it)\n * so the caller can bound its retry. */\nconst reclaimUndecodableReservationSync = (path: string): boolean => {\n\ttry {\n\t\tunlinkSync(path);\n\t\treturn true;\n\t} catch {\n\t\t// Already gone (peer reclaimed it) or unremovable; either way\n\t\t// the next loop turn re-evaluates via exclusive-create.\n\t\treturn false;\n\t}\n};\n\nconst acquirePortReservation = (\n\troot: string,\n\tport: number,\n\towner: string,\n): Effect.Effect<ReservationAttempt> =>\n\tEffect.gen(function* () {\n\t\tconst path = portReservationPath(root, port);\n\t\tconst ownerId = `${selfPid()}-${randomUUID().slice(0, 8)}`;\n\t\tconst holder: RosterHolder = ownHolder();\n\t\tconst doc: PortReservationDoc = {\n\t\t\tversion: PORT_RESERVATION_VERSION,\n\t\t\tport,\n\t\t\towner,\n\t\t\townerId,\n\t\t\tholder,\n\t\t};\n\n\t\t// Bounds the self-heal: an undecodable EEXIST body is reclaimed at\n\t\t// most once. If we see another undecodable body after that (a peer\n\t\t// keeps rewriting garbage), give up with `busy` rather than spin.\n\t\tlet reclaimedUndecodable = false;\n\t\twhile (true) {\n\t\t\tconst attempt = tryWriteReservationSync(path, doc);\n\t\t\tif (attempt._tag === 'written') {\n\t\t\t\treturn {\n\t\t\t\t\t_tag: 'acquired',\n\t\t\t\t\treservation: {\n\t\t\t\t\t\tpath,\n\t\t\t\t\t\townerId,\n\t\t\t\t\t\trelease: Effect.sync(() => unlinkReservationIfOwnerSync(path, ownerId)),\n\t\t\t\t\t},\n\t\t\t\t} as const;\n\t\t\t}\n\t\t\tif (attempt._tag === 'race') continue;\n\t\t\tif (attempt._tag === 'failed') {\n\t\t\t\treturn {\n\t\t\t\t\t_tag: 'failed',\n\t\t\t\t\tdetail: `port reservation ${path} could not be written`,\n\t\t\t\t\tcause: attempt.cause,\n\t\t\t\t} as const;\n\t\t\t}\n\t\t\tif (attempt.doc === null) {\n\t\t\t\t// EEXIST file we cannot decode (stale incompatible-version\n\t\t\t\t// leftover or torn write). Self-heal once: unlink it and retry\n\t\t\t\t// the exclusive-create so a live port is not lost permanently.\n\t\t\t\tif (!reclaimedUndecodable && reclaimUndecodableReservationSync(path)) {\n\t\t\t\t\treclaimedUndecodable = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn { _tag: 'busy' } as const;\n\t\t\t}\n\n\t\t\tconst liveness = yield* checkHolderLiveness(attempt.doc.holder).pipe(\n\t\t\t\tEffect.catch(() => Effect.succeed('alive' as const)),\n\t\t\t);\n\t\t\tif (liveness === 'alive') return { _tag: 'busy' } as const;\n\t\t\tif (!reclaimReservationIfOwnerSync(path, attempt.doc.ownerId)) {\n\t\t\t\treturn { _tag: 'busy' } as const;\n\t\t\t}\n\t\t}\n\t});\n\n// ----------------------------------------------------------------------\n// Service tag + Layer\n// ----------------------------------------------------------------------\n\nexport class PortBrokerService extends Context.Service<PortBrokerService, PortBroker>()(\n\t'@devstack/substrate/PortBroker',\n) {}\n\n/**\n * Layer that materializes one `PortBroker` per stack scope.\n *\n * The broker's state lives in a `Ref` captured by the Layer's\n * `Effect.gen`; closing the Layer's scope drops every allocation.\n * Parallel stacks each get their own broker (Layer-driven), and they\n * coordinate cross-process via runtime-root reservation files plus the\n * kernel's bind table — never via shared in-process state.\n */\nexport const layerPortBroker: Layer.Layer<PortBrokerService, never, RuntimeRoot> = Layer.effect(\n\tPortBrokerService,\n\tEffect.gen(function* () {\n\t\tconst runtimeRoot = yield* RuntimeRoot;\n\t\tconst reservationRoot = runtimeRoot.root;\n\t\tconst state = yield* Ref.make<State>(new Map());\n\n\t\tconst tryReserve = (port: number, owner: string): Effect.Effect<boolean> =>\n\t\t\tRef.modify<State, boolean>(state, (current) => {\n\t\t\t\tif (current.has(port)) return [false, current];\n\t\t\t\tconst next = new Map(current);\n\t\t\t\tnext.set(port, { owner });\n\t\t\t\treturn [true, next];\n\t\t\t});\n\n\t\tconst drop = (port: number): Effect.Effect<void> =>\n\t\t\tRef.update(state, (current) => {\n\t\t\t\tif (!current.has(port)) return current;\n\t\t\t\tconst next = new Map(current);\n\t\t\t\tnext.delete(port);\n\t\t\t\treturn next;\n\t\t\t});\n\n\t\t/** Outcome of one candidate-port attempt.\n\t\t *\n\t\t * `kept` means `finishAllocation` armed its scope finalizer; the\n\t\t * in-process slot is now owned by that finalizer and we MUST\n\t\t * NOT drop it on this critical region's exit. Any other outcome\n\t\t * (busy / probe-in-use / probe-failed / reservation-failed)\n\t\t * must release the slot before we hand control back to the\n\t\t * caller. */\n\t\ttype SlotOutcome =\n\t\t\t| { readonly _tag: 'kept'; readonly allocated: AllocatedPort }\n\t\t\t| { readonly _tag: 'busy' }\n\t\t\t| {\n\t\t\t\t\treadonly _tag: 'reservation-failed';\n\t\t\t\t\treadonly detail: string;\n\t\t\t\t\treadonly cause: unknown;\n\t\t\t }\n\t\t\t| { readonly _tag: 'probe-failed'; readonly cause: unknown };\n\n\t\t/** Reserve `port` in the in-process Map, run the probe+reservation\n\t\t * chain through to either (a) `finishAllocation` arming its scope\n\t\t * finalizer or (b) a clean release of BOTH the in-process slot\n\t\t * AND the on-disk reservation file.\n\t\t *\n\t\t * Interrupt safety: the critical region is wrapped in\n\t\t * `Effect.acquireUseRelease`, whose `release` step is\n\t\t * uninterruptible while its `use` step is NOT (Effect v4). The\n\t\t * acquired `PortReservation` is created INSIDE `use` (after the\n\t\t * on-disk file is written), so the `use` body stashes it in\n\t\t * `reservationCell`; the uninterruptible `release` reads that cell\n\t\t * and unlinks the reservation file on ANY non-`kept` exit —\n\t\t * interrupt, typed failure, or a non-`kept` success outcome.\n\t\t * Without this, an interrupt landing between the reservation write\n\t\t * (in `acquirePortReservation`) and the finalizer arm (in\n\t\t * `finishAllocation`) would leave `port-locks/<port>.json` on disk\n\t\t * while only dropping the in-process slot. Its `holder` is this\n\t\t * still-alive process, so every future `allocate` of that port then\n\t\t * sees `checkHolderLiveness` report `alive`, marking it busy for the\n\t\t * whole supervisor lifetime.\n\t\t *\n\t\t * The only path that intentionally KEEPS both the slot and the\n\t\t * file is the `kept` outcome, where ownership transfers to the\n\t\t * scope finalizer (see `finishAllocation`); `reservationCell` is\n\t\t * cleared there so `release` is a no-op for it.\n\t\t * (`unlinkReservationIfOwnerSync` is owner-id-guarded and\n\t\t * idempotent, so a stray double-release is harmless — clearing\n\t\t * the cell just avoids the redundant stat.)\n\t\t */\n\t\tconst attemptSlot = (\n\t\t\tport: number,\n\t\t\towner: string,\n\t\t\tprobeHost: PortProbeHost,\n\t\t): Effect.Effect<SlotOutcome | null, never, Scope.Scope> => {\n\t\t\t// Visible to BOTH the `use` body (fills it once the reservation\n\t\t\t// file exists) and the uninterruptible `release` callback\n\t\t\t// (unlinks it on any non-`kept` exit). On-disk analogue of the\n\t\t\t// in-process slot.\n\t\t\tlet reservationCell: PortReservation | null = null;\n\t\t\treturn Effect.acquireUseRelease(\n\t\t\t\ttryReserve(port, owner),\n\t\t\t\t(reserved): Effect.Effect<SlotOutcome | null, never, Scope.Scope> => {\n\t\t\t\t\tif (!reserved) return Effect.succeed(null);\n\t\t\t\t\treturn Effect.gen(function* () {\n\t\t\t\t\t\tconst reservation = yield* acquirePortReservation(reservationRoot, port, owner);\n\t\t\t\t\t\tif (reservation._tag === 'busy') return { _tag: 'busy' as const };\n\t\t\t\t\t\tif (reservation._tag === 'failed') {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t_tag: 'reservation-failed' as const,\n\t\t\t\t\t\t\t\tdetail: reservation.detail,\n\t\t\t\t\t\t\t\tcause: reservation.cause,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// File is now on disk. Hand it to the uninterruptible\n\t\t\t\t\t\t// `release` step so an interrupt at the `probePort` /\n\t\t\t\t\t\t// `finishAllocation` boundary cannot leak it.\n\t\t\t\t\t\treservationCell = reservation.reservation;\n\t\t\t\t\t\tconst ok = yield* probePort(port, probeHost);\n\t\t\t\t\t\tif (ok._tag === 'ok') {\n\t\t\t\t\t\t\tconst allocated = yield* finishAllocation(port, owner, drop, reservation.reservation);\n\t\t\t\t\t\t\t// Ownership transferred to the scope finalizer; the\n\t\t\t\t\t\t\t// `release` step must NOT also unlink the file.\n\t\t\t\t\t\t\treservationCell = null;\n\t\t\t\t\t\t\treturn { _tag: 'kept' as const, allocated };\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Probe missed. The uninterruptible `release` step below\n\t\t\t\t\t\t// unlinks the reservation file (via `reservationCell`) and\n\t\t\t\t\t\t// drops the in-process slot, so a single owner cleans up\n\t\t\t\t\t\t// both even if an interrupt preempts us right here.\n\t\t\t\t\t\tif (ok._tag === 'probe-failed') {\n\t\t\t\t\t\t\treturn { _tag: 'probe-failed' as const, cause: ok.cause };\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { _tag: 'busy' as const };\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t(reserved, exit) => {\n\t\t\t\t\t// Slot + reservation release (uninterruptible). Drop the\n\t\t\t\t\t// in-process entry AND unlink the on-disk reservation\n\t\t\t\t\t// UNLESS we transferred ownership to a scope finalizer\n\t\t\t\t\t// (the `kept` outcome, which cleared `reservationCell`).\n\t\t\t\t\t// Interrupts, typed failures, and non-`kept` success\n\t\t\t\t\t// outcomes all flow through here, closing both the Map\n\t\t\t\t\t// leak and the port-locks/*.json leak (the on-disk\n\t\t\t\t\t// reservation is unlinked here rather than persisting\n\t\t\t\t\t// until the supervisor exits).\n\t\t\t\t\tif (!reserved) return Effect.void;\n\t\t\t\t\tconst kept = exit._tag === 'Success' && exit.value?._tag === 'kept';\n\t\t\t\t\tif (kept) return Effect.void;\n\t\t\t\t\tconst releaseReservation =\n\t\t\t\t\t\treservationCell !== null ? reservationCell.release : Effect.void;\n\t\t\t\t\treturn Effect.all([drop(port), releaseReservation], { discard: true });\n\t\t\t\t},\n\t\t\t);\n\t\t};\n\n\t\tconst allocate: PortBroker['allocate'] = (opts = {}) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst owner = opts.owner ?? DEFAULT_OWNER;\n\t\t\t\tconst probeHost = opts.probeHost ?? '127.0.0.1';\n\t\t\t\tconst windowStart = opts.windowHint?.start ?? DEFAULT_PORT_WINDOW.start;\n\t\t\t\tconst windowSize = opts.windowHint?.size ?? DEFAULT_PORT_WINDOW.size;\n\n\t\t\t\t// 1. Preferred-port path. The broker honours `preferredPort`\n\t\t\t\t// as a strong hint, but refuses in-process collisions\n\t\t\t\t// (architecture §6: same-stack siblings MUST NOT trample).\n\t\t\t\tif (opts.preferredPort !== undefined) {\n\t\t\t\t\tconst outcome = yield* attemptSlot(opts.preferredPort, owner, probeHost);\n\t\t\t\t\tif (outcome === null) {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'preferred-busy',\n\t\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\t\t`preferred port ${opts.preferredPort} (owner=${owner}) is ` +\n\t\t\t\t\t\t\t\t\t`already held by another allocation in this stack`,\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\t\tif (outcome._tag === 'kept') return outcome.allocated;\n\t\t\t\t\tif (outcome._tag === 'reservation-failed') {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'reservation-failed',\n\t\t\t\t\t\t\t\tdetail: outcome.detail,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\tif (outcome._tag === 'probe-failed') {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'bind-probe-failed',\n\t\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\t\t`bind probe on preferred port ${opts.preferredPort} ` +\n\t\t\t\t\t\t\t\t\t`(owner=${owner}, host=${probeHost}) failed with non-EADDRINUSE error`,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\t// `busy` (either cross-process reservation held by a\n\t\t\t\t\t// live peer OR kernel-level in-use) — fall through to\n\t\t\t\t\t// the window scan; the caller's preferred was a HINT.\n\t\t\t\t}\n\n\t\t\t\t// 2. Forward-scan the window.\n\t\t\t\tfor (let p = windowStart; p < windowStart + windowSize; p++) {\n\t\t\t\t\tconst outcome = yield* attemptSlot(p, owner, probeHost);\n\t\t\t\t\tif (outcome === null) continue;\n\t\t\t\t\tif (outcome._tag === 'kept') return outcome.allocated;\n\t\t\t\t\tif (outcome._tag === 'reservation-failed') {\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'reservation-failed',\n\t\t\t\t\t\t\t\tdetail: outcome.detail,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\tif (outcome._tag === 'probe-failed') {\n\t\t\t\t\t\t// A privileged port (EACCES) in the middle of the\n\t\t\t\t\t\t// window is unrecoverable for this scan. Surface\n\t\t\t\t\t\t// immediately rather than thrash the whole window.\n\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\t\t\treason: 'bind-probe-failed',\n\t\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\t\t`bind probe on port ${p} (owner=${owner}, host=${probeHost}) failed ` +\n\t\t\t\t\t\t\t\t\t`with non-EADDRINUSE error`,\n\t\t\t\t\t\t\t\tcause: outcome.cause,\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\t\t// `busy` — next candidate.\n\t\t\t\t}\n\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew PortBrokerError({\n\t\t\t\t\t\treason: 'no-free-port',\n\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t`no free port found in [${windowStart}, ` +\n\t\t\t\t\t\t\t`${windowStart + windowSize}) for owner '${owner}'`,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t});\n\n\t\tconst finishAllocation = (\n\t\t\tport: number,\n\t\t\towner: string,\n\t\t\tdropFn: (p: number) => Effect.Effect<void>,\n\t\t\treservation: PortReservation,\n\t\t): Effect.Effect<AllocatedPort, never, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\t// Scope finalizer — drops the entry when the surrounding\n\t\t\t\t// scope closes. Uninterruptible so a Ctrl-C double-tap\n\t\t\t\t// doesn't leave the entry dangling in the broker's Ref.\n\t\t\t\tconst release = Effect.all([dropFn(port), reservation.release], {\n\t\t\t\t\tdiscard: true,\n\t\t\t\t});\n\t\t\t\tyield* Effect.addFinalizer(() => release.pipe(Effect.uninterruptible));\n\t\t\t\tyield* Effect.logDebug(`port-broker allocated ${port} (owner=${owner})`);\n\t\t\t\treturn {\n\t\t\t\t\tport,\n\t\t\t\t\trelease,\n\t\t\t\t} satisfies AllocatedPort;\n\t\t\t});\n\n\t\treturn PortBrokerService.of({ allocate });\n\t}),\n);\n\n// ----------------------------------------------------------------------\n// Kernel-level bind probe\n// ----------------------------------------------------------------------\n\n/** Probe result. `'in-use'` is the recoverable case (try the next\n * port); `'probe-failed'` is the unrecoverable case (caller's\n * problem — surface). */\ntype ProbeResult =\n\t| { readonly _tag: 'ok' }\n\t| { readonly _tag: 'in-use' }\n\t| { readonly _tag: 'probe-failed'; readonly cause: unknown };\n\n/**\n * Bind a transient `net.Server` on `<host>:<port>` and close it.\n * The OS atomically rejects with `EADDRINUSE` if another process\n * already holds the port — that's our cross-process collision check.\n *\n * The default probe is loopback-only because most servers bind\n * loopback. Docker-backed callers pass `0.0.0.0`; on Docker Desktop,\n * binding a transient Node server on `0.0.0.0` does not always reject\n * ports already held on `127.0.0.1`, even though Docker's publish step\n * will reject them. Wildcard probes therefore check loopback as well.\n *\n * Race window: between the probe-close and the caller's real `listen`,\n * a non-devstack process could still grab the port. Devstack peers are\n * blocked by the reservation file until the allocation scope releases.\n */\nconst probePort = (port: number, host: PortProbeHost): Effect.Effect<ProbeResult> =>\n\thost === '0.0.0.0'\n\t\t? Effect.gen(function* () {\n\t\t\t\tconst loopback = yield* probeSinglePort(port, '127.0.0.1');\n\t\t\t\tif (loopback._tag !== 'ok') return loopback;\n\t\t\t\treturn yield* probeSinglePort(port, host);\n\t\t\t})\n\t\t: probeSinglePort(port, host);\n\nconst probeSinglePort = (port: number, host: PortProbeHost): Effect.Effect<ProbeResult> =>\n\tEffect.callback<ProbeResult>((resume) => {\n\t\tlet settled = false;\n\t\tlet server: NetServer | null = null;\n\t\tconst settle = (r: ProbeResult): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tresume(Effect.succeed(r));\n\t\t};\n\t\ttry {\n\t\t\tserver = createNetServer();\n\t\t\tserver.unref();\n\t\t\tconst onError = (err: NodeJS.ErrnoException): void => {\n\t\t\t\tconst code = err.code ?? '';\n\t\t\t\tif (code === 'EADDRINUSE' || code === 'EACCES') {\n\t\t\t\t\t// EACCES on these hosts is exotic but treat as in-use\n\t\t\t\t\t// for non-privileged ports (our default window starts\n\t\t\t\t\t// at 39200, well above 1024). Surfaces as probe-failed\n\t\t\t\t\t// only if it persists for every port in the window.\n\t\t\t\t\tsettle({ _tag: 'in-use' });\n\t\t\t\t} else {\n\t\t\t\t\tsettle({ _tag: 'probe-failed', cause: err });\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tserver?.close();\n\t\t\t\t} catch {\n\t\t\t\t\t/* defensive */\n\t\t\t\t}\n\t\t\t};\n\t\t\tserver.once('error', onError);\n\t\t\tserver.listen(port, host, () => {\n\t\t\t\t// Verify the OS actually gave us THIS port (defensive —\n\t\t\t\t// asking for a specific port shouldn't ever surprise\n\t\t\t\t// us, but `address()` is the source of truth).\n\t\t\t\tconst addr = server?.address() as AddressInfo | null;\n\t\t\t\tconst ok = addr !== null && addr.port === port;\n\t\t\t\tserver?.close(() => {\n\t\t\t\t\tsettle(ok ? { _tag: 'ok' } : { _tag: 'in-use' });\n\t\t\t\t});\n\t\t\t});\n\t\t} catch (cause) {\n\t\t\tsettle({ _tag: 'probe-failed', cause });\n\t\t\ttry {\n\t\t\t\tserver?.close();\n\t\t\t} catch {\n\t\t\t\t/* defensive */\n\t\t\t}\n\t\t}\n\t\t// Interrupt mid-`listen()` must close the transient server so the\n\t\t// probed port isn't held open by an orphaned handle.\n\t\treturn Effect.sync(() => {\n\t\t\ttry {\n\t\t\t\tserver?.close();\n\t\t\t} catch {\n\t\t\t\t/* defensive */\n\t\t\t}\n\t\t});\n\t});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-acquire-tasks.mjs","names":[],"sources":["../../../src/substrate/runtime/post-acquire-tasks.ts"],"sourcesContent":["import { Context, Data, Effect, Layer, Ref, Scope } from 'effect';\n\nimport type { PluginKey } from '../brand.ts';\n\nexport interface PostAcquireTask {\n\treadonly pluginKey: PluginKey;\n\treadonly label: string;\n\treadonly run: Effect.Effect<void, unknown, Scope.Scope>;\n}\n\nexport class PostAcquireTaskFailed extends Data.TaggedError('PostAcquireTaskFailed')<{\n\treadonly pluginKey: PluginKey;\n\treadonly label: string;\n\treadonly cause: unknown;\n}> {}\n\nexport interface PostAcquireTasksShape {\n\treadonly register: (task: PostAcquireTask) => Effect.Effect<void, never, Scope.Scope>;\n\treadonly runAll: Effect.Effect<void, PostAcquireTaskFailed, never>;\n}\n\nexport class PostAcquireTasksService extends Context.Service<\n\tPostAcquireTasksService,\n\tPostAcquireTasksShape\n>()('@devstack/substrate/PostAcquireTasks') {}\n\ninterface RegisteredPostAcquireTask extends PostAcquireTask {\n\treadonly id: symbol;\n\treadonly scope: Scope.Scope;\n}\n\nexport const layerPostAcquireTasks: Layer.Layer<PostAcquireTasksService> = Layer.effect(\n\tPostAcquireTasksService,\n\tEffect.gen(function* () {\n\t\tconst tasksRef = yield* Ref.make<ReadonlyArray<RegisteredPostAcquireTask>>([]);\n\n\t\tconst register = (task: PostAcquireTask): Effect.Effect<void, never, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst scope = yield* Effect.scope;\n\t\t\t\tconst id = Symbol(task.label);\n\t\t\t\tconst registered: RegisteredPostAcquireTask = { ...task, id, scope };\n\t\t\t\tyield* Ref.update(tasksRef, (tasks) => [...tasks, registered]);\n\t\t\t\tyield* Effect.addFinalizer(() =>\n\t\t\t\t\tRef.update(tasksRef, (tasks) => tasks.filter((candidate) => candidate.id !== id)),\n\t\t\t\t);\n\t\t\t});\n\n\t\tconst runAll: Effect.Effect<void, PostAcquireTaskFailed, never> = Effect.gen(function* () {\n\t\t\tconst tasks = yield* Ref.get(tasksRef);\n\t\t\tfor (const task of tasks) {\n\t\t\t\tyield* Scope.provide(task.run, task.scope).pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\tnew PostAcquireTaskFailed({\n\t\t\t\t\t\t\t\tpluginKey: task.pluginKey,\n\t\t\t\t\t\t\t\tlabel: task.label,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\n\t\treturn PostAcquireTasksService.of({ register, runAll });\n\t}),\n);\n"],"mappings":";;AAUA,IAAa,wBAAb,cAA2C,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"post-acquire-tasks.mjs","names":[],"sources":["../../../src/substrate/runtime/post-acquire-tasks.ts"],"sourcesContent":["import { Context, Data, Effect, Layer, Ref, Scope } from 'effect';\n\nimport type { PluginKey } from '../brand.ts';\n\nexport interface PostAcquireTask {\n\treadonly pluginKey: PluginKey;\n\treadonly label: string;\n\treadonly run: Effect.Effect<void, unknown, Scope.Scope>;\n}\n\nexport class PostAcquireTaskFailed extends Data.TaggedError('PostAcquireTaskFailed')<{\n\treadonly pluginKey: PluginKey;\n\treadonly label: string;\n\treadonly cause: unknown;\n}> {}\n\nexport interface PostAcquireTasksShape {\n\treadonly register: (task: PostAcquireTask) => Effect.Effect<void, never, Scope.Scope>;\n\treadonly runAll: Effect.Effect<void, PostAcquireTaskFailed, never>;\n}\n\nexport class PostAcquireTasksService extends Context.Service<\n\tPostAcquireTasksService,\n\tPostAcquireTasksShape\n>()('@devstack/substrate/PostAcquireTasks') {}\n\ninterface RegisteredPostAcquireTask extends PostAcquireTask {\n\treadonly id: symbol;\n\treadonly scope: Scope.Scope;\n}\n\nexport const layerPostAcquireTasks: Layer.Layer<PostAcquireTasksService> = Layer.effect(\n\tPostAcquireTasksService,\n\tEffect.gen(function* () {\n\t\tconst tasksRef = yield* Ref.make<ReadonlyArray<RegisteredPostAcquireTask>>([]);\n\n\t\tconst register = (task: PostAcquireTask): Effect.Effect<void, never, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst scope = yield* Effect.scope;\n\t\t\t\tconst id = Symbol(task.label);\n\t\t\t\tconst registered: RegisteredPostAcquireTask = { ...task, id, scope };\n\t\t\t\tyield* Ref.update(tasksRef, (tasks) => [...tasks, registered]);\n\t\t\t\tyield* Effect.addFinalizer(() =>\n\t\t\t\t\tRef.update(tasksRef, (tasks) => tasks.filter((candidate) => candidate.id !== id)),\n\t\t\t\t);\n\t\t\t});\n\n\t\tconst runAll: Effect.Effect<void, PostAcquireTaskFailed, never> = Effect.gen(function* () {\n\t\t\tconst tasks = yield* Ref.get(tasksRef);\n\t\t\tfor (const task of tasks) {\n\t\t\t\tyield* Scope.provide(task.run, task.scope).pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\tnew PostAcquireTaskFailed({\n\t\t\t\t\t\t\t\tpluginKey: task.pluginKey,\n\t\t\t\t\t\t\t\tlabel: task.label,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\n\t\treturn PostAcquireTasksService.of({ register, runAll });\n\t}),\n);\n"],"mappings":";;AAUA,IAAa,wBAAb,cAA2C,KAAK,YAAY,uBAAuB,CAAC,CAIjF,CAAC;AAOJ,IAAa,0BAAb,cAA6C,QAAQ,QAGnD,CAAC,CAAC,sCAAsC,CAAC,CAAC,CAAC;AAO7C,MAAa,wBAA8D,MAAM,OAChF,yBACA,OAAO,IAAI,aAAa;CACvB,MAAM,WAAW,OAAO,IAAI,KAA+C,CAAC,CAAC;CAE7E,MAAM,YAAY,SACjB,OAAO,IAAI,aAAa;EACvB,MAAM,QAAQ,OAAO,OAAO;EAC5B,MAAM,KAAK,OAAO,KAAK,KAAK;EAC5B,MAAM,aAAwC;GAAE,GAAG;GAAM;GAAI;EAAM;EACnE,OAAO,IAAI,OAAO,WAAW,UAAU,CAAC,GAAG,OAAO,UAAU,CAAC;EAC7D,OAAO,OAAO,mBACb,IAAI,OAAO,WAAW,UAAU,MAAM,QAAQ,cAAc,UAAU,OAAO,EAAE,CAAC,CACjF;CACD,CAAC;CAEF,MAAM,SAA4D,OAAO,IAAI,aAAa;EACzF,MAAM,QAAQ,OAAO,IAAI,IAAI,QAAQ;EACrC,KAAK,MAAM,QAAQ,OAClB,OAAO,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,KAC1C,OAAO,UACL,UACA,IAAI,sBAAsB;GACzB,WAAW,KAAK;GAChB,OAAO,KAAK;GACZ;EACD,CAAC,CACH,CACD;CAEF,CAAC;CAED,OAAO,wBAAwB,GAAG;EAAE;EAAU;CAAO,CAAC;AACvD,CAAC,CACF"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect, Types } from "effect";
|
|
2
|
-
import * as _$effect_Cause0 from "effect/Cause";
|
|
3
2
|
|
|
4
3
|
//#region src/substrate/runtime/probes.d.ts
|
|
5
4
|
declare namespace probes_d_exports {
|
|
@@ -17,7 +16,7 @@ interface ProbeOptions<E = unknown, R = never> {
|
|
|
17
16
|
readonly probe: () => Effect.Effect<ProbeAttemptResult, E, R>;
|
|
18
17
|
readonly isRetryableError?: (error: E) => boolean;
|
|
19
18
|
}
|
|
20
|
-
declare const ProbeAttemptTimeoutError_base: new <A extends Record<string, any> = {}>(args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) =>
|
|
19
|
+
declare const ProbeAttemptTimeoutError_base: new <A extends Record<string, any> = {}>(args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
21
20
|
readonly _tag: "ProbeAttemptTimeoutError";
|
|
22
21
|
} & Readonly<A>;
|
|
23
22
|
declare class ProbeAttemptTimeoutError extends ProbeAttemptTimeoutError_base<{
|
|
@@ -25,7 +24,7 @@ declare class ProbeAttemptTimeoutError extends ProbeAttemptTimeoutError_base<{
|
|
|
25
24
|
readonly attemptTimeoutMs: number;
|
|
26
25
|
readonly message: string;
|
|
27
26
|
}> {}
|
|
28
|
-
declare const ProbeTimeoutError_base: new <A extends Record<string, any> = {}>(args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) =>
|
|
27
|
+
declare const ProbeTimeoutError_base: new <A extends Record<string, any> = {}>(args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
29
28
|
readonly _tag: "ProbeTimeoutError";
|
|
30
29
|
} & Readonly<A>;
|
|
31
30
|
declare class ProbeTimeoutError extends ProbeTimeoutError_base<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probes.mjs","names":[],"sources":["../../../src/substrate/runtime/probes.ts"],"sourcesContent":["import { Data, Duration, Effect, Types } from 'effect';\n\nexport type ProbeAttemptResult =\n\t| void\n\t| boolean\n\t| {\n\t\t\treadonly ready: boolean;\n\t\t\treadonly detail?: unknown;\n\t };\n\nexport interface ProbeOptions<E = unknown, R = never> {\n\treadonly label: string;\n\treadonly timeoutMs: number;\n\treadonly intervalMs?: number;\n\treadonly attemptTimeoutMs?: number;\n\treadonly probe: () => Effect.Effect<ProbeAttemptResult, E, R>;\n\treadonly isRetryableError?: (error: E) => boolean;\n}\n\nexport class ProbeAttemptTimeoutError extends Data.TaggedError('ProbeAttemptTimeoutError')<{\n\treadonly label: string;\n\treadonly attemptTimeoutMs: number;\n\treadonly message: string;\n}> {}\n\nexport class ProbeTimeoutError extends Data.TaggedError('ProbeTimeoutError')<{\n\treadonly label: string;\n\treadonly timeoutMs: number;\n\treadonly intervalMs: number;\n\treadonly attemptTimeoutMs?: number;\n\treadonly attempts: number;\n\treadonly message: string;\n\treadonly lastError?: unknown;\n\treadonly lastNotReady?: unknown;\n}> {}\n\nconst DEFAULT_INTERVAL_MS = 250;\n\nconst normalizeAttempt = (\n\tresult: ProbeAttemptResult,\n): { readonly ready: boolean; readonly detail?: unknown } => {\n\tif (result === undefined || result === true) return { ready: true };\n\tif (result === false) return { ready: false };\n\treturn result;\n};\n\nconst withAttemptTimeout = <A, E, R>(\n\teffect: Effect.Effect<A, E, R>,\n\toptions: {\n\t\treadonly label: string;\n\t\treadonly attemptTimeoutMs?: number;\n\t\treadonly remainingMs: number;\n\t},\n): Effect.Effect<A, E | ProbeAttemptTimeoutError, R> => {\n\tif (options.attemptTimeoutMs === undefined) return effect;\n\tconst timeoutMs = Math.max(1, Math.min(options.attemptTimeoutMs, options.remainingMs));\n\treturn effect.pipe(\n\t\tEffect.timeoutOrElse({\n\t\t\tduration: Duration.millis(timeoutMs),\n\t\t\torElse: () =>\n\t\t\t\tEffect.fail(\n\t\t\t\t\tnew ProbeAttemptTimeoutError({\n\t\t\t\t\t\tlabel: options.label,\n\t\t\t\t\t\tattemptTimeoutMs: timeoutMs,\n\t\t\t\t\t\tmessage: `Probe ${options.label} attempt did not complete within ${timeoutMs}ms`,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t}),\n\t);\n};\n\nexport const waitForProbe = <E, R = never>(\n\toptions: ProbeOptions<E, R>,\n): Effect.Effect<void, E | ProbeTimeoutError, R> =>\n\tEffect.gen(function* () {\n\t\tconst timeoutMs = options.timeoutMs;\n\t\tconst intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n\t\tconst started = Date.now();\n\t\tconst deadline = started + timeoutMs;\n\t\tlet attempts = 0;\n\t\tlet lastError: unknown;\n\t\tlet lastNotReady: unknown;\n\n\t\tfor (;;) {\n\t\t\tconst remainingMs = deadline - Date.now();\n\t\t\tif (remainingMs <= 0) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew ProbeTimeoutError({\n\t\t\t\t\t\tlabel: options.label,\n\t\t\t\t\t\ttimeoutMs,\n\t\t\t\t\t\tintervalMs,\n\t\t\t\t\t\t...(options.attemptTimeoutMs === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { attemptTimeoutMs: options.attemptTimeoutMs }),\n\t\t\t\t\t\tattempts,\n\t\t\t\t\t\tmessage: `Probe ${options.label} did not become ready within ${timeoutMs}ms`,\n\t\t\t\t\t\t...(lastError === undefined ? {} : { lastError }),\n\t\t\t\t\t\t...(lastNotReady === undefined ? {} : { lastNotReady }),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tattempts += 1;\n\t\t\tconst ready = yield* withAttemptTimeout(options.probe(), {\n\t\t\t\tlabel: options.label,\n\t\t\t\tattemptTimeoutMs: options.attemptTimeoutMs,\n\t\t\t\tremainingMs,\n\t\t\t}).pipe(\n\t\t\t\tEffect.matchEffect({\n\t\t\t\t\tonFailure: (error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t!(error instanceof ProbeAttemptTimeoutError) &&\n\t\t\t\t\t\t\toptions.isRetryableError?.(error as E) === false\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treturn Effect.fail(error as E);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastError = error;\n\t\t\t\t\t\treturn Effect.succeed(false);\n\t\t\t\t\t},\n\t\t\t\t\tonSuccess: (result) => {\n\t\t\t\t\t\tconst normalized = normalizeAttempt(result);\n\t\t\t\t\t\tif (!normalized.ready) lastNotReady = normalized.detail ?? false;\n\t\t\t\t\t\treturn Effect.succeed(normalized.ready);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (ready) return;\n\n\t\t\tconst sleepMs = Math.min(intervalMs, Math.max(1, deadline - Date.now()));\n\t\t\tyield* Effect.sleep(Duration.millis(sleepMs));\n\t\t}\n\t});\n\nexport interface ExitCodeProbeResult {\n\treadonly exitCode: number;\n\treadonly stdout?: string;\n\treadonly stderr?: string;\n}\n\nexport const exitCodeProbeResult = (result: ExitCodeProbeResult): ProbeAttemptResult => {\n\tif (result.exitCode === 0) return true;\n\treturn {\n\t\tready: false,\n\t\tdetail: {\n\t\t\texitCode: result.exitCode,\n\t\t\t...(result.stdout === undefined ? {} : { stdout: result.stdout }),\n\t\t\t...(result.stderr === undefined ? {} : { stderr: result.stderr }),\n\t\t},\n\t};\n};\n\n// ---------------------------------------------------------------------------\n// probeManyLenient — run a set of lenient `T | null` probes and surface\n// each probe's verdict so the caller can fold the array however it likes.\n// ---------------------------------------------------------------------------\n//\n// Lenient probes (per `ChainProbe`'s `'lenient'` contract) return `T | null`\n// where `null` is a non-authoritative \"not yet\" signal. Several plugins\n// (walrus deploy, deepbook deploy, pyth init) hand-roll a sequential loop\n// that early-returns `null` on the first non-ready probe. This helper\n// centralizes the iteration so the call sites collapse to a `.every()`\n// or `.find()` against the returned array.\n//\n// Concurrency defaults to `'unbounded'`. Callers that need sequential\n// short-circuit semantics (e.g. RPC backoff politeness) can pass\n// `concurrency: 1`. The return shape is the full array even when concurrency\n// is 1 — callers that want \"any null aborts and returns null\" should fold\n// with `.every((x) => x !== null) ? array : null` themselves.\n\nexport const probeManyLenient = <T, E, R>(\n\tprobes: ReadonlyArray<Effect.Effect<T | null, E, R>>,\n\toptions?: { readonly concurrency?: Types.Concurrency },\n): Effect.Effect<ReadonlyArray<T | null>, E, R> =>\n\tEffect.forEach(probes, (probe) => probe, {\n\t\tconcurrency: options?.concurrency ?? 'unbounded',\n\t});\n"],"mappings":";;;;;;;;;;AAmBA,IAAa,2BAAb,cAA8C,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"probes.mjs","names":[],"sources":["../../../src/substrate/runtime/probes.ts"],"sourcesContent":["import { Data, Duration, Effect, Types } from 'effect';\n\nexport type ProbeAttemptResult =\n\t| void\n\t| boolean\n\t| {\n\t\t\treadonly ready: boolean;\n\t\t\treadonly detail?: unknown;\n\t };\n\nexport interface ProbeOptions<E = unknown, R = never> {\n\treadonly label: string;\n\treadonly timeoutMs: number;\n\treadonly intervalMs?: number;\n\treadonly attemptTimeoutMs?: number;\n\treadonly probe: () => Effect.Effect<ProbeAttemptResult, E, R>;\n\treadonly isRetryableError?: (error: E) => boolean;\n}\n\nexport class ProbeAttemptTimeoutError extends Data.TaggedError('ProbeAttemptTimeoutError')<{\n\treadonly label: string;\n\treadonly attemptTimeoutMs: number;\n\treadonly message: string;\n}> {}\n\nexport class ProbeTimeoutError extends Data.TaggedError('ProbeTimeoutError')<{\n\treadonly label: string;\n\treadonly timeoutMs: number;\n\treadonly intervalMs: number;\n\treadonly attemptTimeoutMs?: number;\n\treadonly attempts: number;\n\treadonly message: string;\n\treadonly lastError?: unknown;\n\treadonly lastNotReady?: unknown;\n}> {}\n\nconst DEFAULT_INTERVAL_MS = 250;\n\nconst normalizeAttempt = (\n\tresult: ProbeAttemptResult,\n): { readonly ready: boolean; readonly detail?: unknown } => {\n\tif (result === undefined || result === true) return { ready: true };\n\tif (result === false) return { ready: false };\n\treturn result;\n};\n\nconst withAttemptTimeout = <A, E, R>(\n\teffect: Effect.Effect<A, E, R>,\n\toptions: {\n\t\treadonly label: string;\n\t\treadonly attemptTimeoutMs?: number;\n\t\treadonly remainingMs: number;\n\t},\n): Effect.Effect<A, E | ProbeAttemptTimeoutError, R> => {\n\tif (options.attemptTimeoutMs === undefined) return effect;\n\tconst timeoutMs = Math.max(1, Math.min(options.attemptTimeoutMs, options.remainingMs));\n\treturn effect.pipe(\n\t\tEffect.timeoutOrElse({\n\t\t\tduration: Duration.millis(timeoutMs),\n\t\t\torElse: () =>\n\t\t\t\tEffect.fail(\n\t\t\t\t\tnew ProbeAttemptTimeoutError({\n\t\t\t\t\t\tlabel: options.label,\n\t\t\t\t\t\tattemptTimeoutMs: timeoutMs,\n\t\t\t\t\t\tmessage: `Probe ${options.label} attempt did not complete within ${timeoutMs}ms`,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t}),\n\t);\n};\n\nexport const waitForProbe = <E, R = never>(\n\toptions: ProbeOptions<E, R>,\n): Effect.Effect<void, E | ProbeTimeoutError, R> =>\n\tEffect.gen(function* () {\n\t\tconst timeoutMs = options.timeoutMs;\n\t\tconst intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n\t\tconst started = Date.now();\n\t\tconst deadline = started + timeoutMs;\n\t\tlet attempts = 0;\n\t\tlet lastError: unknown;\n\t\tlet lastNotReady: unknown;\n\n\t\tfor (;;) {\n\t\t\tconst remainingMs = deadline - Date.now();\n\t\t\tif (remainingMs <= 0) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew ProbeTimeoutError({\n\t\t\t\t\t\tlabel: options.label,\n\t\t\t\t\t\ttimeoutMs,\n\t\t\t\t\t\tintervalMs,\n\t\t\t\t\t\t...(options.attemptTimeoutMs === undefined\n\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t: { attemptTimeoutMs: options.attemptTimeoutMs }),\n\t\t\t\t\t\tattempts,\n\t\t\t\t\t\tmessage: `Probe ${options.label} did not become ready within ${timeoutMs}ms`,\n\t\t\t\t\t\t...(lastError === undefined ? {} : { lastError }),\n\t\t\t\t\t\t...(lastNotReady === undefined ? {} : { lastNotReady }),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tattempts += 1;\n\t\t\tconst ready = yield* withAttemptTimeout(options.probe(), {\n\t\t\t\tlabel: options.label,\n\t\t\t\tattemptTimeoutMs: options.attemptTimeoutMs,\n\t\t\t\tremainingMs,\n\t\t\t}).pipe(\n\t\t\t\tEffect.matchEffect({\n\t\t\t\t\tonFailure: (error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t!(error instanceof ProbeAttemptTimeoutError) &&\n\t\t\t\t\t\t\toptions.isRetryableError?.(error as E) === false\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treturn Effect.fail(error as E);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastError = error;\n\t\t\t\t\t\treturn Effect.succeed(false);\n\t\t\t\t\t},\n\t\t\t\t\tonSuccess: (result) => {\n\t\t\t\t\t\tconst normalized = normalizeAttempt(result);\n\t\t\t\t\t\tif (!normalized.ready) lastNotReady = normalized.detail ?? false;\n\t\t\t\t\t\treturn Effect.succeed(normalized.ready);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (ready) return;\n\n\t\t\tconst sleepMs = Math.min(intervalMs, Math.max(1, deadline - Date.now()));\n\t\t\tyield* Effect.sleep(Duration.millis(sleepMs));\n\t\t}\n\t});\n\nexport interface ExitCodeProbeResult {\n\treadonly exitCode: number;\n\treadonly stdout?: string;\n\treadonly stderr?: string;\n}\n\nexport const exitCodeProbeResult = (result: ExitCodeProbeResult): ProbeAttemptResult => {\n\tif (result.exitCode === 0) return true;\n\treturn {\n\t\tready: false,\n\t\tdetail: {\n\t\t\texitCode: result.exitCode,\n\t\t\t...(result.stdout === undefined ? {} : { stdout: result.stdout }),\n\t\t\t...(result.stderr === undefined ? {} : { stderr: result.stderr }),\n\t\t},\n\t};\n};\n\n// ---------------------------------------------------------------------------\n// probeManyLenient — run a set of lenient `T | null` probes and surface\n// each probe's verdict so the caller can fold the array however it likes.\n// ---------------------------------------------------------------------------\n//\n// Lenient probes (per `ChainProbe`'s `'lenient'` contract) return `T | null`\n// where `null` is a non-authoritative \"not yet\" signal. Several plugins\n// (walrus deploy, deepbook deploy, pyth init) hand-roll a sequential loop\n// that early-returns `null` on the first non-ready probe. This helper\n// centralizes the iteration so the call sites collapse to a `.every()`\n// or `.find()` against the returned array.\n//\n// Concurrency defaults to `'unbounded'`. Callers that need sequential\n// short-circuit semantics (e.g. RPC backoff politeness) can pass\n// `concurrency: 1`. The return shape is the full array even when concurrency\n// is 1 — callers that want \"any null aborts and returns null\" should fold\n// with `.every((x) => x !== null) ? array : null` themselves.\n\nexport const probeManyLenient = <T, E, R>(\n\tprobes: ReadonlyArray<Effect.Effect<T | null, E, R>>,\n\toptions?: { readonly concurrency?: Types.Concurrency },\n): Effect.Effect<ReadonlyArray<T | null>, E, R> =>\n\tEffect.forEach(probes, (probe) => probe, {\n\t\tconcurrency: options?.concurrency ?? 'unbounded',\n\t});\n"],"mappings":";;;;;;;;;;AAmBA,IAAa,2BAAb,cAA8C,KAAK,YAAY,0BAA0B,CAAC,CAIvF,CAAC;AAEJ,IAAa,oBAAb,cAAuC,KAAK,YAAY,mBAAmB,CAAC,CASzE,CAAC;AAEJ,MAAM,sBAAsB;AAE5B,MAAM,oBACL,WAC4D;CAC5D,IAAI,WAAW,KAAA,KAAa,WAAW,MAAM,OAAO,EAAE,OAAO,KAAK;CAClE,IAAI,WAAW,OAAO,OAAO,EAAE,OAAO,MAAM;CAC5C,OAAO;AACR;AAEA,MAAM,sBACL,QACA,YAKuD;CACvD,IAAI,QAAQ,qBAAqB,KAAA,GAAW,OAAO;CACnD,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,kBAAkB,QAAQ,WAAW,CAAC;CACrF,OAAO,OAAO,KACb,OAAO,cAAc;EACpB,UAAU,SAAS,OAAO,SAAS;EACnC,cACC,OAAO,KACN,IAAI,yBAAyB;GAC5B,OAAO,QAAQ;GACf,kBAAkB;GAClB,SAAS,SAAS,QAAQ,MAAM,mCAAmC,UAAU;EAC9E,CAAC,CACF;CACF,CAAC,CACF;AACD;AAEA,MAAa,gBACZ,YAEA,OAAO,IAAI,aAAa;CACvB,MAAM,YAAY,QAAQ;CAC1B,MAAM,aAAa,QAAQ,cAAc;CAEzC,MAAM,WADU,KAAK,IACE,IAAI;CAC3B,IAAI,WAAW;CACf,IAAI;CACJ,IAAI;CAEJ,SAAS;EACR,MAAM,cAAc,WAAW,KAAK,IAAI;EACxC,IAAI,eAAe,GAClB,OAAO,OAAO,OAAO,KACpB,IAAI,kBAAkB;GACrB,OAAO,QAAQ;GACf;GACA;GACA,GAAI,QAAQ,qBAAqB,KAAA,IAC9B,CAAC,IACD,EAAE,kBAAkB,QAAQ,iBAAiB;GAChD;GACA,SAAS,SAAS,QAAQ,MAAM,+BAA+B,UAAU;GACzE,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;GAC/C,GAAI,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,aAAa;EACtD,CAAC,CACF;EAGD,YAAY;EAwBZ,IAAI,OAvBiB,mBAAmB,QAAQ,MAAM,GAAG;GACxD,OAAO,QAAQ;GACf,kBAAkB,QAAQ;GAC1B;EACD,CAAC,CAAC,CAAC,KACF,OAAO,YAAY;GAClB,YAAY,UAAU;IACrB,IACC,EAAE,iBAAiB,6BACnB,QAAQ,mBAAmB,KAAU,MAAM,OAE3C,OAAO,OAAO,KAAK,KAAU;IAE9B,YAAY;IACZ,OAAO,OAAO,QAAQ,KAAK;GAC5B;GACA,YAAY,WAAW;IACtB,MAAM,aAAa,iBAAiB,MAAM;IAC1C,IAAI,CAAC,WAAW,OAAO,eAAe,WAAW,UAAU;IAC3D,OAAO,OAAO,QAAQ,WAAW,KAAK;GACvC;EACD,CAAC,CACF,GACW;EAEX,MAAM,UAAU,KAAK,IAAI,YAAY,KAAK,IAAI,GAAG,WAAW,KAAK,IAAI,CAAC,CAAC;EACvE,OAAO,OAAO,MAAM,SAAS,OAAO,OAAO,CAAC;CAC7C;AACD,CAAC;AAQF,MAAa,uBAAuB,WAAoD;CACvF,IAAI,OAAO,aAAa,GAAG,OAAO;CAClC,OAAO;EACN,OAAO;EACP,QAAQ;GACP,UAAU,OAAO;GACjB,GAAI,OAAO,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,OAAO,OAAO;GAC/D,GAAI,OAAO,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,OAAO,OAAO;EAChE;CACD;AACD;AAoBA,MAAa,oBACZ,QACA,YAEA,OAAO,QAAQ,SAAS,UAAU,OAAO,EACxC,aAAa,SAAS,eAAe,YACtC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-supervisor.mjs","names":[],"sources":["../../../src/substrate/runtime/process-supervisor.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport type { Readable } from 'node:stream';\n\nimport { Effect } from 'effect';\n\nexport interface ManagedProcessExitStatus {\n\treadonly code: number | null;\n\treadonly signal: NodeJS.Signals | null;\n}\n\nexport interface ManagedProcessChild {\n\treadonly pid?: number;\n\treadonly stdout?: Readable | null;\n\treadonly stderr?: Readable | null;\n\treadonly exitCode?: number | null;\n\treadonly signalCode?: NodeJS.Signals | null;\n\tkill(signal?: NodeJS.Signals): boolean;\n\ton(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\ton(event: 'error', listener: (cause: Error) => void): this;\n\tonce(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\tonce(event: 'error', listener: (cause: Error) => void): this;\n\toff(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\toff(event: 'error', listener: (cause: Error) => void): this;\n}\n\nexport interface ManagedProcessSpawnOptions {\n\treadonly cwd: string;\n\treadonly env: NodeJS.ProcessEnv;\n\treadonly stdio: 'pipe';\n\treadonly detached?: boolean;\n}\n\nexport type ManagedProcessSpawner = (\n\tcommand: string,\n\targs: ReadonlyArray<string>,\n\toptions: ManagedProcessSpawnOptions,\n) => ManagedProcessChild;\n\nexport const nodeProcessSpawner: ManagedProcessSpawner = (command, args, options) =>\n\tspawn(command, [...args], {\n\t\tcwd: options.cwd,\n\t\tenv: options.env,\n\t\tstdio: options.stdio,\n\t\tdetached: options.detached,\n\t}) as ManagedProcessChild;\n\nexport const describeProcessExitStatus = (status: ManagedProcessExitStatus): string => {\n\tif (status.code !== null) return `exit code ${status.code}`;\n\tif (status.signal !== null) return `signal ${status.signal}`;\n\treturn 'unknown exit status';\n};\n\nexport const onceProcessExit = (child: ManagedProcessChild): Promise<ManagedProcessExitStatus> =>\n\tnew Promise((resolveExit) => {\n\t\tchild.once('exit', (code, signal) => resolveExit({ code, signal }));\n\t});\n\nexport const getProcessExitStatus = (\n\tchild: ManagedProcessChild,\n): ManagedProcessExitStatus | null => {\n\tif (child.exitCode !== undefined && child.exitCode !== null) {\n\t\treturn { code: child.exitCode, signal: null };\n\t}\n\tif (child.signalCode !== undefined && child.signalCode !== null) {\n\t\treturn { code: null, signal: child.signalCode };\n\t}\n\treturn null;\n};\n\nexport const awaitProcessExit = (child: ManagedProcessChild): Promise<ManagedProcessExitStatus> =>\n\tPromise.resolve(getProcessExitStatus(child) ?? onceProcessExit(child));\n\nexport const onceProcessError = (child: ManagedProcessChild): Promise<unknown> =>\n\tnew Promise((resolveError) => {\n\t\tchild.once('error', (cause) => resolveError(cause));\n\t});\n\nexport const waitForProcessExitOrTimeout = (\n\tchild: ManagedProcessChild,\n\ttimeoutMs: number,\n): Promise<ManagedProcessExitStatus | null> =>\n\tPromise.race([\n\t\tonceProcessExit(child),\n\t\tnew Promise<null>((resolveTimeout) => setTimeout(() => resolveTimeout(null), timeoutMs)),\n\t]);\n\nexport interface TerminateManagedProcessOptions {\n\treadonly graceMs: number;\n\treadonly killTimeoutMs?: number;\n\treadonly processGroup?: boolean;\n\treadonly onEscalate?: () => Effect.Effect<void>;\n}\n\nconst signalManagedProcess = (\n\tchild: ManagedProcessChild,\n\tsignal: NodeJS.Signals,\n\tprocessGroup: boolean,\n): void => {\n\tif (processGroup && process.platform !== 'win32' && child.pid !== undefined) {\n\t\ttry {\n\t\t\tprocess.kill(-child.pid, signal);\n\t\t\treturn;\n\t\t} catch {\n\t\t\t// Fall back to the direct child signal below. This covers races\n\t\t\t// where the group has already exited and tests with fake pids.\n\t\t}\n\t}\n\tchild.kill(signal);\n};\n\nexport const terminateManagedProcess = (\n\tchild: ManagedProcessChild,\n\toptions: TerminateManagedProcessOptions,\n): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tconst exited = waitForProcessExitOrTimeout(child, options.graceMs);\n\t\tyield* Effect.sync(() => {\n\t\t\tsignalManagedProcess(child, 'SIGTERM', options.processGroup === true);\n\t\t});\n\t\tconst first = yield* Effect.promise(() => exited);\n\t\tif (first !== null) return;\n\t\tif (options.onEscalate !== undefined) {\n\t\t\tyield* options.onEscalate().pipe(Effect.ignore);\n\t\t}\n\t\tconst killed = waitForProcessExitOrTimeout(child, options.killTimeoutMs ?? 1_000);\n\t\tyield* Effect.sync(() => {\n\t\t\tsignalManagedProcess(child, 'SIGKILL', options.processGroup === true);\n\t\t});\n\t\tyield* Effect.promise(() => killed);\n\t});\n\nexport interface AwaitManagedProcessReadyOptions<E, R = never> {\n\treadonly ready: Effect.Effect<void, E, R>;\n\treadonly exit: Promise<ManagedProcessExitStatus>;\n\treadonly processError: Promise<unknown>;\n\treadonly onExitBeforeReady: (status: ManagedProcessExitStatus) => E;\n\treadonly onProcessErrorBeforeReady: (cause: unknown) => E;\n}\n\nexport const awaitManagedProcessReady = <E, R = never>(\n\toptions: AwaitManagedProcessReadyOptions<E, R>,\n): Effect.Effect<void, E, R> =>\n\tEffect.raceFirst(\n\t\toptions.ready,\n\t\tEffect.raceFirst(\n\t\t\tEffect.promise(() => options.exit).pipe(\n\t\t\t\tEffect.flatMap((status) => Effect.fail(options.onExitBeforeReady(status))),\n\t\t\t),\n\t\t\tEffect.promise(() => options.processError).pipe(\n\t\t\t\tEffect.flatMap((cause) => Effect.fail(options.onProcessErrorBeforeReady(cause))),\n\t\t\t),\n\t\t),\n\t);\n"],"mappings":";;;;;;;;;;;;;;;AAsCA,MAAa,sBAA6C,SAAS,MAAM,YACxE,MAAM,SAAS,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"process-supervisor.mjs","names":[],"sources":["../../../src/substrate/runtime/process-supervisor.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport type { Readable } from 'node:stream';\n\nimport { Effect } from 'effect';\n\nexport interface ManagedProcessExitStatus {\n\treadonly code: number | null;\n\treadonly signal: NodeJS.Signals | null;\n}\n\nexport interface ManagedProcessChild {\n\treadonly pid?: number;\n\treadonly stdout?: Readable | null;\n\treadonly stderr?: Readable | null;\n\treadonly exitCode?: number | null;\n\treadonly signalCode?: NodeJS.Signals | null;\n\tkill(signal?: NodeJS.Signals): boolean;\n\ton(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\ton(event: 'error', listener: (cause: Error) => void): this;\n\tonce(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\tonce(event: 'error', listener: (cause: Error) => void): this;\n\toff(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;\n\toff(event: 'error', listener: (cause: Error) => void): this;\n}\n\nexport interface ManagedProcessSpawnOptions {\n\treadonly cwd: string;\n\treadonly env: NodeJS.ProcessEnv;\n\treadonly stdio: 'pipe';\n\treadonly detached?: boolean;\n}\n\nexport type ManagedProcessSpawner = (\n\tcommand: string,\n\targs: ReadonlyArray<string>,\n\toptions: ManagedProcessSpawnOptions,\n) => ManagedProcessChild;\n\nexport const nodeProcessSpawner: ManagedProcessSpawner = (command, args, options) =>\n\tspawn(command, [...args], {\n\t\tcwd: options.cwd,\n\t\tenv: options.env,\n\t\tstdio: options.stdio,\n\t\tdetached: options.detached,\n\t}) as ManagedProcessChild;\n\nexport const describeProcessExitStatus = (status: ManagedProcessExitStatus): string => {\n\tif (status.code !== null) return `exit code ${status.code}`;\n\tif (status.signal !== null) return `signal ${status.signal}`;\n\treturn 'unknown exit status';\n};\n\nexport const onceProcessExit = (child: ManagedProcessChild): Promise<ManagedProcessExitStatus> =>\n\tnew Promise((resolveExit) => {\n\t\tchild.once('exit', (code, signal) => resolveExit({ code, signal }));\n\t});\n\nexport const getProcessExitStatus = (\n\tchild: ManagedProcessChild,\n): ManagedProcessExitStatus | null => {\n\tif (child.exitCode !== undefined && child.exitCode !== null) {\n\t\treturn { code: child.exitCode, signal: null };\n\t}\n\tif (child.signalCode !== undefined && child.signalCode !== null) {\n\t\treturn { code: null, signal: child.signalCode };\n\t}\n\treturn null;\n};\n\nexport const awaitProcessExit = (child: ManagedProcessChild): Promise<ManagedProcessExitStatus> =>\n\tPromise.resolve(getProcessExitStatus(child) ?? onceProcessExit(child));\n\nexport const onceProcessError = (child: ManagedProcessChild): Promise<unknown> =>\n\tnew Promise((resolveError) => {\n\t\tchild.once('error', (cause) => resolveError(cause));\n\t});\n\nexport const waitForProcessExitOrTimeout = (\n\tchild: ManagedProcessChild,\n\ttimeoutMs: number,\n): Promise<ManagedProcessExitStatus | null> =>\n\tPromise.race([\n\t\tonceProcessExit(child),\n\t\tnew Promise<null>((resolveTimeout) => setTimeout(() => resolveTimeout(null), timeoutMs)),\n\t]);\n\nexport interface TerminateManagedProcessOptions {\n\treadonly graceMs: number;\n\treadonly killTimeoutMs?: number;\n\treadonly processGroup?: boolean;\n\treadonly onEscalate?: () => Effect.Effect<void>;\n}\n\nconst signalManagedProcess = (\n\tchild: ManagedProcessChild,\n\tsignal: NodeJS.Signals,\n\tprocessGroup: boolean,\n): void => {\n\tif (processGroup && process.platform !== 'win32' && child.pid !== undefined) {\n\t\ttry {\n\t\t\tprocess.kill(-child.pid, signal);\n\t\t\treturn;\n\t\t} catch {\n\t\t\t// Fall back to the direct child signal below. This covers races\n\t\t\t// where the group has already exited and tests with fake pids.\n\t\t}\n\t}\n\tchild.kill(signal);\n};\n\nexport const terminateManagedProcess = (\n\tchild: ManagedProcessChild,\n\toptions: TerminateManagedProcessOptions,\n): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tconst exited = waitForProcessExitOrTimeout(child, options.graceMs);\n\t\tyield* Effect.sync(() => {\n\t\t\tsignalManagedProcess(child, 'SIGTERM', options.processGroup === true);\n\t\t});\n\t\tconst first = yield* Effect.promise(() => exited);\n\t\tif (first !== null) return;\n\t\tif (options.onEscalate !== undefined) {\n\t\t\tyield* options.onEscalate().pipe(Effect.ignore);\n\t\t}\n\t\tconst killed = waitForProcessExitOrTimeout(child, options.killTimeoutMs ?? 1_000);\n\t\tyield* Effect.sync(() => {\n\t\t\tsignalManagedProcess(child, 'SIGKILL', options.processGroup === true);\n\t\t});\n\t\tyield* Effect.promise(() => killed);\n\t});\n\nexport interface AwaitManagedProcessReadyOptions<E, R = never> {\n\treadonly ready: Effect.Effect<void, E, R>;\n\treadonly exit: Promise<ManagedProcessExitStatus>;\n\treadonly processError: Promise<unknown>;\n\treadonly onExitBeforeReady: (status: ManagedProcessExitStatus) => E;\n\treadonly onProcessErrorBeforeReady: (cause: unknown) => E;\n}\n\nexport const awaitManagedProcessReady = <E, R = never>(\n\toptions: AwaitManagedProcessReadyOptions<E, R>,\n): Effect.Effect<void, E, R> =>\n\tEffect.raceFirst(\n\t\toptions.ready,\n\t\tEffect.raceFirst(\n\t\t\tEffect.promise(() => options.exit).pipe(\n\t\t\t\tEffect.flatMap((status) => Effect.fail(options.onExitBeforeReady(status))),\n\t\t\t),\n\t\t\tEffect.promise(() => options.processError).pipe(\n\t\t\t\tEffect.flatMap((cause) => Effect.fail(options.onProcessErrorBeforeReady(cause))),\n\t\t\t),\n\t\t),\n\t);\n"],"mappings":";;;;;;;;;;;;;;;AAsCA,MAAa,sBAA6C,SAAS,MAAM,YACxE,MAAM,SAAS,CAAC,GAAG,IAAI,GAAG;CACzB,KAAK,QAAQ;CACb,KAAK,QAAQ;CACb,OAAO,QAAQ;CACf,UAAU,QAAQ;AACnB,CAAC;AAEF,MAAa,6BAA6B,WAA6C;CACtF,IAAI,OAAO,SAAS,MAAM,OAAO,aAAa,OAAO;CACrD,IAAI,OAAO,WAAW,MAAM,OAAO,UAAU,OAAO;CACpD,OAAO;AACR;AAEA,MAAa,mBAAmB,UAC/B,IAAI,SAAS,gBAAgB;CAC5B,MAAM,KAAK,SAAS,MAAM,WAAW,YAAY;EAAE;EAAM;CAAO,CAAC,CAAC;AACnE,CAAC;AAEF,MAAa,wBACZ,UACqC;CACrC,IAAI,MAAM,aAAa,KAAA,KAAa,MAAM,aAAa,MACtD,OAAO;EAAE,MAAM,MAAM;EAAU,QAAQ;CAAK;CAE7C,IAAI,MAAM,eAAe,KAAA,KAAa,MAAM,eAAe,MAC1D,OAAO;EAAE,MAAM;EAAM,QAAQ,MAAM;CAAW;CAE/C,OAAO;AACR;AAEA,MAAa,oBAAoB,UAChC,QAAQ,QAAQ,qBAAqB,KAAK,KAAK,gBAAgB,KAAK,CAAC;AAEtE,MAAa,oBAAoB,UAChC,IAAI,SAAS,iBAAiB;CAC7B,MAAM,KAAK,UAAU,UAAU,aAAa,KAAK,CAAC;AACnD,CAAC;AAEF,MAAa,+BACZ,OACA,cAEA,QAAQ,KAAK,CACZ,gBAAgB,KAAK,GACrB,IAAI,SAAe,mBAAmB,iBAAiB,eAAe,IAAI,GAAG,SAAS,CAAC,CACxF,CAAC;AASF,MAAM,wBACL,OACA,QACA,iBACU;CACV,IAAI,gBAAgB,QAAQ,aAAa,WAAW,MAAM,QAAQ,KAAA,GACjE,IAAI;EACH,QAAQ,KAAK,CAAC,MAAM,KAAK,MAAM;EAC/B;CACD,QAAQ,CAGR;CAED,MAAM,KAAK,MAAM;AAClB;AAEA,MAAa,2BACZ,OACA,YAEA,OAAO,IAAI,aAAa;CACvB,MAAM,SAAS,4BAA4B,OAAO,QAAQ,OAAO;CACjE,OAAO,OAAO,WAAW;EACxB,qBAAqB,OAAO,WAAW,QAAQ,iBAAiB,IAAI;CACrE,CAAC;CAED,KAAI,OADiB,OAAO,cAAc,MAAM,OAClC,MAAM;CACpB,IAAI,QAAQ,eAAe,KAAA,GAC1B,OAAO,QAAQ,WAAW,CAAC,CAAC,KAAK,OAAO,MAAM;CAE/C,MAAM,SAAS,4BAA4B,OAAO,QAAQ,iBAAiB,GAAK;CAChF,OAAO,OAAO,WAAW;EACxB,qBAAqB,OAAO,WAAW,QAAQ,iBAAiB,IAAI;CACrE,CAAC;CACD,OAAO,OAAO,cAAc,MAAM;AACnC,CAAC;AAUF,MAAa,4BACZ,YAEA,OAAO,UACN,QAAQ,OACR,OAAO,UACN,OAAO,cAAc,QAAQ,IAAI,CAAC,CAAC,KAClC,OAAO,SAAS,WAAW,OAAO,KAAK,QAAQ,kBAAkB,MAAM,CAAC,CAAC,CAC1E,GACA,OAAO,cAAc,QAAQ,YAAY,CAAC,CAAC,KAC1C,OAAO,SAAS,UAAU,OAAO,KAAK,QAAQ,0BAA0B,KAAK,CAAC,CAAC,CAChF,CACD,CACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operational-endpoints.mjs","names":[],"sources":["../../../../src/substrate/runtime/projection/operational-endpoints.ts"],"sourcesContent":["import { endpointKey, type PluginKey } from '../../brand.ts';\nimport type { EngineEvent } from '../../events.ts';\n\ntype EndpointRegisteredEvent = Extract<EngineEvent, { readonly tag: 'endpoint.registered' }>;\n\n// Endpoint-projection policy is closed-allowlist:\n// only the fields enumerated below are projected as operational\n// endpoints. Anything else — including legitimate sensitive URLs\n// (`pairUrl`, `walletPairingToken`, OAuth `tokenUrl`, etc.) and any\n// future field a plugin adds — is ignored by default. A plugin that\n// wants a new field projected must extend this list AND ship a\n// projection test pinning its display name. The previous loose\n// \"skip-if-matches-sensitive-regex\" check dropped legitimate OAuth\n// fields like `tokenUrl` and missed novel sensitive names; the\n// allowlist removes that whole class of false-negative/false-positive\n// pair.\nconst SAFE_URL_FIELDS = {\n\turl: 'http',\n\trpcUrl: 'rpc',\n\tfaucetUrl: 'faucet',\n\tgraphqlUrl: 'graphql',\n} as const;\n\nexport interface OperationalEndpointProjectionOptions {\n\t/** Routable capabilities are the authoritative public endpoint\n\t * source. Resolved-value URL scraping is only a fallback for\n\t * plugins with no router contribution, such as live/local-rpc\n\t * network modes. */\n\treadonly routablesPresent?: boolean;\n}\n\nexport const operationalEndpointEventsFromResolvedValue = (\n\tpluginKey: PluginKey,\n\tvalue: unknown,\n\tregisteredAt: number,\n\toptions: OperationalEndpointProjectionOptions = {},\n): ReadonlyArray<EndpointRegisteredEvent> => {\n\tif (options.routablesPresent === true) return [];\n\tif (value === null || typeof value !== 'object') return [];\n\tconst record = value as Readonly<Record<string, unknown>>;\n\tconst events: Array<EndpointRegisteredEvent> = [];\n\tfor (const [field, name] of Object.entries(SAFE_URL_FIELDS)) {\n\t\tconst raw = record[field];\n\t\tif (typeof raw !== 'string' || !isHttpUrl(raw)) continue;\n\t\tevents.push({\n\t\t\ttag: 'endpoint.registered',\n\t\t\tendpoint: {\n\t\t\t\tendpointKey: endpointKey(`${pluginKey}:${field}`),\n\t\t\t\tpluginKey,\n\t\t\t\tname,\n\t\t\t\turl: raw,\n\t\t\t\tdisplayUrl: null,\n\t\t\t\twireProtocol: 'http',\n\t\t\t\tregisteredAt,\n\t\t\t},\n\t\t});\n\t}\n\treturn events;\n};\n\nconst isHttpUrl = (value: string): boolean => {\n\ttry {\n\t\tconst parsed = new URL(value);\n\t\treturn parsed.protocol === 'http:' || parsed.protocol === 'https:';\n\t} catch {\n\t\treturn false;\n\t}\n};\n"],"mappings":";;AAgBA,MAAM,kBAAkB;CACvB,KAAK;CACL,QAAQ;CACR,WAAW;CACX,YAAY;
|
|
1
|
+
{"version":3,"file":"operational-endpoints.mjs","names":[],"sources":["../../../../src/substrate/runtime/projection/operational-endpoints.ts"],"sourcesContent":["import { endpointKey, type PluginKey } from '../../brand.ts';\nimport type { EngineEvent } from '../../events.ts';\n\ntype EndpointRegisteredEvent = Extract<EngineEvent, { readonly tag: 'endpoint.registered' }>;\n\n// Endpoint-projection policy is closed-allowlist:\n// only the fields enumerated below are projected as operational\n// endpoints. Anything else — including legitimate sensitive URLs\n// (`pairUrl`, `walletPairingToken`, OAuth `tokenUrl`, etc.) and any\n// future field a plugin adds — is ignored by default. A plugin that\n// wants a new field projected must extend this list AND ship a\n// projection test pinning its display name. The previous loose\n// \"skip-if-matches-sensitive-regex\" check dropped legitimate OAuth\n// fields like `tokenUrl` and missed novel sensitive names; the\n// allowlist removes that whole class of false-negative/false-positive\n// pair.\nconst SAFE_URL_FIELDS = {\n\turl: 'http',\n\trpcUrl: 'rpc',\n\tfaucetUrl: 'faucet',\n\tgraphqlUrl: 'graphql',\n} as const;\n\nexport interface OperationalEndpointProjectionOptions {\n\t/** Routable capabilities are the authoritative public endpoint\n\t * source. Resolved-value URL scraping is only a fallback for\n\t * plugins with no router contribution, such as live/local-rpc\n\t * network modes. */\n\treadonly routablesPresent?: boolean;\n}\n\nexport const operationalEndpointEventsFromResolvedValue = (\n\tpluginKey: PluginKey,\n\tvalue: unknown,\n\tregisteredAt: number,\n\toptions: OperationalEndpointProjectionOptions = {},\n): ReadonlyArray<EndpointRegisteredEvent> => {\n\tif (options.routablesPresent === true) return [];\n\tif (value === null || typeof value !== 'object') return [];\n\tconst record = value as Readonly<Record<string, unknown>>;\n\tconst events: Array<EndpointRegisteredEvent> = [];\n\tfor (const [field, name] of Object.entries(SAFE_URL_FIELDS)) {\n\t\tconst raw = record[field];\n\t\tif (typeof raw !== 'string' || !isHttpUrl(raw)) continue;\n\t\tevents.push({\n\t\t\ttag: 'endpoint.registered',\n\t\t\tendpoint: {\n\t\t\t\tendpointKey: endpointKey(`${pluginKey}:${field}`),\n\t\t\t\tpluginKey,\n\t\t\t\tname,\n\t\t\t\turl: raw,\n\t\t\t\tdisplayUrl: null,\n\t\t\t\twireProtocol: 'http',\n\t\t\t\tregisteredAt,\n\t\t\t},\n\t\t});\n\t}\n\treturn events;\n};\n\nconst isHttpUrl = (value: string): boolean => {\n\ttry {\n\t\tconst parsed = new URL(value);\n\t\treturn parsed.protocol === 'http:' || parsed.protocol === 'https:';\n\t} catch {\n\t\treturn false;\n\t}\n};\n"],"mappings":";;AAgBA,MAAM,kBAAkB;CACvB,KAAK;CACL,QAAQ;CACR,WAAW;CACX,YAAY;AACb;AAUA,MAAa,8CACZ,WACA,OACA,cACA,UAAgD,CAAC,MACL;CAC5C,IAAI,QAAQ,qBAAqB,MAAM,OAAO,CAAC;CAC/C,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO,CAAC;CACzD,MAAM,SAAS;CACf,MAAM,SAAyC,CAAC;CAChD,KAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,eAAe,GAAG;EAC5D,MAAM,MAAM,OAAO;EACnB,IAAI,OAAO,QAAQ,YAAY,CAAC,UAAU,GAAG,GAAG;EAChD,OAAO,KAAK;GACX,KAAK;GACL,UAAU;IACT,aAAa,YAAY,GAAG,UAAU,GAAG,OAAO;IAChD;IACA;IACA,KAAK;IACL,YAAY;IACZ,cAAc;IACd;GACD;EACD,CAAC;CACF;CACA,OAAO;AACR;AAEA,MAAM,aAAa,UAA2B;CAC7C,IAAI;EACH,MAAM,SAAS,IAAI,IAAI,KAAK;EAC5B,OAAO,OAAO,aAAa,WAAW,OAAO,aAAa;CAC3D,QAAQ;EACP,OAAO;CACR;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-ref.mjs","names":[],"sources":["../../../../src/substrate/runtime/projection/state-ref.ts"],"sourcesContent":["// Subscribable projection state-ref.\n//\n// Architecture § Renderer § \"Subscribable projection — exact field\n// enumeration\" (G2) + § \"Renderer mount lifetime vs engine cycle\n// lifetime\" (decision #8). The state-ref is process-scoped (mounted\n// once); the engine cycle re-runs many times and never re-creates the\n// ref. The renderer subscribes once at mount; the engine updates\n// in-place across cycles.\n//\n// The projection's TypeScript shape is enforced closed by\n// `__ProjectionFieldsClosed` in `../../projection.ts`. Adding a\n// display-vocabulary field (`title`, `primary`, `extras`) at this\n// layer would fail to assign into `SubscribableState` — renderers\n// derive those from `rows[*].kind` + display rules.\n\nimport { Effect, SubscriptionRef } from 'effect';\n\nimport type {\n\tBuildEntry,\n\tAccountProjection,\n\tEndpoint,\n\tPackageProjection,\n\tRow,\n\tStructuredError,\n\tSubscribableState,\n} from '../../projection.ts';\n\n/**\n * Empty initial projection. Cycle starts at `id: 0`, `phase:\n * 'booting'`. Identity fields are placeholder; the engine MUST\n * replace via `update.ts::setIdentity` before any renderer is\n * notified of anything user-visible.\n *\n * The shape here is the entire contract the renderer sees — no\n * service names, no display vocabulary. If a future field needs\n * adding, the architecture-revision discipline kicks in via\n * `__ProjectionFieldsClosed`.\n */\nexport const emptyProjection = (): SubscribableState => ({\n\tidentity: { app: '', stack: '', network: '' },\n\tcycle: { id: 0, startedAt: 0, phase: 'booting' },\n\trows: [],\n\tendpoints: [],\n\taccounts: [],\n\tpackages: [],\n\terrors: [],\n\tlastEvent: { seq: 0, at: 0 },\n\tstackBuild: [],\n});\n\n/**\n * Create the subscribable projection ref. One per process.\n *\n * Engine cycle: the ref outlives the engine cycle. Stop-and-restart\n * keeps the same ref; only `cycle.id` increments. Renderers see a\n * continuous stream — that's the whole point of decoupling renderer\n * lifetime from engine cycle.\n */\nexport const makeProjectionRef = (): Effect.Effect<\n\tSubscriptionRef.SubscriptionRef<SubscribableState>\n> => SubscriptionRef.make(emptyProjection());\n\n/**\n * Sync-only constructor for the subscribable projection ref. The\n * `runStack(...)` API allocates the ref BEFORE the supervised\n * Effect runs so callers can subscribe to `state.changes` ahead of\n * boot — that allocation path has no Effect runtime to run under and\n * must complete synchronously.\n *\n * Callers that already live inside an Effect should use the\n * Effect-wrapped `makeProjectionRef()` so future changes to the\n * constructor stay Effect-correct without breaking those call\n * sites. This sync variant is pinned at the substrate boundary: any\n * future refactor of `makeProjectionRef` (e.g. layering a `withSpan`\n * or annotation) MUST keep `makeProjectionRefSync` synchronous — if\n * that ever becomes impossible the boot-time call site needs a\n * different signaling strategy (Deferred-handoff), not a silent\n * `Effect.runSync` crash on an async effect.\n *\n * `SubscriptionRef.make` is sync-effect today (no async, no\n * side-effects beyond an `unbounded` `PubSub.unbounded({ replay:\n * 1 })`), so `Effect.runSync` is safe right now; the indirection\n * exists so the constraint is documented at the constructor, not\n * inferred from the caller.\n */\nexport const makeProjectionRefSync = (): SubscriptionRef.SubscriptionRef<SubscribableState> =>\n\tEffect.runSync(makeProjectionRef());\n\n
|
|
1
|
+
{"version":3,"file":"state-ref.mjs","names":[],"sources":["../../../../src/substrate/runtime/projection/state-ref.ts"],"sourcesContent":["// Subscribable projection state-ref.\n//\n// Architecture § Renderer § \"Subscribable projection — exact field\n// enumeration\" (G2) + § \"Renderer mount lifetime vs engine cycle\n// lifetime\" (decision #8). The state-ref is process-scoped (mounted\n// once); the engine cycle re-runs many times and never re-creates the\n// ref. The renderer subscribes once at mount; the engine updates\n// in-place across cycles.\n//\n// The projection's TypeScript shape is enforced closed by\n// `__ProjectionFieldsClosed` in `../../projection.ts`. Adding a\n// display-vocabulary field (`title`, `primary`, `extras`) at this\n// layer would fail to assign into `SubscribableState` — renderers\n// derive those from `rows[*].kind` + display rules.\n\nimport { Effect, SubscriptionRef } from 'effect';\n\nimport type {\n\tBuildEntry,\n\tAccountProjection,\n\tEndpoint,\n\tPackageProjection,\n\tRow,\n\tStructuredError,\n\tSubscribableState,\n} from '../../projection.ts';\n\n/**\n * Empty initial projection. Cycle starts at `id: 0`, `phase:\n * 'booting'`. Identity fields are placeholder; the engine MUST\n * replace via `update.ts::setIdentity` before any renderer is\n * notified of anything user-visible.\n *\n * The shape here is the entire contract the renderer sees — no\n * service names, no display vocabulary. If a future field needs\n * adding, the architecture-revision discipline kicks in via\n * `__ProjectionFieldsClosed`.\n */\nexport const emptyProjection = (): SubscribableState => ({\n\tidentity: { app: '', stack: '', network: '' },\n\tcycle: { id: 0, startedAt: 0, phase: 'booting' },\n\trows: [],\n\tendpoints: [],\n\taccounts: [],\n\tpackages: [],\n\terrors: [],\n\tlastEvent: { seq: 0, at: 0 },\n\tstackBuild: [],\n});\n\n/**\n * Create the subscribable projection ref. One per process.\n *\n * Engine cycle: the ref outlives the engine cycle. Stop-and-restart\n * keeps the same ref; only `cycle.id` increments. Renderers see a\n * continuous stream — that's the whole point of decoupling renderer\n * lifetime from engine cycle.\n */\nexport const makeProjectionRef = (): Effect.Effect<\n\tSubscriptionRef.SubscriptionRef<SubscribableState>\n> => SubscriptionRef.make(emptyProjection());\n\n/**\n * Sync-only constructor for the subscribable projection ref. The\n * `runStack(...)` API allocates the ref BEFORE the supervised\n * Effect runs so callers can subscribe to `state.changes` ahead of\n * boot — that allocation path has no Effect runtime to run under and\n * must complete synchronously.\n *\n * Callers that already live inside an Effect should use the\n * Effect-wrapped `makeProjectionRef()` so future changes to the\n * constructor stay Effect-correct without breaking those call\n * sites. This sync variant is pinned at the substrate boundary: any\n * future refactor of `makeProjectionRef` (e.g. layering a `withSpan`\n * or annotation) MUST keep `makeProjectionRefSync` synchronous — if\n * that ever becomes impossible the boot-time call site needs a\n * different signaling strategy (Deferred-handoff), not a silent\n * `Effect.runSync` crash on an async effect.\n *\n * `SubscriptionRef.make` is sync-effect today (no async, no\n * side-effects beyond an `unbounded` `PubSub.unbounded({ replay:\n * 1 })`), so `Effect.runSync` is safe right now; the indirection\n * exists so the constraint is documented at the constructor, not\n * inferred from the caller.\n */\nexport const makeProjectionRefSync = (): SubscriptionRef.SubscriptionRef<SubscribableState> =>\n\tEffect.runSync(makeProjectionRef());\n\n// Re-export the sub-types renderers reach for so they don't import\n// across the substrate boundary directly — `state-ref.ts` is the\n// renderer-facing entry point.\nexport type {\n\tAccountProjection,\n\tBuildEntry,\n\tEndpoint,\n\tPackageProjection,\n\tRow,\n\tStructuredError,\n\tSubscribableState,\n};\n"],"mappings":";;;;;;;;;;;;;AAsCA,MAAa,yBAA4C;CACxD,UAAU;EAAE,KAAK;EAAI,OAAO;EAAI,SAAS;CAAG;CAC5C,OAAO;EAAE,IAAI;EAAG,WAAW;EAAG,OAAO;CAAU;CAC/C,MAAM,CAAC;CACP,WAAW,CAAC;CACZ,UAAU,CAAC;CACX,UAAU,CAAC;CACX,QAAQ,CAAC;CACT,WAAW;EAAE,KAAK;EAAG,IAAI;CAAE;CAC3B,YAAY,CAAC;AACd;;;;;;;;;AAUA,MAAa,0BAER,gBAAgB,KAAK,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyB3C,MAAa,8BACZ,OAAO,QAAQ,kBAAkB,CAAC"}
|
|
@@ -1,15 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AccountProjectionSchema, PackageProjectionSchema } from "./persisted.mjs";
|
|
1
|
+
import { LogAttr } from "../observability/log-attrs.mjs";
|
|
3
2
|
import { eventAtOrNull } from "../../event-time.mjs";
|
|
4
|
-
import { applyLifecycleFact, factFromEvent } from "../lifecycle/lifecycle-fact.mjs";
|
|
5
3
|
import { Effect, Schema, SubscriptionRef } from "effect";
|
|
6
4
|
//#region src/substrate/runtime/projection/update.ts
|
|
5
|
+
/** Structural schema for a `projection.updated[account]` payload. */
|
|
6
|
+
const AccountProjectionSchema = Schema.Struct({
|
|
7
|
+
key: Schema.String,
|
|
8
|
+
rowKey: Schema.NullOr(Schema.String),
|
|
9
|
+
name: Schema.String,
|
|
10
|
+
address: Schema.NullOr(Schema.String),
|
|
11
|
+
scheme: Schema.NullOr(Schema.Literals([
|
|
12
|
+
"ed25519",
|
|
13
|
+
"secp256k1",
|
|
14
|
+
"secp256r1"
|
|
15
|
+
])),
|
|
16
|
+
source: Schema.NullOr(Schema.Literals(["real", "impersonate"])),
|
|
17
|
+
funding: Schema.Struct({
|
|
18
|
+
status: Schema.Literals([
|
|
19
|
+
"pending",
|
|
20
|
+
"funded",
|
|
21
|
+
"skipped",
|
|
22
|
+
"failed",
|
|
23
|
+
"unknown"
|
|
24
|
+
]),
|
|
25
|
+
balanceMist: Schema.NullOr(Schema.String),
|
|
26
|
+
requestedMist: Schema.NullOr(Schema.String),
|
|
27
|
+
entries: Schema.optional(Schema.Array(Schema.Struct({
|
|
28
|
+
coin: Schema.String,
|
|
29
|
+
fullCoinType: Schema.String,
|
|
30
|
+
amount: Schema.String,
|
|
31
|
+
status: Schema.Literals([
|
|
32
|
+
"funded",
|
|
33
|
+
"already-satisfied",
|
|
34
|
+
"skipped"
|
|
35
|
+
])
|
|
36
|
+
})))
|
|
37
|
+
}),
|
|
38
|
+
walletVisible: Schema.Boolean,
|
|
39
|
+
updatedAt: Schema.Number
|
|
40
|
+
});
|
|
41
|
+
/** Structural schema for a `projection.updated[package]` payload. */
|
|
42
|
+
const PackageProjectionSchema = Schema.Struct({
|
|
43
|
+
key: Schema.String,
|
|
44
|
+
rowKey: Schema.NullOr(Schema.String),
|
|
45
|
+
name: Schema.String,
|
|
46
|
+
kind: Schema.Literals(["local", "known"]),
|
|
47
|
+
packageId: Schema.String,
|
|
48
|
+
upgradeCapId: Schema.NullOr(Schema.String),
|
|
49
|
+
mvrPlaceholder: Schema.String,
|
|
50
|
+
sourcePath: Schema.NullOr(Schema.String),
|
|
51
|
+
updatedAt: Schema.Number
|
|
52
|
+
});
|
|
7
53
|
/** Bounded buffer for the top-level errors list. */
|
|
8
54
|
const MAX_ERRORS_KEPT = 100;
|
|
9
55
|
/** Bounded buffer for the top-level build-entries list. */
|
|
10
56
|
const MAX_BUILD_ENTRIES_KEPT = 200;
|
|
11
57
|
/** Per-row bounded log-tail length. Architecture: default 100 lines. */
|
|
12
58
|
const MAX_ROW_LOG_LINES = 100;
|
|
59
|
+
/** Project a lifecycle-shaped `EngineEvent` into a per-plugin delta.
|
|
60
|
+
* Returns `null` for events that don't carry lifecycle information so
|
|
61
|
+
* the reducer can short-circuit. The substrate stays event-name-blind
|
|
62
|
+
* by routing through this single mapping table. */
|
|
63
|
+
const factFromEvent = (event) => {
|
|
64
|
+
switch (event.tag) {
|
|
65
|
+
case "lifecycle.statusChanged": return {
|
|
66
|
+
pluginKey: event.pluginKey,
|
|
67
|
+
delta: { status: event.to }
|
|
68
|
+
};
|
|
69
|
+
case "lifecycle.phaseSet": return {
|
|
70
|
+
pluginKey: event.pluginKey,
|
|
71
|
+
delta: { phase: event.phase }
|
|
72
|
+
};
|
|
73
|
+
default: return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Apply a fact delta to a row. Pure. Fields not in the delta are
|
|
77
|
+
* preserved verbatim — the merge-not-replace shape `LifecycleFact`
|
|
78
|
+
* promises. */
|
|
79
|
+
const applyLifecycleFact = (row, delta) => ({
|
|
80
|
+
...row,
|
|
81
|
+
...delta.status !== void 0 ? { status: delta.status } : {},
|
|
82
|
+
...delta.phase !== void 0 ? { phase: delta.phase } : {},
|
|
83
|
+
...delta.selectiveRestartHighlight !== void 0 ? { selectiveRestartHighlight: delta.selectiveRestartHighlight } : {}
|
|
84
|
+
});
|
|
13
85
|
/**
|
|
14
86
|
* Pure projection reducer. One event in, one new state out. All field
|
|
15
87
|
* writes preserve `readonly` invariants by constructing fresh
|
|
@@ -56,13 +128,17 @@ const applyEvent = (state, event, prevalidated) => {
|
|
|
56
128
|
case "strategy.registered":
|
|
57
129
|
case "strategy.unregistered":
|
|
58
130
|
case "manifest.flushed":
|
|
59
|
-
case "codegen.emitted":
|
|
60
|
-
case "snapshot.captureStarted":
|
|
61
|
-
case "snapshot.captureProgress":
|
|
131
|
+
case "codegen.emitted": return withTouched({});
|
|
62
132
|
case "snapshot.captureSkipped":
|
|
63
133
|
case "snapshot.captureFailed":
|
|
64
|
-
case "snapshot.captured":
|
|
65
|
-
|
|
134
|
+
case "snapshot.captured": return withTouched({ cycle: state.cycle.phase === "snapshotting" ? {
|
|
135
|
+
...state.cycle,
|
|
136
|
+
phase: "running"
|
|
137
|
+
} : state.cycle });
|
|
138
|
+
case "snapshot.restored": return withTouched({ cycle: {
|
|
139
|
+
...state.cycle,
|
|
140
|
+
phase: "restoring"
|
|
141
|
+
} });
|
|
66
142
|
case "shutdown.escalated": return withTouched({ cycle: {
|
|
67
143
|
...state.cycle,
|
|
68
144
|
phase: "shutting-down"
|
|
@@ -133,7 +209,7 @@ const updateRef = (ref, event) => Effect.gen(function* () {
|
|
|
133
209
|
if (event.tag === "projection.updated") {
|
|
134
210
|
if (event.kind === "account" || event.kind === "package") {
|
|
135
211
|
prevalidated = tryDecodeProjectionPayload(event.kind === "account" ? AccountProjectionSchema : PackageProjectionSchema, event.payload);
|
|
136
|
-
if (!prevalidated.ok) yield* Effect.logWarning(`projection.updated: dropping malformed ${event.kind} payload for key=${event.key}`).pipe(Effect.annotateLogs({ [
|
|
212
|
+
if (!prevalidated.ok) yield* Effect.logWarning(`projection.updated: dropping malformed ${event.kind} payload for key=${event.key}`).pipe(Effect.annotateLogs({ [LogAttr.errorMessage]: formatDecodeIssue(prevalidated.cause) }));
|
|
137
213
|
}
|
|
138
214
|
}
|
|
139
215
|
yield* SubscriptionRef.update(ref, (state) => {
|
|
@@ -199,7 +275,7 @@ const upsertRow = (rows, key, update) => {
|
|
|
199
275
|
const appendLogLine = (row, line, level) => {
|
|
200
276
|
const nextLines = [...row.logTail.lines, line];
|
|
201
277
|
const truncated = nextLines.length > MAX_ROW_LOG_LINES;
|
|
202
|
-
const trimmed = truncated ? nextLines.slice(-
|
|
278
|
+
const trimmed = truncated ? nextLines.slice(-100) : nextLines;
|
|
203
279
|
return {
|
|
204
280
|
...row,
|
|
205
281
|
logTail: {
|
|
@@ -259,6 +335,6 @@ const pushBounded = (xs, value, max) => {
|
|
|
259
335
|
return next.length > max ? next.slice(-max) : next;
|
|
260
336
|
};
|
|
261
337
|
//#endregion
|
|
262
|
-
export {
|
|
338
|
+
export { AccountProjectionSchema, PackageProjectionSchema, declareAccount, setIdentity, updateRef };
|
|
263
339
|
|
|
264
340
|
//# sourceMappingURL=update.mjs.map
|