@mysten-incubation/devstack 0.2.0 → 0.4.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 +1 -1
- package/dashboard-ui/assets/{grpc-a4usE3Nk.js → grpc-CpkDu4SA.js} +1 -1
- package/dashboard-ui/assets/{index-CRYJ4pru.js → index-jLPRmjst.js} +39 -33
- package/dashboard-ui/index.html +1 -1
- package/dist/api/inference-network.d.mts +5 -5
- package/dist/api/inference-network.mjs +5 -5
- package/dist/api/inference-network.mjs.map +1 -1
- package/dist/api/run-stack-internal.mjs +2 -3
- package/dist/api/run-stack-internal.mjs.map +1 -1
- package/dist/build-integrations/dapp-kit/index.d.mts +12 -0
- package/dist/build-integrations/dapp-kit/index.mjs +14 -1
- package/dist/build-integrations/dapp-kit/index.mjs.map +1 -1
- package/dist/build-integrations/playwright/config.d.mts +2 -1
- package/dist/build-integrations/playwright/config.mjs +1 -1
- package/dist/build-integrations/playwright/config.mjs.map +1 -1
- package/dist/build-integrations/playwright/errors.d.mts +1 -1
- package/dist/build-integrations/playwright/errors.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/wallet-context.d.mts +2 -1
- package/dist/build-integrations/playwright/wallet-context.mjs +141 -41
- package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/dapp-kit-slot.mjs +1 -1
- package/dist/build-integrations/runtime/dapp-kit-slot.mjs.map +1 -1
- package/dist/build-integrations/vite/index.d.mts +82 -40
- package/dist/build-integrations/vite/index.mjs +281 -112
- package/dist/build-integrations/vite/index.mjs.map +1 -1
- package/dist/build-integrations/vitest/config.mjs +3 -4
- package/dist/build-integrations/vitest/config.mjs.map +1 -1
- package/dist/build-integrations/vitest/global-setup.mjs +8 -8
- package/dist/build-integrations/vitest/global-setup.mjs.map +1 -1
- package/dist/cli/doctor-probes.mjs +7 -2
- package/dist/cli/doctor-probes.mjs.map +1 -1
- package/dist/cli/main.mjs +5 -4
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/wirings/apply.mjs +0 -1
- package/dist/cli/wirings/apply.mjs.map +1 -1
- package/dist/cli/wirings/codegen.mjs +37 -38
- package/dist/cli/wirings/codegen.mjs.map +1 -1
- package/dist/cli/wirings/dump-deployment.mjs +141 -0
- package/dist/cli/wirings/dump-deployment.mjs.map +1 -0
- package/dist/cli/wirings/snapshot.mjs +0 -1
- package/dist/cli/wirings/snapshot.mjs.map +1 -1
- package/dist/cli/wirings/up.mjs +7 -0
- package/dist/cli/wirings/up.mjs.map +1 -1
- package/dist/contracts/codegenable.d.mts +22 -25
- package/dist/contracts/codegenable.mjs +35 -4
- package/dist/contracts/codegenable.mjs.map +1 -1
- package/dist/contracts/config-bindings.mjs +43 -18
- package/dist/contracts/config-bindings.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/orchestrators/boot.d.mts +0 -10
- package/dist/orchestrators/boot.mjs +24 -54
- package/dist/orchestrators/boot.mjs.map +1 -1
- package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
- package/dist/orchestrators/codegen/config-runtime.mjs +137 -112
- package/dist/orchestrators/codegen/config-runtime.mjs.map +1 -1
- package/dist/orchestrators/codegen/deployment-network-file.mjs +130 -0
- package/dist/orchestrators/codegen/deployment-network-file.mjs.map +1 -0
- package/dist/orchestrators/codegen/deployment-strict.mjs +77 -0
- package/dist/orchestrators/codegen/deployment-strict.mjs.map +1 -0
- package/dist/orchestrators/codegen/deployment.d.mts +63 -0
- package/dist/orchestrators/codegen/deployment.mjs +153 -0
- package/dist/orchestrators/codegen/deployment.mjs.map +1 -0
- package/dist/orchestrators/codegen/errors.mjs +7 -9
- package/dist/orchestrators/codegen/errors.mjs.map +1 -1
- package/dist/orchestrators/codegen/format.mjs +20 -1
- package/dist/orchestrators/codegen/format.mjs.map +1 -1
- package/dist/orchestrators/codegen/paths.mjs +1 -8
- package/dist/orchestrators/codegen/paths.mjs.map +1 -1
- package/dist/orchestrators/codegen/service.mjs +369 -141
- package/dist/orchestrators/codegen/service.mjs.map +1 -1
- package/dist/orchestrators/layers.mjs +0 -1
- package/dist/orchestrators/layers.mjs.map +1 -1
- package/dist/orchestrators/network-options.d.mts +37 -35
- package/dist/orchestrators/network-options.mjs +13 -10
- package/dist/orchestrators/network-options.mjs.map +1 -1
- package/dist/plugins/account/codegen.d.mts +3 -4
- package/dist/plugins/account/codegen.mjs +5 -7
- package/dist/plugins/account/codegen.mjs.map +1 -1
- package/dist/plugins/account/index.mjs +1 -3
- package/dist/plugins/account/index.mjs.map +1 -1
- package/dist/plugins/coin/codegen.mjs +5 -5
- package/dist/plugins/coin/codegen.mjs.map +1 -1
- package/dist/plugins/coin/index.mjs.map +1 -1
- package/dist/plugins/dashboard/domain.mjs +18 -1
- package/dist/plugins/dashboard/domain.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/root.mjs +5 -3
- package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/types.mjs +15 -2
- package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
- package/dist/plugins/deepbook/codegen.mjs +4 -4
- package/dist/plugins/deepbook/codegen.mjs.map +1 -1
- package/dist/plugins/deepbook/index.d.mts +7 -3
- package/dist/plugins/deepbook/index.mjs +3 -3
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/internal/codegen-helpers.mjs +0 -1
- package/dist/plugins/internal/codegen-helpers.mjs.map +1 -1
- package/dist/plugins/package/codegen.d.mts +1 -12
- package/dist/plugins/package/codegen.mjs +43 -25
- package/dist/plugins/package/codegen.mjs.map +1 -1
- package/dist/plugins/package/dep-resolution.mjs +1 -1
- package/dist/plugins/package/dep-resolution.mjs.map +1 -1
- package/dist/plugins/package/index.d.mts +14 -11
- package/dist/plugins/package/index.mjs +6 -7
- package/dist/plugins/package/index.mjs.map +1 -1
- package/dist/plugins/seal/codegen.mjs +3 -3
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/sui/codegen.mjs +5 -4
- package/dist/plugins/sui/codegen.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +10 -10
- package/dist/plugins/sui/index.mjs +1 -2
- package/dist/plugins/sui/index.mjs.map +1 -1
- package/dist/plugins/sui/move-summary-runner.mjs +29 -2
- package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
- package/dist/plugins/wallet/codegen.d.mts +8 -12
- package/dist/plugins/wallet/codegen.mjs +22 -15
- package/dist/plugins/wallet/codegen.mjs.map +1 -1
- package/dist/plugins/wallet/index.d.mts +4 -3
- package/dist/plugins/wallet/index.mjs +1 -1
- package/dist/plugins/wallet/index.mjs.map +1 -1
- package/dist/plugins/wallet/service.d.mts +2 -2
- package/dist/plugins/wallet/service.mjs +2 -3
- package/dist/plugins/wallet/service.mjs.map +1 -1
- package/dist/plugins/walrus/codegen.mjs +4 -4
- package/dist/plugins/walrus/codegen.mjs.map +1 -1
- package/dist/plugins/walrus/index.mjs +1 -1
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/substrate/manifest.d.mts +17 -22
- package/dist/substrate/manifest.mjs +4 -6
- package/dist/substrate/manifest.mjs.map +1 -1
- package/dist/substrate/plugin-ctx.d.mts +6 -6
- package/dist/substrate/plugin-ctx.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
- package/dist/substrate/runtime/index.mjs +1 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.d.mts +1 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.mjs +54 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.mjs.map +1 -0
- package/dist/substrate/runtime/lifecycle/index.mjs +1 -0
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +76 -6
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
- package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
- package/dist/substrate/runtime/paths.mjs +1 -1
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs +2 -6
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
- package/dist/surfaces/cli/command-tree.mjs +6 -6
- package/dist/surfaces/cli/command-tree.mjs.map +1 -1
- package/dist/surfaces/cli/index.mjs +6 -4
- package/dist/surfaces/cli/index.mjs.map +1 -1
- package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/cli/wirings/dump-ids.mjs +0 -86
- package/dist/cli/wirings/dump-ids.mjs.map +0 -1
- package/dist/orchestrators/codegen/id-config.d.mts +0 -1
- package/dist/orchestrators/codegen/id-config.mjs +0 -107
- package/dist/orchestrators/codegen/id-config.mjs.map +0 -1
- package/dist/orchestrators/codegen/output-location.mjs +0 -32
- package/dist/orchestrators/codegen/output-location.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/plugins/walrus/index.ts"],"sourcesContent":["// Walrus plugin — barrel + factories.\n//\n// Architecture: Walrus is a service plugin that owns a local cluster\n// or describes a known deployment. The factory at this file folds the\n// four modes behind:\n//\n// - `walrus(opts?)` — local-cluster shorthand. No env\n// defaulting; use `walrusFor(...)`\n// for known deployments.\n// - `walrusFor(network)` — mode-narrowed factory namespace\n// (architecture Tension 11). Returns\n// `{ local: …, known: … }` narrowed\n// to the network's mode. Crucially,\n// the fork branch exposes ONLY\n// `.known` — `.local` is a compile\n// error on a fork-mode network.\n//\n// During `start`, the plugin emits (via the typed `ctx.*` verbs, per mode):\n//\n// Local mode (full local cluster):\n// 1. `ctx.snapshotExtra` — runtime/walrus/<name>/deploy/ subtree\n// + storage-node managed containers.\n// 2. `ctx.codegen` — `walrus-network` bindings.\n// 3. `ctx.endpoint` × (N+2) — per-node + aggregator + publisher.\n// 4. `ctx.provides` walrus-state-registry — local entry.\n// 5. `ctx.provides` endpoint-registry — N+2 entries.\n// 6. `ctx.provides` package-registry — `walrus.<name>`.\n// 7. `ctx.provides` coinType:<WAL fullCoinType> — WAL faucet\n// strategy\n// (when exchange exists).\n//\n// Known mode (read-only deployment):\n// 1. `ctx.snapshotExtra` — identity-guard only; no subtrees.\n// 2. `ctx.codegen` — `walrus-network` bindings (mode='known').\n// 3. `ctx.provides` walrus-state-registry — known entry.\n//\n// Resource id: `'walrus'` (singular). The plugin's substrate-level\n// plugin key is the same string.\n\nimport { Effect, Path } from 'effect';\n\nimport { defineModeNamespace } from '../../api/mode-narrowed-factory.ts';\nimport { definePlugin, resource, type ResourceRef } from '../../api/define-plugin.ts';\nimport type { ContainerRuntime } from '../../contracts/container-runtime.ts';\nimport type { RoutableDecl } from '../../contracts/routable.ts';\nimport type { StrategyContributorDecl } from '../../contracts/strategy-contributor.ts';\nimport { emitContributions, PluginContext } from '../../substrate/plugin-ctx.ts';\nimport { ContainerRuntimeService } from '../../runtime/docker/service.ts';\nimport { IdentityContext, StackPathsService } from '../../substrate/runtime/paths.ts';\nimport { CacheService } from '../../substrate/runtime/cache/index.ts';\nimport { deriveSubnetPrefix, withSubnetAddressing } from '../../substrate/runtime/subnet-broker.ts';\nimport type { AccountFundingCoinValue } from '../account/index.ts';\nimport { coinResourceId, type CoinResourceId } from '../coin/index.ts';\nimport { suiResource, type SuiProbeKey } from '../sui/index.ts';\n\nimport { chainProbeFor } from '../../substrate/runtime/strategy-registry/index.ts';\n\nimport { makeCodegenable, makeWalrusStaticCodegen } from './codegen.ts';\nimport { LOCAL_NETWORK_NAME } from '../../api/inference-network.ts';\nimport { walrusPluginKey } from './plugin-key.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\nimport { makeWalFaucetContribution, type WalFaucetStrategy } from './faucet-strategy.ts';\nimport { bootWalrusService, type WalrusMode } from './service.ts';\nimport {\n\tresolveLocalClusterOptions,\n\ttype WalrusLocalClusterOptions,\n} from './mode/local-cluster.ts';\nimport {\n\tresolveKnownDeploymentOptions,\n\ttype WalrusKnownDeploymentOptions,\n} from './mode/known-deploy.ts';\nimport { makeSnapshotable, type WalrusSnapshotMode } from './snapshot.ts';\nimport { makeLocalRoutables } from './routable.ts';\nimport { WALRUS_STATE_REGISTRY_KEY, type WalrusStateEntry } from './registry-publish.ts';\nimport { buildWalrusNetworkName, type WalrusStorageNode } from './storage-nodes.ts';\n\n// ---------------------------------------------------------------------------\n// Resource — the resolved value all consumers read\n// ---------------------------------------------------------------------------\n\n/** The Walrus resolved value carried by the resource. */\nexport interface WalrusResolved {\n\treadonly mode: 'local' | 'known';\n\t/** Network name the walrus deployment targets (`localnet`/`testnet`/…). */\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\t/** SDK-ready `packageConfig` — structurally compatible with\n\t * `@mysten/walrus`'s `WalrusPackageConfig`. */\n\treadonly packageConfig: {\n\t\treadonly systemObjectId: string;\n\t\treadonly stakingPoolId: string;\n\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t};\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly walFaucetStrategy: WalFaucetStrategy | null;\n\treadonly walCoinType: string | null;\n}\n\n/** Walrus plugin resource. */\nexport const walrusResource = resource<'walrus', WalrusResolved>('walrus');\n\nexport interface WalrusNetworkIdentity {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n}\n\n/** Walrus deploy records storage-node listening IPs under this /24.\n * Docker network create requests the matching subnet explicitly, with\n * the prefix derived from the Walrus network identity so parallel\n * stacks don't all claim the same Docker IPAM range. Walrus claims\n * the `10.64.*` – `10.127.*` band; see substrate/runtime/subnet-broker.ts\n * for the algorithm and band coordination with seal. */\nexport const deriveWalrusSubnetPrefix = (identity: WalrusNetworkIdentity): string =>\n\tderiveSubnetPrefix(`${identity.app}\\0${identity.stack}\\0${identity.walrusName}`, 64);\n\nconst withWalrusNetworkAddressing = (\n\truntime: ContainerRuntime,\n\twalrusNetworkName: string,\n\tsubnetPrefix: string,\n): ContainerRuntime => ({\n\t...runtime,\n\tensureNetwork: (spec) =>\n\t\truntime.ensureNetwork(\n\t\t\tspec.name === walrusNetworkName ? withSubnetAddressing(spec, subnetPrefix) : spec,\n\t\t),\n});\n\n// ---------------------------------------------------------------------------\n// Plugin construction (internal — used by walrus() + walrusFor())\n// ---------------------------------------------------------------------------\n\nconst buildLocalPlugin = (opts: WalrusLocalClusterOptions) => {\n\t// Synchronous factory-time validation (distilled-doc invariants\n\t// 11 — `nodeCount >= 1` + `shards >= nodeCount`).\n\tconst resolved = resolveLocalClusterOptions(opts);\n\n\tconst walrusKey = walrusPluginKey(resolved.name);\n\n\treturn definePlugin({\n\t\tid: walrusResource.id,\n\t\tdependsOn: [suiResource] as const,\n\t\trole: 'service',\n\t\tsection: 'service',\n\t\tpluginKey: walrusKey,\n\t\t// Stack-free codegen: a local walrus cluster's deploy ids / endpoint\n\t\t// URLs are LOADED CONFIG DATA -- the committed `walrus.ts` stub emits\n\t\t// `resolveValue('walrus', '<key>')`, never a baked object id / URL.\n\t\tstaticCodegen: () => [makeWalrusStaticCodegen({ mode: 'local', network: LOCAL_NETWORK_NAME })],\n\t\t// `deps` auto-infers the resolved `[sui]` tuple from the\n\t\t// `[suiResource] as const` dependency. `ctx` arrives via the\n\t\t// `PluginContext` service.\n\t\tstart: (deps) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst ctx = yield* PluginContext;\n\t\t\t\tconst [sui] = deps;\n\n\t\t\t\t// Substrate-context primitives:\n\t\t\t\t// - `ContainerRuntimeService` + `IdentityContext` arrive\n\t\t\t\t// via the supervisor's plugin runtime context.\n\t\t\t\t// - `ArtifactPublisher` is the substrate-level\n\t\t\t\t// publisher (cache → verify → produce → register cycle).\n\t\t\t\t// - `ChainProbe<SuiProbeKey>` is looked up via the\n\t\t\t\t// StrategyRegistry under `chain-probe:<chainId>`;\n\t\t\t\t// Sui registered itself there at its own acquire.\n\t\t\t\t// - `StackPathsService` resolves the per-stack on-disk\n\t\t\t\t// root so the deploy-output bind-mount source is a\n\t\t\t\t// real path (not the `<runtime>/...` template).\n\t\t\t\tconst runtime = yield* ContainerRuntimeService;\n\t\t\t\tconst identity = yield* IdentityContext;\n\t\t\t\tconst stackPaths = yield* StackPathsService;\n\t\t\t\tconst path = yield* Path.Path;\n\t\t\t\tconst publisher = yield* CacheService;\n\t\t\t\tconst probe = yield* chainProbeFor<SuiProbeKey>(sui.chainId);\n\n\t\t\t\t// Resolve the deploy-output bind-mount source from the\n\t\t\t\t// per-stack paths bundle. The deploy one-shot owns preparing\n\t\t\t\t// the directory immediately before its Docker bind mount.\n\t\t\t\tconst deployHostMountPath = path.join(\n\t\t\t\t\tstackPaths.stackRoot,\n\t\t\t\t\t'walrus',\n\t\t\t\t\tresolved.name,\n\t\t\t\t\t'deploy',\n\t\t\t\t);\n\n\t\t\t\t// Cross-container DNS: walrus containers (deploy one-shot\n\t\t\t\t// + N storage nodes) dial sui RPC + faucet via\n\t\t\t\t// `host.docker.internal`. The sui plugin binds brokered\n\t\t\t\t// host ports — no shared docker network needed.\n\t\t\t\t//\n\t\t\t\t// Architectural decision (B5): walrus owns its OWN docker\n\t\t\t\t// network for storage-node ↔ deploy connectivity; sui-side\n\t\t\t\t// hops go through the host gateway.\n\t\t\t\t// On Linux this requires Docker Desktop or the\n\t\t\t\t// `host.docker.internal:host-gateway` runtime hint (the\n\t\t\t\t// established devstack convention — see plugins/deepbook\n\t\t\t\t// which uses the same pattern).\n\t\t\t\tconst walrusNetworkName = buildWalrusNetworkName(\n\t\t\t\t\tidentity.app,\n\t\t\t\t\tidentity.stack,\n\t\t\t\t\tresolved.name,\n\t\t\t\t);\n\t\t\t\tconst walrusSubnetPrefix = deriveWalrusSubnetPrefix({\n\t\t\t\t\tapp: identity.app,\n\t\t\t\t\tstack: identity.stack,\n\t\t\t\t\twalrusName: resolved.name,\n\t\t\t\t});\n\t\t\t\tconst walrusRuntime = withWalrusNetworkAddressing(\n\t\t\t\t\truntime,\n\t\t\t\t\twalrusNetworkName,\n\t\t\t\t\twalrusSubnetPrefix,\n\t\t\t\t);\n\t\t\t\tconst suiRpcUrlInNetwork = sui.hostGateway.rpcUrl;\n\t\t\t\t// sui-faucet v2 endpoint — `/v2/gas` is the supported path\n\t\t\t\t// on devnet-v1.71.0+.\n\t\t\t\tconst suiFaucetUrlInNetwork = sui.hostGateway.faucetUrl;\n\t\t\t\tif (suiFaucetUrlInNetwork === null) {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t'deploy',\n\t\t\t\t\t\t\t'walrus local-cluster requires a Sui faucet URL for deploy funding.',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst walrusFaucetUrlInNetwork = `${suiFaucetUrlInNetwork}/v2/gas`;\n\n\t\t\t\tconst mode: WalrusMode = { mode: 'local', opts: resolved };\n\t\t\t\tconst boot = yield* bootWalrusService(\n\t\t\t\t\t{\n\t\t\t\t\t\truntime: walrusRuntime,\n\t\t\t\t\t\tpublisher,\n\t\t\t\t\t\tprobe,\n\t\t\t\t\t\tsuiSdk: sui.sdk,\n\t\t\t\t\t\tsuiChainId: sui.chainId,\n\t\t\t\t\t\tsuiRpcUrlInNetwork,\n\t\t\t\t\t\twalrusFaucetUrlInNetwork,\n\t\t\t\t\t\twaitForFundsReady: sui.waitForTransactionsReady.wait,\n\t\t\t\t\t\tapp: identity.app,\n\t\t\t\t\t\tstack: identity.stack,\n\t\t\t\t\t\tsubnetPrefix: walrusSubnetPrefix,\n\t\t\t\t\t\twalrusNetworkName,\n\t\t\t\t\t\t// Walrus has no `sui-net` to attach to (sui binds\n\t\t\t\t\t\t// host ports). Reuse the walrus network so the\n\t\t\t\t\t\t// deploy one-shot + storage nodes land on a single\n\t\t\t\t\t\t// network and reach sui via `host.docker.internal`.\n\t\t\t\t\t\tsuiNetworkName: walrusNetworkName,\n\t\t\t\t\t\tdeployHostMountPath,\n\t\t\t\t\t\tstackRoot: stackPaths.stackRoot,\n\t\t\t\t\t},\n\t\t\t\t\tmode,\n\t\t\t\t);\n\n\t\t\t\tif (boot.mode !== 'local') {\n\t\t\t\t\t// Should be unreachable — dispatch is by mode. Defense.\n\t\t\t\t\treturn yield* Effect.die('walrus: mode mismatch in local plugin');\n\t\t\t\t}\n\n\t\t\t\tconst resolvedValue: WalrusResolved = {\n\t\t\t\t\tmode: 'local',\n\t\t\t\t\tnetwork: identity.network,\n\t\t\t\t\twalrusPackageId: boot.walrusPackageId,\n\t\t\t\t\twalPackageId: boot.walPackageId,\n\t\t\t\t\tpackageConfig: {\n\t\t\t\t\t\tsystemObjectId: boot.deploy.systemObject,\n\t\t\t\t\t\tstakingPoolId: boot.deploy.stakingObject,\n\t\t\t\t\t\texchangeIds: boot.exchangeObjectId ? [boot.exchangeObjectId] : undefined,\n\t\t\t\t\t},\n\t\t\t\t\tnodes: boot.nodes,\n\t\t\t\t\tproxyUrl: boot.proxyUrl,\n\t\t\t\t\taggregatorUrl: boot.aggregatorUrl,\n\t\t\t\t\tpublisherUrl: boot.publisherUrl,\n\t\t\t\t\twalFaucetStrategy: boot.walFaucetStrategy,\n\t\t\t\t\twalCoinType: boot.walCoinType,\n\t\t\t\t};\n\t\t\t\t// Emit the resolved contributions inline: snapshot → codegen →\n\t\t\t\t// state-registry → (optional WAL faucet) → routables. `identity`\n\t\t\t\t// (from `IdentityContext`) stamps the snapshot + routable\n\t\t\t\t// container names. ID-stability surfaces (deploy/blob ids via\n\t\t\t\t// ArtifactPublisher) live OUTSIDE this emission and are untouched.\n\t\t\t\tconst walFaucetContribution: ReadonlyArray<StrategyContributorDecl> =\n\t\t\t\t\tresolvedValue.walFaucetStrategy === null || resolvedValue.walCoinType === null\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\tmakeWalFaucetContribution(\n\t\t\t\t\t\t\t\t\tresolvedValue.walFaucetStrategy,\n\t\t\t\t\t\t\t\t\tresolvedValue.walCoinType,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t];\n\t\t\t\temitContributions(ctx, [\n\t\t\t\t\tmakeSnapshotable(\n\t\t\t\t\t\t'local' satisfies WalrusSnapshotMode,\n\t\t\t\t\t\tidentity.app,\n\t\t\t\t\t\tidentity.stack,\n\t\t\t\t\t\tresolved.name,\n\t\t\t\t\t\tresolvedValue.network,\n\t\t\t\t\t\tresolved.nodeCount,\n\t\t\t\t\t),\n\t\t\t\t\tmakeCodegenable({\n\t\t\t\t\t\tmode: 'local',\n\t\t\t\t\t\tnetwork: resolvedValue.network,\n\t\t\t\t\t\twalrusPackageId: resolvedValue.walrusPackageId,\n\t\t\t\t\t\twalPackageId: resolvedValue.walPackageId,\n\t\t\t\t\t\twalCoinType: resolvedValue.walCoinType,\n\t\t\t\t\t\tsystemObjectId: resolvedValue.packageConfig.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolvedValue.packageConfig.stakingPoolId,\n\t\t\t\t\t\texchangeIds: resolvedValue.packageConfig.exchangeIds\n\t\t\t\t\t\t\t? [...resolvedValue.packageConfig.exchangeIds]\n\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\tproxyUrl: resolvedValue.proxyUrl,\n\t\t\t\t\t\taggregatorUrl: resolvedValue.aggregatorUrl,\n\t\t\t\t\t\tpublisherUrl: resolvedValue.publisherUrl,\n\t\t\t\t\t\tnodes: resolvedValue.nodes,\n\t\t\t\t\t}),\n\t\t\t\t\t{\n\t\t\t\t\t\tkind: 'strategy-contributor',\n\t\t\t\t\t\tcapabilityKey: WALRUS_STATE_REGISTRY_KEY,\n\t\t\t\t\t\tstrategy: { noteName: resolved.name },\n\t\t\t\t\t\tautoMounted: true,\n\t\t\t\t\t} satisfies StrategyContributorDecl<\n\t\t\t\t\t\ttypeof WALRUS_STATE_REGISTRY_KEY,\n\t\t\t\t\t\t{ readonly noteName: string }\n\t\t\t\t\t>,\n\t\t\t\t\t...walFaucetContribution,\n\t\t\t\t\t...(makeLocalRoutables({\n\t\t\t\t\t\tapp: identity.app,\n\t\t\t\t\t\tstack: identity.stack,\n\t\t\t\t\t\twalrusName: resolved.name,\n\t\t\t\t\t\tserviceKey: String(walrusKey),\n\t\t\t\t\t\tnodeCount: resolved.nodeCount,\n\t\t\t\t\t\tcontainerApiPort: resolved.containerApiPort,\n\t\t\t\t\t}) as readonly RoutableDecl[]),\n\t\t\t\t]);\n\t\t\t\treturn resolvedValue;\n\t\t\t}),\n\t});\n};\n\nconst buildKnownPlugin = (opts: WalrusKnownDeploymentOptions) => {\n\t// Synchronous factory-time validation for required deployment ids.\n\tconst resolved = resolveKnownDeploymentOptions(opts);\n\n\treturn definePlugin({\n\t\tid: walrusResource.id,\n\t\tdependsOn: [suiResource] as const,\n\t\t// Known deployment is a pure value-producer — no containers,\n\t\t// no long-running children.\n\t\trole: 'task',\n\t\tsection: 'service',\n\t\t// Stack-free codegen: a known deployment's ids / URLs are DECLARED\n\t\t// config (not loaded-at-runtime data) — bake them as literals in the\n\t\t// committed `walrus.ts` (mirrors `knownPackage`). `walrusPackageId` /\n\t\t// `walPackageId` / `walCoinType` are null for a known deployment.\n\t\tstaticCodegen: () => [\n\t\t\tmakeWalrusStaticCodegen({\n\t\t\t\tmode: 'known',\n\t\t\t\tnetwork: resolved.network,\n\t\t\t\tknown: {\n\t\t\t\t\twalrusPackageId: null,\n\t\t\t\t\twalPackageId: null,\n\t\t\t\t\twalCoinType: null,\n\t\t\t\t\tpackageConfig: {\n\t\t\t\t\t\tsystemObjectId: resolved.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolved.stakingPoolId,\n\t\t\t\t\t\t...(resolved.exchangeIds.length > 0 ? { exchangeIds: [...resolved.exchangeIds] } : {}),\n\t\t\t\t\t},\n\t\t\t\t\tproxyUrl: resolved.proxyUrl,\n\t\t\t\t\taggregatorUrl: resolved.aggregatorUrl,\n\t\t\t\t\tpublisherUrl: resolved.publisherUrl,\n\t\t\t\t\tnodes: resolved.nodes,\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t// Known mode is a pure value-producer — `sui` is unused (the value\n\t\t// reads `resolved.network` from the deployment options) but the\n\t\t// `dependsOn` edge still orders boot, so `start` is zero-arg.\n\t\t// `ctx` arrives via the `PluginContext` service.\n\t\tstart: () =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst ctx = yield* PluginContext;\n\t\t\t\tconst identity = yield* IdentityContext;\n\t\t\t\tconst resolvedValue = {\n\t\t\t\t\tmode: 'known',\n\t\t\t\t\tnetwork: resolved.network,\n\t\t\t\t\twalrusPackageId: null,\n\t\t\t\t\twalPackageId: null,\n\t\t\t\t\tpackageConfig: {\n\t\t\t\t\t\tsystemObjectId: resolved.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolved.stakingPoolId,\n\t\t\t\t\t\texchangeIds: resolved.exchangeIds.length > 0 ? resolved.exchangeIds : undefined,\n\t\t\t\t\t},\n\t\t\t\t\tnodes: resolved.nodes,\n\t\t\t\t\tproxyUrl: resolved.proxyUrl,\n\t\t\t\t\taggregatorUrl: resolved.aggregatorUrl,\n\t\t\t\t\tpublisherUrl: resolved.publisherUrl,\n\t\t\t\t\twalFaucetStrategy: null,\n\t\t\t\t\twalCoinType: null,\n\t\t\t\t} satisfies WalrusResolved;\n\t\t\t\t// Emit inline: snapshot → codegen → state-registry. Known mode\n\t\t\t\t// emits no routable. `identity` (from `IdentityContext`) stamps\n\t\t\t\t// the snapshot scoping.\n\t\t\t\tctx.snapshotExtra(\n\t\t\t\t\tmakeSnapshotable(\n\t\t\t\t\t\t'known' satisfies WalrusSnapshotMode,\n\t\t\t\t\t\tidentity.app,\n\t\t\t\t\t\tidentity.stack,\n\t\t\t\t\t\t'walrusKnownDeployment',\n\t\t\t\t\t\tresolvedValue.network,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tctx.codegen(\n\t\t\t\t\tmakeCodegenable({\n\t\t\t\t\t\tmode: 'known',\n\t\t\t\t\t\tnetwork: resolvedValue.network,\n\t\t\t\t\t\twalrusPackageId: resolvedValue.walrusPackageId,\n\t\t\t\t\t\twalPackageId: resolvedValue.walPackageId,\n\t\t\t\t\t\twalCoinType: resolvedValue.walCoinType,\n\t\t\t\t\t\tsystemObjectId: resolvedValue.packageConfig.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolvedValue.packageConfig.stakingPoolId,\n\t\t\t\t\t\texchangeIds: resolvedValue.packageConfig.exchangeIds\n\t\t\t\t\t\t\t? [...resolvedValue.packageConfig.exchangeIds]\n\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\tproxyUrl: resolvedValue.proxyUrl,\n\t\t\t\t\t\taggregatorUrl: resolvedValue.aggregatorUrl,\n\t\t\t\t\t\tpublisherUrl: resolvedValue.publisherUrl,\n\t\t\t\t\t\tnodes: resolvedValue.nodes,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tctx.provides({\n\t\t\t\t\tkind: 'strategy-contributor',\n\t\t\t\t\tcapabilityKey: WALRUS_STATE_REGISTRY_KEY,\n\t\t\t\t\tstrategy: {\n\t\t\t\t\t\tname: 'walrusKnownDeployment',\n\t\t\t\t\t\tsystemObjectId: resolvedValue.packageConfig.systemObjectId,\n\t\t\t\t\t\tstakingObjectId: resolvedValue.packageConfig.stakingPoolId,\n\t\t\t\t\t\tnetwork: resolvedValue.network,\n\t\t\t\t\t},\n\t\t\t\t\tautoMounted: true,\n\t\t\t\t} satisfies StrategyContributorDecl<typeof WALRUS_STATE_REGISTRY_KEY, WalrusStateEntry>);\n\t\t\t\treturn resolvedValue;\n\t\t\t}),\n\t});\n};\n\nexport interface WalCoinValue extends AccountFundingCoinValue {\n\treadonly symbol: 'WAL';\n\treadonly fullCoinType: `${string}::wal::WAL`;\n\treadonly decimals: 9;\n\treadonly source: 'walrus';\n}\n\n/** Resolve the local Walrus deployment's WAL coin as an account-funding\n * coin ref. Accounts that need WAL should use:\n *\n * funding: [{ coin: walCoin(localWalrus), amount }]\n *\n * The funding strategy itself is contributed by the Walrus service\n * once its local exchange exists. */\nexport const walCoin = (walrusMember: ResourceRef<'walrus', WalrusResolved>) => {\n\tconst coinRef = resource<CoinResourceId<'wal'>, WalCoinValue>(coinResourceId('wal'));\n\n\treturn definePlugin({\n\t\tid: coinRef.id,\n\t\tdependsOn: walrusMember,\n\t\trole: 'task',\n\t\tsection: 'action',\n\t\tstart: (resolved): Effect.Effect<WalCoinValue, WalrusPluginError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tif (resolved.walCoinType === null) {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t'exchange',\n\t\t\t\t\t\t\t'walCoin(...) requires a local Walrus deployment with a WAL package.',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tsymbol: 'WAL',\n\t\t\t\t\tfullCoinType: resolved.walCoinType as `${string}::wal::WAL`,\n\t\t\t\t\tdecimals: 9,\n\t\t\t\t\tsource: 'walrus',\n\t\t\t\t} satisfies WalCoinValue;\n\t\t\t}),\n\t});\n};\n\n// ---------------------------------------------------------------------------\n// User-facing factories\n// ---------------------------------------------------------------------------\n\n/** Local-cluster shorthand. Known deployments are selected through\n * `walrusFor(network).known(...)` so network choice stays explicit. */\nexport const walrus = (opts?: { readonly local?: WalrusLocalClusterOptions }) => {\n\treturn buildLocalPlugin(opts?.local ?? {});\n};\n\n/** Mode-narrowed factory namespace.\n *\n * Usage:\n * const network = { mode: 'local', network: 'localnet' } as const;\n * walrusFor(network).local({...}) // OK\n * walrusFor(network).known({...}) // type error: 'known' not in 'local' branch\n *\n * Critically, the fork branch exposes ONLY `.known` — calling\n * `.local` on a fork-mode network is a **compile error** at the\n * call site (distilled-doc invariant 12 — type-level refusal). */\nexport const walrusFor = defineModeNamespace({\n\tlocal: {\n\t\tlocal: (opts: WalrusLocalClusterOptions = {}) => buildLocalPlugin(opts),\n\t\tknown: (opts: WalrusKnownDeploymentOptions) => buildKnownPlugin(opts),\n\t},\n\tlive: {\n\t\tknown: (opts: WalrusKnownDeploymentOptions) => buildKnownPlugin(opts),\n\t},\n\tfork: {\n\t\t// `.local` is intentionally absent — calling\n\t\t// `walrusFor(forkNetwork).local(...)` is a compile error.\n\t\tknown: (opts: WalrusKnownDeploymentOptions) => buildKnownPlugin(opts),\n\t},\n});\n\n// ---------------------------------------------------------------------------\n// Re-exports for advanced callers\n// ---------------------------------------------------------------------------\n\nexport type { WalrusLocalClusterOptions } from './mode/local-cluster.ts';\nexport type { WalrusKnownDeploymentOptions, WalrusKnownNetwork } from './mode/known-deploy.ts';\nexport type { WalrusStorageNode } from './storage-nodes.ts';\nexport type { WalrusBindings, WalrusNodeBinding } from './codegen.ts';\nexport type { WalrusError, WalrusPluginError, WalrusConfigError, WalrusPhase } from './errors.ts';\nexport { walCoinType, walFaucetStrategyKey, type WalFaucetStrategy } from './faucet-strategy.ts';\nexport {\n\tWALRUS_STATE_REGISTRY_KEY,\n\ttype WalrusStateEntry,\n\ttype WalrusLocalStateEntry,\n\ttype WalrusKnownStateEntry,\n} from './registry-publish.ts';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/plugins/walrus/index.ts"],"sourcesContent":["// Walrus plugin — barrel + factories.\n//\n// Architecture: Walrus is a service plugin that owns a local cluster\n// or describes a known deployment. The factory at this file folds the\n// four modes behind:\n//\n// - `walrus(opts?)` — local-cluster shorthand. No env\n// defaulting; use `walrusFor(...)`\n// for known deployments.\n// - `walrusFor(network)` — mode-narrowed factory namespace\n// (architecture Tension 11). Returns\n// `{ local: …, known: … }` narrowed\n// to the network's mode. Crucially,\n// the fork branch exposes ONLY\n// `.known` — `.local` is a compile\n// error on a fork-mode network.\n//\n// During `start`, the plugin emits (via the typed `ctx.*` verbs, per mode):\n//\n// Local mode (full local cluster):\n// 1. `ctx.snapshotExtra` — runtime/walrus/<name>/deploy/ subtree\n// + storage-node managed containers.\n// 2. `ctx.codegen` — `walrus-network` bindings.\n// 3. `ctx.endpoint` × (N+2) — per-node + aggregator + publisher.\n// 4. `ctx.provides` walrus-state-registry — local entry.\n// 5. `ctx.provides` endpoint-registry — N+2 entries.\n// 6. `ctx.provides` package-registry — `walrus.<name>`.\n// 7. `ctx.provides` coinType:<WAL fullCoinType> — WAL faucet\n// strategy\n// (when exchange exists).\n//\n// Known mode (read-only deployment):\n// 1. `ctx.snapshotExtra` — identity-guard only; no subtrees.\n// 2. `ctx.codegen` — `walrus-network` bindings (mode='known').\n// 3. `ctx.provides` walrus-state-registry — known entry.\n//\n// Resource id: `'walrus'` (singular). The plugin's substrate-level\n// plugin key is the same string.\n\nimport { Effect, Path } from 'effect';\n\nimport { defineModeNamespace } from '../../api/mode-narrowed-factory.ts';\nimport { definePlugin, resource, type ResourceRef } from '../../api/define-plugin.ts';\nimport type { ContainerRuntime } from '../../contracts/container-runtime.ts';\nimport type { RoutableDecl } from '../../contracts/routable.ts';\nimport type { StrategyContributorDecl } from '../../contracts/strategy-contributor.ts';\nimport { emitContributions, PluginContext } from '../../substrate/plugin-ctx.ts';\nimport { ContainerRuntimeService } from '../../runtime/docker/service.ts';\nimport { IdentityContext, StackPathsService } from '../../substrate/runtime/paths.ts';\nimport { CacheService } from '../../substrate/runtime/cache/index.ts';\nimport { deriveSubnetPrefix, withSubnetAddressing } from '../../substrate/runtime/subnet-broker.ts';\nimport type { AccountFundingCoinValue } from '../account/index.ts';\nimport { coinResourceId, type CoinResourceId } from '../coin/index.ts';\nimport { suiResource, type SuiProbeKey } from '../sui/index.ts';\n\nimport { chainProbeFor } from '../../substrate/runtime/strategy-registry/index.ts';\n\nimport { makeCodegenable, makeWalrusStaticCodegen } from './codegen.ts';\nimport { LOCAL_NETWORK_NAME } from '../../api/inference-network.ts';\nimport { walrusPluginKey } from './plugin-key.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\nimport { makeWalFaucetContribution, type WalFaucetStrategy } from './faucet-strategy.ts';\nimport { bootWalrusService, type WalrusMode } from './service.ts';\nimport {\n\tresolveLocalClusterOptions,\n\ttype WalrusLocalClusterOptions,\n} from './mode/local-cluster.ts';\nimport {\n\tresolveKnownDeploymentOptions,\n\ttype WalrusKnownDeploymentOptions,\n} from './mode/known-deploy.ts';\nimport { makeSnapshotable, type WalrusSnapshotMode } from './snapshot.ts';\nimport { makeLocalRoutables } from './routable.ts';\nimport { WALRUS_STATE_REGISTRY_KEY, type WalrusStateEntry } from './registry-publish.ts';\nimport { buildWalrusNetworkName, type WalrusStorageNode } from './storage-nodes.ts';\n\n// ---------------------------------------------------------------------------\n// Resource — the resolved value all consumers read\n// ---------------------------------------------------------------------------\n\n/** The Walrus resolved value carried by the resource. */\nexport interface WalrusResolved {\n\treadonly mode: 'local' | 'known';\n\t/** Network name the walrus deployment targets (`localnet`/`testnet`/…). */\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\t/** SDK-ready `packageConfig` — structurally compatible with\n\t * `@mysten/walrus`'s `WalrusPackageConfig`. */\n\treadonly packageConfig: {\n\t\treadonly systemObjectId: string;\n\t\treadonly stakingPoolId: string;\n\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t};\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly walFaucetStrategy: WalFaucetStrategy | null;\n\treadonly walCoinType: string | null;\n}\n\n/** Walrus plugin resource. */\nexport const walrusResource = resource<'walrus', WalrusResolved>('walrus');\n\nexport interface WalrusNetworkIdentity {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n}\n\n/** Walrus deploy records storage-node listening IPs under this /24.\n * Docker network create requests the matching subnet explicitly, with\n * the prefix derived from the Walrus network identity so parallel\n * stacks don't all claim the same Docker IPAM range. Walrus claims\n * the `10.64.*` – `10.127.*` band; see substrate/runtime/subnet-broker.ts\n * for the algorithm and band coordination with seal. */\nexport const deriveWalrusSubnetPrefix = (identity: WalrusNetworkIdentity): string =>\n\tderiveSubnetPrefix(`${identity.app}\\0${identity.stack}\\0${identity.walrusName}`, 64);\n\nconst withWalrusNetworkAddressing = (\n\truntime: ContainerRuntime,\n\twalrusNetworkName: string,\n\tsubnetPrefix: string,\n): ContainerRuntime => ({\n\t...runtime,\n\tensureNetwork: (spec) =>\n\t\truntime.ensureNetwork(\n\t\t\tspec.name === walrusNetworkName ? withSubnetAddressing(spec, subnetPrefix) : spec,\n\t\t),\n});\n\n// ---------------------------------------------------------------------------\n// Plugin construction (internal — used by walrus() + walrusFor())\n// ---------------------------------------------------------------------------\n\nconst buildLocalPlugin = (opts: WalrusLocalClusterOptions) => {\n\t// Synchronous factory-time validation (distilled-doc invariants\n\t// 11 — `nodeCount >= 1` + `shards >= nodeCount`).\n\tconst resolved = resolveLocalClusterOptions(opts);\n\n\tconst walrusKey = walrusPluginKey(resolved.name);\n\n\treturn definePlugin({\n\t\tid: walrusResource.id,\n\t\tdependsOn: [suiResource] as const,\n\t\trole: 'service',\n\t\tsection: 'service',\n\t\tpluginKey: walrusKey,\n\t\t// Stack-free codegen: a local walrus cluster's deploy ids / endpoint\n\t\t// URLs are LOADED CONFIG DATA -- the committed `walrus.ts` stub emits\n\t\t// `requireValue(dep, 'walrus', '<key>')`, never a baked object id / URL.\n\t\tstaticCodegen: () => [makeWalrusStaticCodegen({ mode: 'local', network: LOCAL_NETWORK_NAME })],\n\t\t// `deps` auto-infers the resolved `[sui]` tuple from the\n\t\t// `[suiResource] as const` dependency. `ctx` arrives via the\n\t\t// `PluginContext` service.\n\t\tstart: (deps) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst ctx = yield* PluginContext;\n\t\t\t\tconst [sui] = deps;\n\n\t\t\t\t// Substrate-context primitives:\n\t\t\t\t// - `ContainerRuntimeService` + `IdentityContext` arrive\n\t\t\t\t// via the supervisor's plugin runtime context.\n\t\t\t\t// - `ArtifactPublisher` is the substrate-level\n\t\t\t\t// publisher (cache → verify → produce → register cycle).\n\t\t\t\t// - `ChainProbe<SuiProbeKey>` is looked up via the\n\t\t\t\t// StrategyRegistry under `chain-probe:<chainId>`;\n\t\t\t\t// Sui registered itself there at its own acquire.\n\t\t\t\t// - `StackPathsService` resolves the per-stack on-disk\n\t\t\t\t// root so the deploy-output bind-mount source is a\n\t\t\t\t// real path (not the `<runtime>/...` template).\n\t\t\t\tconst runtime = yield* ContainerRuntimeService;\n\t\t\t\tconst identity = yield* IdentityContext;\n\t\t\t\tconst stackPaths = yield* StackPathsService;\n\t\t\t\tconst path = yield* Path.Path;\n\t\t\t\tconst publisher = yield* CacheService;\n\t\t\t\tconst probe = yield* chainProbeFor<SuiProbeKey>(sui.chainId);\n\n\t\t\t\t// Resolve the deploy-output bind-mount source from the\n\t\t\t\t// per-stack paths bundle. The deploy one-shot owns preparing\n\t\t\t\t// the directory immediately before its Docker bind mount.\n\t\t\t\tconst deployHostMountPath = path.join(\n\t\t\t\t\tstackPaths.stackRoot,\n\t\t\t\t\t'walrus',\n\t\t\t\t\tresolved.name,\n\t\t\t\t\t'deploy',\n\t\t\t\t);\n\n\t\t\t\t// Cross-container DNS: walrus containers (deploy one-shot\n\t\t\t\t// + N storage nodes) dial sui RPC + faucet via\n\t\t\t\t// `host.docker.internal`. The sui plugin binds brokered\n\t\t\t\t// host ports — no shared docker network needed.\n\t\t\t\t//\n\t\t\t\t// Architectural decision (B5): walrus owns its OWN docker\n\t\t\t\t// network for storage-node ↔ deploy connectivity; sui-side\n\t\t\t\t// hops go through the host gateway.\n\t\t\t\t// On Linux this requires Docker Desktop or the\n\t\t\t\t// `host.docker.internal:host-gateway` runtime hint (the\n\t\t\t\t// established devstack convention — see plugins/deepbook\n\t\t\t\t// which uses the same pattern).\n\t\t\t\tconst walrusNetworkName = buildWalrusNetworkName(\n\t\t\t\t\tidentity.app,\n\t\t\t\t\tidentity.stack,\n\t\t\t\t\tresolved.name,\n\t\t\t\t);\n\t\t\t\tconst walrusSubnetPrefix = deriveWalrusSubnetPrefix({\n\t\t\t\t\tapp: identity.app,\n\t\t\t\t\tstack: identity.stack,\n\t\t\t\t\twalrusName: resolved.name,\n\t\t\t\t});\n\t\t\t\tconst walrusRuntime = withWalrusNetworkAddressing(\n\t\t\t\t\truntime,\n\t\t\t\t\twalrusNetworkName,\n\t\t\t\t\twalrusSubnetPrefix,\n\t\t\t\t);\n\t\t\t\tconst suiRpcUrlInNetwork = sui.hostGateway.rpcUrl;\n\t\t\t\t// sui-faucet v2 endpoint — `/v2/gas` is the supported path\n\t\t\t\t// on devnet-v1.71.0+.\n\t\t\t\tconst suiFaucetUrlInNetwork = sui.hostGateway.faucetUrl;\n\t\t\t\tif (suiFaucetUrlInNetwork === null) {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t'deploy',\n\t\t\t\t\t\t\t'walrus local-cluster requires a Sui faucet URL for deploy funding.',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst walrusFaucetUrlInNetwork = `${suiFaucetUrlInNetwork}/v2/gas`;\n\n\t\t\t\tconst mode: WalrusMode = { mode: 'local', opts: resolved };\n\t\t\t\tconst boot = yield* bootWalrusService(\n\t\t\t\t\t{\n\t\t\t\t\t\truntime: walrusRuntime,\n\t\t\t\t\t\tpublisher,\n\t\t\t\t\t\tprobe,\n\t\t\t\t\t\tsuiSdk: sui.sdk,\n\t\t\t\t\t\tsuiChainId: sui.chainId,\n\t\t\t\t\t\tsuiRpcUrlInNetwork,\n\t\t\t\t\t\twalrusFaucetUrlInNetwork,\n\t\t\t\t\t\twaitForFundsReady: sui.waitForTransactionsReady.wait,\n\t\t\t\t\t\tapp: identity.app,\n\t\t\t\t\t\tstack: identity.stack,\n\t\t\t\t\t\tsubnetPrefix: walrusSubnetPrefix,\n\t\t\t\t\t\twalrusNetworkName,\n\t\t\t\t\t\t// Walrus has no `sui-net` to attach to (sui binds\n\t\t\t\t\t\t// host ports). Reuse the walrus network so the\n\t\t\t\t\t\t// deploy one-shot + storage nodes land on a single\n\t\t\t\t\t\t// network and reach sui via `host.docker.internal`.\n\t\t\t\t\t\tsuiNetworkName: walrusNetworkName,\n\t\t\t\t\t\tdeployHostMountPath,\n\t\t\t\t\t\tstackRoot: stackPaths.stackRoot,\n\t\t\t\t\t},\n\t\t\t\t\tmode,\n\t\t\t\t);\n\n\t\t\t\tif (boot.mode !== 'local') {\n\t\t\t\t\t// Should be unreachable — dispatch is by mode. Defense.\n\t\t\t\t\treturn yield* Effect.die('walrus: mode mismatch in local plugin');\n\t\t\t\t}\n\n\t\t\t\tconst resolvedValue: WalrusResolved = {\n\t\t\t\t\tmode: 'local',\n\t\t\t\t\tnetwork: identity.network,\n\t\t\t\t\twalrusPackageId: boot.walrusPackageId,\n\t\t\t\t\twalPackageId: boot.walPackageId,\n\t\t\t\t\tpackageConfig: {\n\t\t\t\t\t\tsystemObjectId: boot.deploy.systemObject,\n\t\t\t\t\t\tstakingPoolId: boot.deploy.stakingObject,\n\t\t\t\t\t\texchangeIds: boot.exchangeObjectId ? [boot.exchangeObjectId] : undefined,\n\t\t\t\t\t},\n\t\t\t\t\tnodes: boot.nodes,\n\t\t\t\t\tproxyUrl: boot.proxyUrl,\n\t\t\t\t\taggregatorUrl: boot.aggregatorUrl,\n\t\t\t\t\tpublisherUrl: boot.publisherUrl,\n\t\t\t\t\twalFaucetStrategy: boot.walFaucetStrategy,\n\t\t\t\t\twalCoinType: boot.walCoinType,\n\t\t\t\t};\n\t\t\t\t// Emit the resolved contributions inline: snapshot → codegen →\n\t\t\t\t// state-registry → (optional WAL faucet) → routables. `identity`\n\t\t\t\t// (from `IdentityContext`) stamps the snapshot + routable\n\t\t\t\t// container names. ID-stability surfaces (deploy/blob ids via\n\t\t\t\t// ArtifactPublisher) live OUTSIDE this emission and are untouched.\n\t\t\t\tconst walFaucetContribution: ReadonlyArray<StrategyContributorDecl> =\n\t\t\t\t\tresolvedValue.walFaucetStrategy === null || resolvedValue.walCoinType === null\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\tmakeWalFaucetContribution(\n\t\t\t\t\t\t\t\t\tresolvedValue.walFaucetStrategy,\n\t\t\t\t\t\t\t\t\tresolvedValue.walCoinType,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t];\n\t\t\t\temitContributions(ctx, [\n\t\t\t\t\tmakeSnapshotable(\n\t\t\t\t\t\t'local' satisfies WalrusSnapshotMode,\n\t\t\t\t\t\tidentity.app,\n\t\t\t\t\t\tidentity.stack,\n\t\t\t\t\t\tresolved.name,\n\t\t\t\t\t\tresolvedValue.network,\n\t\t\t\t\t\tresolved.nodeCount,\n\t\t\t\t\t),\n\t\t\t\t\tmakeCodegenable({\n\t\t\t\t\t\tmode: 'local',\n\t\t\t\t\t\tnetwork: resolvedValue.network,\n\t\t\t\t\t\twalrusPackageId: resolvedValue.walrusPackageId,\n\t\t\t\t\t\twalPackageId: resolvedValue.walPackageId,\n\t\t\t\t\t\twalCoinType: resolvedValue.walCoinType,\n\t\t\t\t\t\tsystemObjectId: resolvedValue.packageConfig.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolvedValue.packageConfig.stakingPoolId,\n\t\t\t\t\t\texchangeIds: resolvedValue.packageConfig.exchangeIds\n\t\t\t\t\t\t\t? [...resolvedValue.packageConfig.exchangeIds]\n\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\tproxyUrl: resolvedValue.proxyUrl,\n\t\t\t\t\t\taggregatorUrl: resolvedValue.aggregatorUrl,\n\t\t\t\t\t\tpublisherUrl: resolvedValue.publisherUrl,\n\t\t\t\t\t\tnodes: resolvedValue.nodes,\n\t\t\t\t\t}),\n\t\t\t\t\t{\n\t\t\t\t\t\tkind: 'strategy-contributor',\n\t\t\t\t\t\tcapabilityKey: WALRUS_STATE_REGISTRY_KEY,\n\t\t\t\t\t\tstrategy: { noteName: resolved.name },\n\t\t\t\t\t\tautoMounted: true,\n\t\t\t\t\t} satisfies StrategyContributorDecl<\n\t\t\t\t\t\ttypeof WALRUS_STATE_REGISTRY_KEY,\n\t\t\t\t\t\t{ readonly noteName: string }\n\t\t\t\t\t>,\n\t\t\t\t\t...walFaucetContribution,\n\t\t\t\t\t...(makeLocalRoutables({\n\t\t\t\t\t\tapp: identity.app,\n\t\t\t\t\t\tstack: identity.stack,\n\t\t\t\t\t\twalrusName: resolved.name,\n\t\t\t\t\t\tserviceKey: String(walrusKey),\n\t\t\t\t\t\tnodeCount: resolved.nodeCount,\n\t\t\t\t\t\tcontainerApiPort: resolved.containerApiPort,\n\t\t\t\t\t}) as readonly RoutableDecl[]),\n\t\t\t\t]);\n\t\t\t\treturn resolvedValue;\n\t\t\t}),\n\t});\n};\n\nconst buildKnownPlugin = (opts: WalrusKnownDeploymentOptions) => {\n\t// Synchronous factory-time validation for required deployment ids.\n\tconst resolved = resolveKnownDeploymentOptions(opts);\n\n\treturn definePlugin({\n\t\tid: walrusResource.id,\n\t\tdependsOn: [suiResource] as const,\n\t\t// Known deployment is a pure value-producer — no containers,\n\t\t// no long-running children.\n\t\trole: 'task',\n\t\tsection: 'service',\n\t\t// Stack-free codegen: a known deployment's ids / URLs are DECLARED\n\t\t// config (not loaded-at-runtime data) — bake them as literals in the\n\t\t// committed `walrus.ts` (mirrors `knownPackage`). `walrusPackageId` /\n\t\t// `walPackageId` / `walCoinType` are null for a known deployment.\n\t\tstaticCodegen: () => [\n\t\t\tmakeWalrusStaticCodegen({\n\t\t\t\tmode: 'known',\n\t\t\t\tnetwork: resolved.network,\n\t\t\t\tknown: {\n\t\t\t\t\twalrusPackageId: null,\n\t\t\t\t\twalPackageId: null,\n\t\t\t\t\twalCoinType: null,\n\t\t\t\t\tpackageConfig: {\n\t\t\t\t\t\tsystemObjectId: resolved.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolved.stakingPoolId,\n\t\t\t\t\t\t...(resolved.exchangeIds.length > 0 ? { exchangeIds: [...resolved.exchangeIds] } : {}),\n\t\t\t\t\t},\n\t\t\t\t\tproxyUrl: resolved.proxyUrl,\n\t\t\t\t\taggregatorUrl: resolved.aggregatorUrl,\n\t\t\t\t\tpublisherUrl: resolved.publisherUrl,\n\t\t\t\t\tnodes: resolved.nodes,\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t\t// Known mode is a pure value-producer — `sui` is unused (the value\n\t\t// reads `resolved.network` from the deployment options) but the\n\t\t// `dependsOn` edge still orders boot, so `start` is zero-arg.\n\t\t// `ctx` arrives via the `PluginContext` service.\n\t\tstart: () =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst ctx = yield* PluginContext;\n\t\t\t\tconst identity = yield* IdentityContext;\n\t\t\t\tconst resolvedValue = {\n\t\t\t\t\tmode: 'known',\n\t\t\t\t\tnetwork: resolved.network,\n\t\t\t\t\twalrusPackageId: null,\n\t\t\t\t\twalPackageId: null,\n\t\t\t\t\tpackageConfig: {\n\t\t\t\t\t\tsystemObjectId: resolved.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolved.stakingPoolId,\n\t\t\t\t\t\texchangeIds: resolved.exchangeIds.length > 0 ? resolved.exchangeIds : undefined,\n\t\t\t\t\t},\n\t\t\t\t\tnodes: resolved.nodes,\n\t\t\t\t\tproxyUrl: resolved.proxyUrl,\n\t\t\t\t\taggregatorUrl: resolved.aggregatorUrl,\n\t\t\t\t\tpublisherUrl: resolved.publisherUrl,\n\t\t\t\t\twalFaucetStrategy: null,\n\t\t\t\t\twalCoinType: null,\n\t\t\t\t} satisfies WalrusResolved;\n\t\t\t\t// Emit inline: snapshot → codegen → state-registry. Known mode\n\t\t\t\t// emits no routable. `identity` (from `IdentityContext`) stamps\n\t\t\t\t// the snapshot scoping.\n\t\t\t\tctx.snapshotExtra(\n\t\t\t\t\tmakeSnapshotable(\n\t\t\t\t\t\t'known' satisfies WalrusSnapshotMode,\n\t\t\t\t\t\tidentity.app,\n\t\t\t\t\t\tidentity.stack,\n\t\t\t\t\t\t'walrusKnownDeployment',\n\t\t\t\t\t\tresolvedValue.network,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tctx.codegen(\n\t\t\t\t\tmakeCodegenable({\n\t\t\t\t\t\tmode: 'known',\n\t\t\t\t\t\tnetwork: resolvedValue.network,\n\t\t\t\t\t\twalrusPackageId: resolvedValue.walrusPackageId,\n\t\t\t\t\t\twalPackageId: resolvedValue.walPackageId,\n\t\t\t\t\t\twalCoinType: resolvedValue.walCoinType,\n\t\t\t\t\t\tsystemObjectId: resolvedValue.packageConfig.systemObjectId,\n\t\t\t\t\t\tstakingPoolId: resolvedValue.packageConfig.stakingPoolId,\n\t\t\t\t\t\texchangeIds: resolvedValue.packageConfig.exchangeIds\n\t\t\t\t\t\t\t? [...resolvedValue.packageConfig.exchangeIds]\n\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\tproxyUrl: resolvedValue.proxyUrl,\n\t\t\t\t\t\taggregatorUrl: resolvedValue.aggregatorUrl,\n\t\t\t\t\t\tpublisherUrl: resolvedValue.publisherUrl,\n\t\t\t\t\t\tnodes: resolvedValue.nodes,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tctx.provides({\n\t\t\t\t\tkind: 'strategy-contributor',\n\t\t\t\t\tcapabilityKey: WALRUS_STATE_REGISTRY_KEY,\n\t\t\t\t\tstrategy: {\n\t\t\t\t\t\tname: 'walrusKnownDeployment',\n\t\t\t\t\t\tsystemObjectId: resolvedValue.packageConfig.systemObjectId,\n\t\t\t\t\t\tstakingObjectId: resolvedValue.packageConfig.stakingPoolId,\n\t\t\t\t\t\tnetwork: resolvedValue.network,\n\t\t\t\t\t},\n\t\t\t\t\tautoMounted: true,\n\t\t\t\t} satisfies StrategyContributorDecl<typeof WALRUS_STATE_REGISTRY_KEY, WalrusStateEntry>);\n\t\t\t\treturn resolvedValue;\n\t\t\t}),\n\t});\n};\n\nexport interface WalCoinValue extends AccountFundingCoinValue {\n\treadonly symbol: 'WAL';\n\treadonly fullCoinType: `${string}::wal::WAL`;\n\treadonly decimals: 9;\n\treadonly source: 'walrus';\n}\n\n/** Resolve the local Walrus deployment's WAL coin as an account-funding\n * coin ref. Accounts that need WAL should use:\n *\n * funding: [{ coin: walCoin(localWalrus), amount }]\n *\n * The funding strategy itself is contributed by the Walrus service\n * once its local exchange exists. */\nexport const walCoin = (walrusMember: ResourceRef<'walrus', WalrusResolved>) => {\n\tconst coinRef = resource<CoinResourceId<'wal'>, WalCoinValue>(coinResourceId('wal'));\n\n\treturn definePlugin({\n\t\tid: coinRef.id,\n\t\tdependsOn: walrusMember,\n\t\trole: 'task',\n\t\tsection: 'action',\n\t\tstart: (resolved): Effect.Effect<WalCoinValue, WalrusPluginError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tif (resolved.walCoinType === null) {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t'exchange',\n\t\t\t\t\t\t\t'walCoin(...) requires a local Walrus deployment with a WAL package.',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tsymbol: 'WAL',\n\t\t\t\t\tfullCoinType: resolved.walCoinType as `${string}::wal::WAL`,\n\t\t\t\t\tdecimals: 9,\n\t\t\t\t\tsource: 'walrus',\n\t\t\t\t} satisfies WalCoinValue;\n\t\t\t}),\n\t});\n};\n\n// ---------------------------------------------------------------------------\n// User-facing factories\n// ---------------------------------------------------------------------------\n\n/** Local-cluster shorthand. Known deployments are selected through\n * `walrusFor(network).known(...)` so network choice stays explicit. */\nexport const walrus = (opts?: { readonly local?: WalrusLocalClusterOptions }) => {\n\treturn buildLocalPlugin(opts?.local ?? {});\n};\n\n/** Mode-narrowed factory namespace.\n *\n * Usage:\n * const network = { mode: 'local', network: 'localnet' } as const;\n * walrusFor(network).local({...}) // OK\n * walrusFor(network).known({...}) // type error: 'known' not in 'local' branch\n *\n * Critically, the fork branch exposes ONLY `.known` — calling\n * `.local` on a fork-mode network is a **compile error** at the\n * call site (distilled-doc invariant 12 — type-level refusal). */\nexport const walrusFor = defineModeNamespace({\n\tlocal: {\n\t\tlocal: (opts: WalrusLocalClusterOptions = {}) => buildLocalPlugin(opts),\n\t\tknown: (opts: WalrusKnownDeploymentOptions) => buildKnownPlugin(opts),\n\t},\n\tlive: {\n\t\tknown: (opts: WalrusKnownDeploymentOptions) => buildKnownPlugin(opts),\n\t},\n\tfork: {\n\t\t// `.local` is intentionally absent — calling\n\t\t// `walrusFor(forkNetwork).local(...)` is a compile error.\n\t\tknown: (opts: WalrusKnownDeploymentOptions) => buildKnownPlugin(opts),\n\t},\n});\n\n// ---------------------------------------------------------------------------\n// Re-exports for advanced callers\n// ---------------------------------------------------------------------------\n\nexport type { WalrusLocalClusterOptions } from './mode/local-cluster.ts';\nexport type { WalrusKnownDeploymentOptions, WalrusKnownNetwork } from './mode/known-deploy.ts';\nexport type { WalrusStorageNode } from './storage-nodes.ts';\nexport type { WalrusBindings, WalrusNodeBinding } from './codegen.ts';\nexport type { WalrusError, WalrusPluginError, WalrusConfigError, WalrusPhase } from './errors.ts';\nexport { walCoinType, walFaucetStrategyKey, type WalFaucetStrategy } from './faucet-strategy.ts';\nexport {\n\tWALRUS_STATE_REGISTRY_KEY,\n\ttype WalrusStateEntry,\n\ttype WalrusLocalStateEntry,\n\ttype WalrusKnownStateEntry,\n} from './registry-publish.ts';\n"],"mappings":"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"}
|
|
@@ -23,27 +23,23 @@ declare const ManifestExtrasLookupError_base: Schema.Class<ManifestExtrasLookupE
|
|
|
23
23
|
* classify via `catchTag('ManifestExtrasLookupError', ...)` rather
|
|
24
24
|
* than die-cause inspection. */
|
|
25
25
|
declare class ManifestExtrasLookupError extends ManifestExtrasLookupError_base {}
|
|
26
|
-
/** Codegen metadata recorded per stack. The supervisor writes the
|
|
27
|
-
*
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
26
|
+
/** Codegen metadata recorded per stack. The supervisor writes the live
|
|
27
|
+
* `deploymentFile` (the gitignored `deployment.json` injected as
|
|
28
|
+
* `__DEVSTACK_DEPLOYMENT__`) here at manifest-flush time so the read-side
|
|
29
|
+
* build integrations (the Vite plugin) consult the exact location the
|
|
30
|
+
* boot wrote — read and write share one decision. The dev-wallet
|
|
31
|
+
* connection + dev accounts ride the deployment envelope itself
|
|
32
|
+
* (`values['dev-wallet']` / `accounts`), so no separate dev tree is
|
|
33
|
+
* recorded. Bindings are NOT recorded either: the `@generated` alias
|
|
34
|
+
* always resolves to the committed `src/generated` tree written by the
|
|
35
|
+
* stack-free `codegen` verb. Optional + additive: manifests written
|
|
36
|
+
* before this field existed still decode; consumers fall back to their
|
|
37
|
+
* cold-start path. */
|
|
36
38
|
interface ManifestCodegen {
|
|
37
|
-
/** Absolute path to the
|
|
38
|
-
* for this stack (`<appRoot>/.devstack/stacks/<stack>/generated-extras`).
|
|
39
|
-
* The `@devstack-dev` Vite alias resolves here. Optional +
|
|
40
|
-
* additive — older manifests omit it; the reader falls back to
|
|
41
|
-
* `.devstack/stacks/<stack>/generated-extras`. */
|
|
42
|
-
readonly extrasDir?: string;
|
|
43
|
-
/** Absolute path to the gitignored `devstack-ids.json` the boot wrote
|
|
39
|
+
/** Absolute path to the gitignored `deployment.json` the boot wrote
|
|
44
40
|
* for this stack (the live on-chain ids). The Vite plugin reads it to
|
|
45
|
-
* inject `
|
|
46
|
-
readonly
|
|
41
|
+
* inject `__DEVSTACK_DEPLOYMENT__` in dev. Optional + additive. */
|
|
42
|
+
readonly deploymentFile?: string;
|
|
47
43
|
}
|
|
48
44
|
/** Manifest envelope. */
|
|
49
45
|
interface ManifestEnvelope {
|
|
@@ -98,10 +94,9 @@ declare const ManifestEnvelopeSchema: Schema.Struct<{
|
|
|
98
94
|
}>>;
|
|
99
95
|
readonly extras: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
100
96
|
readonly codegen: Schema.optional<Schema.Struct<{
|
|
101
|
-
|
|
102
|
-
/** Absolute path to the gitignored `devstack-ids.json` the boot
|
|
97
|
+
/** Absolute path to the gitignored `deployment.json` the boot
|
|
103
98
|
* wrote for this stack. The Vite plugin reads it in dev. */
|
|
104
|
-
readonly
|
|
99
|
+
readonly deploymentFile: Schema.optional<Schema.String>;
|
|
105
100
|
}>>;
|
|
106
101
|
}>;
|
|
107
102
|
//#endregion
|
|
@@ -54,12 +54,10 @@ const ManifestEnvelopeSchema = Schema.Struct({
|
|
|
54
54
|
endpointKey: Schema.String
|
|
55
55
|
})),
|
|
56
56
|
extras: Schema.Record(Schema.String, Schema.Unknown),
|
|
57
|
-
codegen: Schema.optional(Schema.Struct({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
idsFile: Schema.optional(Schema.String)
|
|
62
|
-
}))
|
|
57
|
+
codegen: Schema.optional(Schema.Struct({
|
|
58
|
+
/** Absolute path to the gitignored `deployment.json` the boot
|
|
59
|
+
* wrote for this stack. The Vite plugin reads it in dev. */
|
|
60
|
+
deploymentFile: Schema.optional(Schema.String) }))
|
|
63
61
|
});
|
|
64
62
|
//#endregion
|
|
65
63
|
export { ManifestEnvelopeSchema, ManifestExtrasInvalid, ManifestExtrasLookupError, resolveManifestExtras };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.mjs","names":[],"sources":["../../src/substrate/manifest.ts"],"sourcesContent":["// Manifest envelope schema.\n//\n// Architecture § Manifest data model: envelope shape is fixed at L0,\n// per-service projection is each plugin's responsibility (lives in\n// the Codegenable contribution). Build integrations read the\n// envelope; codegen output carries the typed per-service slice.\n//\n// L0 owns:\n// - the envelope (identity tuple, manifestVersion, endpoints lookup,\n// opaque extras),\n// - the endpoint-declaration shape (decl emitted by Routable; the\n// manifest writer at L3 walks them).\n\nimport { Effect, Schema } from 'effect';\n\n/** Tagged failure when a plugin's manifest-extras contribution does\n * not resolve to a plain record. STYLE_GUIDE §2.2 — substrate (L0)\n * errors use `Schema.TaggedErrorClass`; downstream classifiers\n * `catchTag('ManifestExtrasInvalid', ...)` instead of sniffing the\n * message. */\nexport class ManifestExtrasInvalid extends Schema.TaggedErrorClass<ManifestExtrasInvalid>()(\n\t'ManifestExtrasInvalid',\n\t{\n\t\tdetail: Schema.String,\n\t},\n) {}\n\n/** Tagged failure when a plugin's `extras` factory references a\n * resource the host cannot resolve — either the resource id is not\n * registered with the supervisor, or it has not produced a value\n * yet at the point the factory ran. Thrown synchronously from the\n * user-supplied `ctx.value(...)` closure; `resolveManifestExtras`\n * catches the throw and surfaces it as a typed failure so callers\n * classify via `catchTag('ManifestExtrasLookupError', ...)` rather\n * than die-cause inspection. */\nexport class ManifestExtrasLookupError extends Schema.TaggedErrorClass<ManifestExtrasLookupError>()(\n\t'ManifestExtrasLookupError',\n\t{\n\t\tkind: Schema.Literals(['unknown-resource', 'unresolved-resource']),\n\t\tresourceId: Schema.String,\n\t},\n) {}\n\n/** Codegen metadata recorded per stack. The supervisor writes the
|
|
1
|
+
{"version":3,"file":"manifest.mjs","names":[],"sources":["../../src/substrate/manifest.ts"],"sourcesContent":["// Manifest envelope schema.\n//\n// Architecture § Manifest data model: envelope shape is fixed at L0,\n// per-service projection is each plugin's responsibility (lives in\n// the Codegenable contribution). Build integrations read the\n// envelope; codegen output carries the typed per-service slice.\n//\n// L0 owns:\n// - the envelope (identity tuple, manifestVersion, endpoints lookup,\n// opaque extras),\n// - the endpoint-declaration shape (decl emitted by Routable; the\n// manifest writer at L3 walks them).\n\nimport { Effect, Schema } from 'effect';\n\n/** Tagged failure when a plugin's manifest-extras contribution does\n * not resolve to a plain record. STYLE_GUIDE §2.2 — substrate (L0)\n * errors use `Schema.TaggedErrorClass`; downstream classifiers\n * `catchTag('ManifestExtrasInvalid', ...)` instead of sniffing the\n * message. */\nexport class ManifestExtrasInvalid extends Schema.TaggedErrorClass<ManifestExtrasInvalid>()(\n\t'ManifestExtrasInvalid',\n\t{\n\t\tdetail: Schema.String,\n\t},\n) {}\n\n/** Tagged failure when a plugin's `extras` factory references a\n * resource the host cannot resolve — either the resource id is not\n * registered with the supervisor, or it has not produced a value\n * yet at the point the factory ran. Thrown synchronously from the\n * user-supplied `ctx.value(...)` closure; `resolveManifestExtras`\n * catches the throw and surfaces it as a typed failure so callers\n * classify via `catchTag('ManifestExtrasLookupError', ...)` rather\n * than die-cause inspection. */\nexport class ManifestExtrasLookupError extends Schema.TaggedErrorClass<ManifestExtrasLookupError>()(\n\t'ManifestExtrasLookupError',\n\t{\n\t\tkind: Schema.Literals(['unknown-resource', 'unresolved-resource']),\n\t\tresourceId: Schema.String,\n\t},\n) {}\n\n/** Codegen metadata recorded per stack. The supervisor writes the live\n * `deploymentFile` (the gitignored `deployment.json` injected as\n * `__DEVSTACK_DEPLOYMENT__`) here at manifest-flush time so the read-side\n * build integrations (the Vite plugin) consult the exact location the\n * boot wrote — read and write share one decision. The dev-wallet\n * connection + dev accounts ride the deployment envelope itself\n * (`values['dev-wallet']` / `accounts`), so no separate dev tree is\n * recorded. Bindings are NOT recorded either: the `@generated` alias\n * always resolves to the committed `src/generated` tree written by the\n * stack-free `codegen` verb. Optional + additive: manifests written\n * before this field existed still decode; consumers fall back to their\n * cold-start path. */\nexport interface ManifestCodegen {\n\t/** Absolute path to the gitignored `deployment.json` the boot wrote\n\t * for this stack (the live on-chain ids). The Vite plugin reads it to\n\t * inject `__DEVSTACK_DEPLOYMENT__` in dev. Optional + additive. */\n\treadonly deploymentFile?: string;\n}\n\n/** Manifest envelope. */\nexport interface ManifestEnvelope {\n\treadonly identity: {\n\t\treadonly app: string;\n\t\treadonly stack: string;\n\t\treadonly network: string;\n\t};\n\treadonly manifestVersion: number;\n\t/** Optional per-plugin service slot. The read-side build-integration API\n\t * surfaces `{}` when absent. */\n\treadonly services?: Readonly<Record<string, unknown>>;\n\treadonly endpoints: Readonly<Record<string, EndpointEntry>>;\n\treadonly extras: Readonly<Record<string, unknown>>;\n\t/** Per-stack codegen metadata. When absent, readers use `src/generated/`. */\n\treadonly codegen?: ManifestCodegen;\n}\n\nexport type ManifestExtras = Readonly<Record<string, unknown>>;\n\nexport interface ManifestExtrasContext {\n\treadonly value: (resource: { readonly id: string }) => unknown;\n}\n\nexport type ManifestExtrasInput =\n\t| ManifestExtras\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t| Effect.Effect<ManifestExtras, any, never>\n\t| ((\n\t\t\tctx: ManifestExtrasContext,\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t ) => ManifestExtras | Effect.Effect<ManifestExtras, any, never>);\n\nconst isRecord = (value: unknown): value is ManifestExtras =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nexport const resolveManifestExtras = (\n\tinput: ManifestExtrasInput | undefined,\n\tctx: ManifestExtrasContext,\n): Effect.Effect<ManifestExtras, ManifestExtrasInvalid | ManifestExtrasLookupError, never> =>\n\tEffect.gen(function* () {\n\t\tif (input === undefined) return {};\n\t\t// The user-supplied `extras` factory invokes `ctx.value(...)`\n\t\t// synchronously; a missing/unresolved resource throws a\n\t\t// `ManifestExtrasLookupError`. `Effect.try` with a typed\n\t\t// `catch` mapper promotes that throw into the typed failure\n\t\t// channel so callers `catchTag` it instead of having to inspect\n\t\t// the die-cause. Any non-tagged throw is rethrown to preserve\n\t\t// the existing defect semantics for genuine programmer errors\n\t\t// inside the factory body.\n\t\tconst resolvedEffect = Effect.isEffect(input)\n\t\t\t? input\n\t\t\t: typeof input === 'function'\n\t\t\t\t? Effect.try({\n\t\t\t\t\t\ttry: () => input(ctx),\n\t\t\t\t\t\tcatch: (cause) => {\n\t\t\t\t\t\t\tif (cause instanceof ManifestExtrasLookupError) return cause;\n\t\t\t\t\t\t\tthrow cause;\n\t\t\t\t\t\t},\n\t\t\t\t\t}).pipe(\n\t\t\t\t\t\tEffect.flatMap((value) => (Effect.isEffect(value) ? value : Effect.succeed(value))),\n\t\t\t\t\t)\n\t\t\t\t: Effect.succeed(input);\n\t\tconst resolved = yield* resolvedEffect;\n\t\tif (!isRecord(resolved)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew ManifestExtrasInvalid({\n\t\t\t\t\tdetail: 'manifest extras must resolve to a plain record',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn resolved;\n\t});\n\n/** Flat endpoint entry — the manifest's load-bearing surface for\n * build integrations. */\nexport interface EndpointEntry {\n\treadonly name: string;\n\treadonly url: string;\n\treadonly displayUrl: string | null;\n\treadonly wireProtocol: 'http' | 'h2c' | 'tcp';\n\treadonly pluginKey: string;\n\treadonly endpointKey: string;\n}\n\n/** Schema for runtime validation of an on-disk manifest. Build\n * integrations decode through this on read. */\nexport const ManifestEnvelopeSchema = Schema.Struct({\n\tidentity: Schema.Struct({\n\t\tapp: Schema.String,\n\t\tstack: Schema.String,\n\t\tnetwork: Schema.String,\n\t}),\n\tmanifestVersion: Schema.Number,\n\t// Optional service map for read-side integrations.\n\tservices: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n\tendpoints: Schema.Record(\n\t\tSchema.String,\n\t\tSchema.Struct({\n\t\t\tname: Schema.String,\n\t\t\turl: Schema.String,\n\t\t\tdisplayUrl: Schema.NullOr(Schema.String),\n\t\t\twireProtocol: Schema.Literals(['http', 'h2c', 'tcp']),\n\t\t\tpluginKey: Schema.String,\n\t\t\tendpointKey: Schema.String,\n\t\t}),\n\t),\n\textras: Schema.Record(Schema.String, Schema.Unknown),\n\t// Optional codegen metadata. The Vite plugin reads\n\t// `codegen.deploymentFile` to inject the live on-chain ids via\n\t// `__DEVSTACK_DEPLOYMENT__`; on a miss it injects `null`. Bindings are\n\t// not recorded here — `@generated` always resolves to the committed\n\t// `src/generated` tree.\n\tcodegen: Schema.optional(\n\t\tSchema.Struct({\n\t\t\t/** Absolute path to the gitignored `deployment.json` the boot\n\t\t\t * wrote for this stack. The Vite plugin reads it in dev. */\n\t\t\tdeploymentFile: Schema.optional(Schema.String),\n\t\t}),\n\t),\n});\n"],"mappings":";;;;;;;AAoBA,IAAa,wBAAb,cAA2C,OAAO,iBAAwC,CAAC,CAC1F,yBACA,EACC,QAAQ,OAAO,OAChB,CACD,CAAC,CAAC,CAAC;;;;;;;;;AAUH,IAAa,4BAAb,cAA+C,OAAO,iBAA4C,CAAC,CAClG,6BACA;CACC,MAAM,OAAO,SAAS,CAAC,oBAAoB,qBAAqB,CAAC;CACjE,YAAY,OAAO;AACpB,CACD,CAAC,CAAC,CAAC;AAqDH,MAAM,YAAY,UACjB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAEpE,MAAa,yBACZ,OACA,QAEA,OAAO,IAAI,aAAa;CACvB,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CAsBjC,MAAM,WAAW,OAbM,OAAO,SAAS,KAAK,IACzC,QACA,OAAO,UAAU,aAChB,OAAO,IAAI;EACX,WAAW,MAAM,GAAG;EACpB,QAAQ,UAAU;GACjB,IAAI,iBAAiB,2BAA2B,OAAO;GACvD,MAAM;EACP;CACD,CAAC,CAAC,CAAC,KACF,OAAO,SAAS,UAAW,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,QAAQ,KAAK,CAAE,CACnF,IACC,OAAO,QAAQ,KAAK;CAExB,IAAI,CAAC,SAAS,QAAQ,GACrB,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB,EACzB,QAAQ,iDACT,CAAC,CACF;CAED,OAAO;AACR,CAAC;;;AAeF,MAAa,yBAAyB,OAAO,OAAO;CACnD,UAAU,OAAO,OAAO;EACvB,KAAK,OAAO;EACZ,OAAO,OAAO;EACd,SAAS,OAAO;CACjB,CAAC;CACD,iBAAiB,OAAO;CAExB,UAAU,OAAO,SAAS,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,CAAC;CACtE,WAAW,OAAO,OACjB,OAAO,QACP,OAAO,OAAO;EACb,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,YAAY,OAAO,OAAO,OAAO,MAAM;EACvC,cAAc,OAAO,SAAS;GAAC;GAAQ;GAAO;EAAK,CAAC;EACpD,WAAW,OAAO;EAClB,aAAa,OAAO;CACrB,CAAC,CACF;CACA,QAAQ,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO;CAMnD,SAAS,OAAO,SACf,OAAO,OAAO;;;AAGb,gBAAgB,OAAO,SAAS,OAAO,MAAM,EAC9C,CAAC,CACF;AACD,CAAC"}
|
|
@@ -21,12 +21,12 @@ interface PluginCtx {
|
|
|
21
21
|
* so it cannot be a static spec field; that is exactly why it is a
|
|
22
22
|
* buffered verb). Since codegen was decoupled from boot this is the
|
|
23
23
|
* LIVE half ONLY: at boot the orchestrator projects the decl into the
|
|
24
|
-
* loadable
|
|
25
|
-
*
|
|
26
|
-
* the committed `src/generated` tree. That committed,
|
|
27
|
-
* emitted separately by the `devstack codegen` verb
|
|
28
|
-
* spec's `staticCodegen` hook, derived from the SAME
|
|
29
|
-
* Returns void (the orchestrator owns rendering). */
|
|
24
|
+
* loadable deployment (`assembleDeployment`) — folding both typed fields
|
|
25
|
+
* and the values-only channels (dev accounts, the dev-wallet connection)
|
|
26
|
+
* — and does NOT write the committed `src/generated` tree. That committed,
|
|
27
|
+
* stack-free tree is emitted separately by the `devstack codegen` verb
|
|
28
|
+
* from the plugin spec's `staticCodegen` hook, derived from the SAME
|
|
29
|
+
* `ConfigBindingSet`. Returns void (the orchestrator owns rendering). */
|
|
30
30
|
readonly codegen: <E extends string>(decl: CodegenableDecl<E>) => void;
|
|
31
31
|
/** Buffered: declare a routable endpoint for the router orchestrator.
|
|
32
32
|
* Replayed in the supervisor frame after a successful `start`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-ctx.mjs","names":[],"sources":["../../src/substrate/plugin-ctx.ts"],"sourcesContent":["// PluginCtx — the minimal, closed, typed plugin-authoring surface.\n//\n// A plugin contributes to the stack through inline typed verbs it calls\n// from `start`. The plugin reaches this `ctx` by `yield* PluginContext` (an Effect\n// service tag, declared at the bottom of this file) — exactly like any\n// other substrate service it `yield*`s (`ContainerRuntimeService`,\n// `IdentityContext`, …). The supervisor PROVIDES a freshly-built\n// per-plugin ctx into `start`'s requirement (R) channel via\n// `Effect.provideService(start(deps), PluginContext, ctx)`. Delivering\n// ctx through the requirement channel — rather than as a 2nd positional\n// `start` argument — keeps `start` SINGLE-arg, which restores automatic\n// contextual typing of `deps` (no per-plugin `deps:` annotations), while\n// `ctx` stays always-present (the supervisor never omits it).\n//\n// -----------------------------------------------------------------------------\n// INV-5 — the closed 5-key set\n// -----------------------------------------------------------------------------\n//\n// `PluginCtx` has EXACTLY these 5 readonly keys and no more:\n//\n// codegen · endpoint · snapshotExtra · publish · provides\n//\n// This is a HARD invariant (pinned by `plugin-ctx-keyset.test-d.ts`).\n// Growing the set re-builds a god-object surface. The rule for what may\n// live here:\n//\n// - `codegen` / `endpoint` / `snapshotExtra` / `publish` are the four\n// BUFFERED declarative verbs. Their backing services\n// (CodegenOrchestratorService, RouterService +\n// ManifestEndpointRegistryService, SnapshotOrchestratorService, the\n// projection sink) are SUPERVISOR-FRAME-ONLY — they are absent from\n// plugin scope. A verb call therefore cannot run its backing\n// service inline; it pushes a typed buffer entry that the supervisor\n// REPLAYS in its own frame after the plugin's `start` succeeds.\n// - `provides` is the strategy bus writer (the faucet pattern\n// generalized): a plugin contributes to a sibling's\n// capability-keyed registry without a dep-graph edge. Siblings READ\n// it with `yield* StrategyRegistryService` directly (no ctx verb).\n//\n// What stays OUT of `ctx`, and WHY: every other substrate service a\n// plugin needs is already in plugin scope and is reached with\n// `yield* Service` directly — `CacheService` (whose `.publish` is the\n// folded-in artifact-publisher cycle),\n// `ContainerRuntimeService`, `IdentityContext`,\n// `PortBrokerService`, `LeaseBrokerService`, etc. The infra\n// orchestrators that are NOT in plugin scope\n// (Snapshot/Codegen/Router/ManifestEndpoint) are precisely the four\n// buffered verbs above — and infra contributions stay `yield*`-shaped\n// declarations, NEVER imperative `ctx.tx`-style Sui leaks. Adding a\n// service to `ctx` that a plugin could already `yield*` is the drift\n// INV-5 forbids.\n\nimport { Context } from 'effect';\n\nimport type { CodegenableDecl } from '../contracts/codegenable.ts';\nimport type { ProjectionDecl } from '../contracts/projection.ts';\nimport type { RoutableDecl } from '../contracts/routable.ts';\nimport type { SnapshotableDecl } from '../contracts/snapshotable.ts';\nimport type { StrategyContributorDecl } from '../contracts/strategy-contributor.ts';\n\n/**\n * The CLOSED union of contribution decls a plugin emits via the four\n * buffered verbs + `provides`. Discriminated by `kind`. A plugin's `start`\n * emits these inline — either by direct `ctx.*` verb calls or, for a\n * conditional/variadic set, through the shared {@link emitContributions}\n * router below. The supervisor's static dispatch switches over exactly\n * these five kinds.\n */\nexport type Contribution =\n\t| CodegenableDecl<string>\n\t| RoutableDecl\n\t| SnapshotableDecl\n\t| ProjectionDecl\n\t| StrategyContributorDecl<string, unknown>;\n\n/**\n * The ONE shared contribution router. A plugin's `start` builds an ordered\n * (possibly conditional/variadic) list of {@link Contribution} decls and\n * feeds it here; this routes each decl to its matching `ctx` verb by its\n * `kind` discriminant, IN LIST ORDER:\n *\n * snapshotable → `ctx.snapshotExtra`\n * codegenable → `ctx.codegen`\n * routable → `ctx.endpoint`\n * projection → `ctx.publish`\n * strategy-contributor → `ctx.provides`\n *\n * This is the single exhaustive `switch` over the closed `Contribution`\n * union — replacing the per-plugin `build*`/`emit*` wrapper pairs that each\n * re-implemented this same routing. The supervisor mirrors this dispatch\n * when it REPLAYS the buffered decls (`acquire-node.ts`); the verbs simply\n * buffer here. For a fixed, unconditional set of decls a plugin may prefer\n * direct `ctx.*` calls — but any conditional or variadic emission should\n * route through this helper so the routing lives in exactly one place.\n *\n * Exhaustive: the `default` arm narrows `decl` to `never`, so adding a\n * sixth `Contribution` kind is a compile error here until handled.\n */\nexport const emitContributions = (ctx: PluginCtx, decls: ReadonlyArray<Contribution>): void => {\n\tfor (const decl of decls) {\n\t\tswitch (decl.kind) {\n\t\t\tcase 'snapshotable':\n\t\t\t\tctx.snapshotExtra(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'codegenable':\n\t\t\t\tctx.codegen(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'routable':\n\t\t\t\tctx.endpoint(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'projection':\n\t\t\t\tctx.publish(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'strategy-contributor':\n\t\t\t\tctx.provides(decl);\n\t\t\t\tbreak;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = decl;\n\t\t\t\tvoid _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n};\n\n/**\n * The minimal typed plugin-authoring context. A plugin reaches it from\n * `start` with `const ctx = yield* PluginContext` (the service tag\n * below); the supervisor provides a per-plugin instance into `start`'s\n * requirement channel.\n *\n * EXACTLY 5 keys (INV-5 — see file header). Do not add a 6th.\n */\nexport interface PluginCtx {\n\t/** Buffered: contribute the plugin's LIVE codegen decl to the codegen\n\t * orchestrator. Replayed in the supervisor frame after a successful\n\t * `start` (it carries post-acquire runtime state — resolved ids, rpc —\n\t * so it cannot be a static spec field; that is exactly why it is a\n\t * buffered verb). Since codegen was decoupled from boot this is the\n\t * LIVE half ONLY: at boot the orchestrator projects the decl into the\n\t * loadable
|
|
1
|
+
{"version":3,"file":"plugin-ctx.mjs","names":[],"sources":["../../src/substrate/plugin-ctx.ts"],"sourcesContent":["// PluginCtx — the minimal, closed, typed plugin-authoring surface.\n//\n// A plugin contributes to the stack through inline typed verbs it calls\n// from `start`. The plugin reaches this `ctx` by `yield* PluginContext` (an Effect\n// service tag, declared at the bottom of this file) — exactly like any\n// other substrate service it `yield*`s (`ContainerRuntimeService`,\n// `IdentityContext`, …). The supervisor PROVIDES a freshly-built\n// per-plugin ctx into `start`'s requirement (R) channel via\n// `Effect.provideService(start(deps), PluginContext, ctx)`. Delivering\n// ctx through the requirement channel — rather than as a 2nd positional\n// `start` argument — keeps `start` SINGLE-arg, which restores automatic\n// contextual typing of `deps` (no per-plugin `deps:` annotations), while\n// `ctx` stays always-present (the supervisor never omits it).\n//\n// -----------------------------------------------------------------------------\n// INV-5 — the closed 5-key set\n// -----------------------------------------------------------------------------\n//\n// `PluginCtx` has EXACTLY these 5 readonly keys and no more:\n//\n// codegen · endpoint · snapshotExtra · publish · provides\n//\n// This is a HARD invariant (pinned by `plugin-ctx-keyset.test-d.ts`).\n// Growing the set re-builds a god-object surface. The rule for what may\n// live here:\n//\n// - `codegen` / `endpoint` / `snapshotExtra` / `publish` are the four\n// BUFFERED declarative verbs. Their backing services\n// (CodegenOrchestratorService, RouterService +\n// ManifestEndpointRegistryService, SnapshotOrchestratorService, the\n// projection sink) are SUPERVISOR-FRAME-ONLY — they are absent from\n// plugin scope. A verb call therefore cannot run its backing\n// service inline; it pushes a typed buffer entry that the supervisor\n// REPLAYS in its own frame after the plugin's `start` succeeds.\n// - `provides` is the strategy bus writer (the faucet pattern\n// generalized): a plugin contributes to a sibling's\n// capability-keyed registry without a dep-graph edge. Siblings READ\n// it with `yield* StrategyRegistryService` directly (no ctx verb).\n//\n// What stays OUT of `ctx`, and WHY: every other substrate service a\n// plugin needs is already in plugin scope and is reached with\n// `yield* Service` directly — `CacheService` (whose `.publish` is the\n// folded-in artifact-publisher cycle),\n// `ContainerRuntimeService`, `IdentityContext`,\n// `PortBrokerService`, `LeaseBrokerService`, etc. The infra\n// orchestrators that are NOT in plugin scope\n// (Snapshot/Codegen/Router/ManifestEndpoint) are precisely the four\n// buffered verbs above — and infra contributions stay `yield*`-shaped\n// declarations, NEVER imperative `ctx.tx`-style Sui leaks. Adding a\n// service to `ctx` that a plugin could already `yield*` is the drift\n// INV-5 forbids.\n\nimport { Context } from 'effect';\n\nimport type { CodegenableDecl } from '../contracts/codegenable.ts';\nimport type { ProjectionDecl } from '../contracts/projection.ts';\nimport type { RoutableDecl } from '../contracts/routable.ts';\nimport type { SnapshotableDecl } from '../contracts/snapshotable.ts';\nimport type { StrategyContributorDecl } from '../contracts/strategy-contributor.ts';\n\n/**\n * The CLOSED union of contribution decls a plugin emits via the four\n * buffered verbs + `provides`. Discriminated by `kind`. A plugin's `start`\n * emits these inline — either by direct `ctx.*` verb calls or, for a\n * conditional/variadic set, through the shared {@link emitContributions}\n * router below. The supervisor's static dispatch switches over exactly\n * these five kinds.\n */\nexport type Contribution =\n\t| CodegenableDecl<string>\n\t| RoutableDecl\n\t| SnapshotableDecl\n\t| ProjectionDecl\n\t| StrategyContributorDecl<string, unknown>;\n\n/**\n * The ONE shared contribution router. A plugin's `start` builds an ordered\n * (possibly conditional/variadic) list of {@link Contribution} decls and\n * feeds it here; this routes each decl to its matching `ctx` verb by its\n * `kind` discriminant, IN LIST ORDER:\n *\n * snapshotable → `ctx.snapshotExtra`\n * codegenable → `ctx.codegen`\n * routable → `ctx.endpoint`\n * projection → `ctx.publish`\n * strategy-contributor → `ctx.provides`\n *\n * This is the single exhaustive `switch` over the closed `Contribution`\n * union — replacing the per-plugin `build*`/`emit*` wrapper pairs that each\n * re-implemented this same routing. The supervisor mirrors this dispatch\n * when it REPLAYS the buffered decls (`acquire-node.ts`); the verbs simply\n * buffer here. For a fixed, unconditional set of decls a plugin may prefer\n * direct `ctx.*` calls — but any conditional or variadic emission should\n * route through this helper so the routing lives in exactly one place.\n *\n * Exhaustive: the `default` arm narrows `decl` to `never`, so adding a\n * sixth `Contribution` kind is a compile error here until handled.\n */\nexport const emitContributions = (ctx: PluginCtx, decls: ReadonlyArray<Contribution>): void => {\n\tfor (const decl of decls) {\n\t\tswitch (decl.kind) {\n\t\t\tcase 'snapshotable':\n\t\t\t\tctx.snapshotExtra(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'codegenable':\n\t\t\t\tctx.codegen(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'routable':\n\t\t\t\tctx.endpoint(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'projection':\n\t\t\t\tctx.publish(decl);\n\t\t\t\tbreak;\n\t\t\tcase 'strategy-contributor':\n\t\t\t\tctx.provides(decl);\n\t\t\t\tbreak;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = decl;\n\t\t\t\tvoid _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n};\n\n/**\n * The minimal typed plugin-authoring context. A plugin reaches it from\n * `start` with `const ctx = yield* PluginContext` (the service tag\n * below); the supervisor provides a per-plugin instance into `start`'s\n * requirement channel.\n *\n * EXACTLY 5 keys (INV-5 — see file header). Do not add a 6th.\n */\nexport interface PluginCtx {\n\t/** Buffered: contribute the plugin's LIVE codegen decl to the codegen\n\t * orchestrator. Replayed in the supervisor frame after a successful\n\t * `start` (it carries post-acquire runtime state — resolved ids, rpc —\n\t * so it cannot be a static spec field; that is exactly why it is a\n\t * buffered verb). Since codegen was decoupled from boot this is the\n\t * LIVE half ONLY: at boot the orchestrator projects the decl into the\n\t * loadable deployment (`assembleDeployment`) — folding both typed fields\n\t * and the values-only channels (dev accounts, the dev-wallet connection)\n\t * — and does NOT write the committed `src/generated` tree. That committed,\n\t * stack-free tree is emitted separately by the `devstack codegen` verb\n\t * from the plugin spec's `staticCodegen` hook, derived from the SAME\n\t * `ConfigBindingSet`. Returns void (the orchestrator owns rendering). */\n\treadonly codegen: <E extends string>(decl: CodegenableDecl<E>) => void;\n\t/** Buffered: declare a routable endpoint for the router orchestrator.\n\t * Replayed in the supervisor frame after a successful `start`.\n\t * Returns VOID — no URL is echoed back to the plugin (the router\n\t * mints and publishes the endpoint event itself). */\n\treadonly endpoint: (decl: RoutableDecl) => void;\n\t/** Buffered: declare extra snapshot subtrees / managed containers /\n\t * identity-guard hooks for the snapshot orchestrator. Replayed in\n\t * the supervisor frame after a successful `start`. */\n\treadonly snapshotExtra: (decl: SnapshotableDecl) => void;\n\t/** Buffered: publish a name-blind projection event through the\n\t * projection sink. Replayed in the supervisor frame after a\n\t * successful `start`. */\n\treadonly publish: (decl: ProjectionDecl) => void;\n\t/** Strategy bus (write): contribute a strategy under a capability\n\t * key for a sibling to read via `StrategyRegistryService`. Registers\n\t * on the scope-local StrategyRegistry, publishes `strategy.registered`,\n\t * and arms a finalizer publishing `strategy.unregistered`. */\n\treadonly provides: <K extends string, S>(decl: StrategyContributorDecl<K, S>) => void;\n}\n\n/**\n * Effect service tag carrying the per-plugin {@link PluginCtx}.\n *\n * A plugin's `start` body reaches its ctx with `const ctx = yield*\n * PluginContext`. The supervisor builds a fresh per-plugin ctx (+ its\n * replay buffer) in `runtime/supervisor/acquire-node.ts` and PROVIDES it\n * into `start`'s requirement channel\n * (`Effect.provideService(start(deps), PluginContext, ctx)`), so the tag\n * is always satisfied for code the supervisor runs.\n *\n * This ambient requirement lives only in `start`'s R-channel; it does NOT\n * surface in the public `Plugin<Id, Value, Needs>` contract (`Needs` is\n * `dependsOn` only — see `plugin.ts`). Declaring it as a service rather\n * than a `start` argument is what keeps `start` single-arg, restoring\n * automatic `deps` inference.\n */\nexport class PluginContext extends Context.Service<PluginContext, PluginCtx>()(\n\t'@devstack/substrate/PluginContext',\n) {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkGA,MAAa,qBAAqB,KAAgB,UAA6C;CAC9F,KAAK,MAAM,QAAQ,OAClB,QAAQ,KAAK,MAAb;EACC,KAAK;GACJ,IAAI,cAAc,IAAI;GACtB;EACD,KAAK;GACJ,IAAI,QAAQ,IAAI;GAChB;EACD,KAAK;GACJ,IAAI,SAAS,IAAI;GACjB;EACD,KAAK;GACJ,IAAI,QAAQ,IAAI;GAChB;EACD,KAAK;GACJ,IAAI,SAAS,IAAI;GACjB;EACD;CAID;AAEF;;;;;;;;;;;;;;;;;AA4DA,IAAa,gBAAb,cAAmC,QAAQ,QAAkC,CAAC,CAC7E,mCACD,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"liveness.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/liveness.ts"],"sourcesContent":["// PID + start-time liveness check.\n//\n// Architecture § Cross-process safety protocol § Claim protocol step 3:\n// \"process exists AND its start-time (as read from `/proc/<pid>/stat` on\n// Linux, `ps -o lstart` on macOS, or equivalent) matches the recorded\n// `startTime`.\"\n//\n// Devstack is single-host: two checks combine. `kill(pid, 0)`\n// determines pid-in-use; `ps -o lstart` confirms it's the SAME process\n// (defending against pid reuse on long-uptime machines).\n//\n// This module is the ONLY place in the substrate that calls into\n// `process.kill` and shells out for start times. Roster/snapshot\n// reservation/stack-lock all consult it through the typed predicates.\n\nimport { execFileSync } from 'node:child_process';\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { RosterHolder } from '../../cross-process.ts';\nimport { selfPid } from './self-pid.ts';\n\n/** Cheap \"send signal 0\" pid liveness. ESRCH → dead; EPERM → alive\n * (foreign-user pid on shared dev hosts). Any other errno is\n * conservatively treated as dead so we don't refuse cleanup on exotic\n * platforms. */\nexport const isPidAlive = (pid: number): boolean => {\n\tif (!Number.isFinite(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn code === 'EPERM';\n\t}\n};\n\n/** Per-sweep cache mapping `pid → start-time stamp` (or `null` when the\n * pid is gone/unprobable). Callers that issue many probes in a single\n * sweep (roster step-3 stale-eviction, container-claim ledger prune,\n * etc.) instantiate one cache per pass and thread it through every\n * probe call so the same pid forks `ps`/`tasklist` AT MOST once per\n * sweep. Single-shot callers omit the cache and get the no-cache\n * behavior.\n *\n * Discriminator is `Map.has(pid)` — a cached `null` is a real result\n * (the pid is missing) and skipping it would re-fork pointlessly. */\nexport type LivenessCache = Map<number, number | null>;\n\n/** Best-effort start-time stamp for `pid`. Returns `null` when the\n * process is gone OR the platform can't produce a time. The stamp\n * itself is opaque text — its only contract is bytewise equality with\n * a previously-recorded sibling stamp.\n *\n * Architecture § Cross-process safety protocol §3 — `ps -o lstart` on\n * macOS/Linux, `tasklist` confirmation on Windows.\n *\n * Pass `cache` to reuse a previously-probed result across a sweep\n * pass — same `pid` only forks the underlying utility once. */\nexport const processStartTime = (pid: number, cache?: LivenessCache): number | null => {\n\tif (!Number.isFinite(pid) || pid <= 0) return null;\n\t// Our own pid's startTime never changes during the process\n\t// lifetime — cache the first SUCCESSFUL probe forever. Eliminates\n\t// the `ps` spawn under high in-process contention (e.g. N fibers\n\t// fighting over a single cross-process lock all probing each\n\t// other's shared pid). Without this cache, 8 concurrent fibers\n\t// each fork `ps -o lstart` with a 2s timeout, compounding into\n\t// seconds of latency that exhaust the claim budget (review fix\n\t// phase 22f reclaim-stress reproducer caught it).\n\t//\n\t// Only a non-null result is memoized: a `null` here means the\n\t// probe transiently FAILED (spawn hiccup / 2s timeout under load),\n\t// not that the pid is gone — it's our own pid, which is alive by\n\t// definition. Caching that null forever would surrender this\n\t// process's PID-reuse protection (it would write `startTime: null`\n\t// to disk for its whole lifetime) on a single flaky fork. Leaving\n\t// the cache UNSET re-probes on the next call; an occasional extra\n\t// `ps` on a previously-failing probe is negligible versus losing\n\t// reuse protection for the lifetime.\n\tif (pid === selfPid()) {\n\t\tif (ownStartTimeCache !== UNSET) return ownStartTimeCache;\n\t\tconst probed = probeStartTimeUncached(pid);\n\t\tif (probed !== null) ownStartTimeCache = probed;\n\t\treturn probed;\n\t}\n\tif (cache?.has(pid)) {\n\t\t// Map.get is `T | undefined` — but `has` is true, so the value\n\t\t// is one of the cached results (a `number` or `null`).\n\t\treturn cache.get(pid) ?? null;\n\t}\n\tconst probed = probeStartTimeUncached(pid);\n\tcache?.set(pid, probed);\n\treturn probed;\n};\n\nconst UNSET: unique symbol = Symbol('UNSET');\n// Only a successful (non-null) self-pid probe is ever stored here; a\n// failed probe leaves this UNSET so the next call re-probes.\nlet ownStartTimeCache: number | typeof UNSET = UNSET;\n\n/** Inner probe — always forks the platform utility. Split out so the\n * cache branch in `processStartTime` stays a single read/write. */\nconst probeStartTimeUncached = (pid: number): number | null => {\n\tif (process.platform === 'win32') {\n\t\ttry {\n\t\t\tconst out = execFileSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\ttimeout: 2000,\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t});\n\t\t\t// Windows path: just hash the line. Windows PID reuse on uptime\n\t\t\t// is a known v1 trade-off; the architecture says the protocol\n\t\t\t// stays POSIX-first.\n\t\t\treturn out.trim().startsWith('\"') ? hashStartTimeStamp(out.trim()) : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\ttry {\n\t\tconst out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 2000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? hashStartTimeStamp(trimmed) : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Stable hash of a start-time string. Reduces the textual `ps -o\n * lstart=` output to a single number so the roster carries a fixed-\n * width integer (the architecture's `startTime` field is `number`).\n *\n * FNV-1a 32-bit; collisions are negligible at the (pid, host, second)\n * granularity we ever compare. */\nconst hashStartTimeStamp = (stamp: string): number => {\n\tlet h = 2166136261;\n\tfor (let i = 0; i < stamp.length; i++) {\n\t\th ^= stamp.charCodeAt(i);\n\t\th = (h * 16777619) >>> 0;\n\t}\n\t// Stay inside Number.MAX_SAFE_INTEGER's positive 32-bit range so the\n\t// number round-trips through JSON without precision loss.\n\treturn h >>> 0;\n};\n\n/** Liveness probe for a roster holder. Used by the claim-protocol\n * sweep AND the stack-lock one-shot orphan check.\n *\n * Discipline (devstack is single-host): pid must be live AND start-time\n * must match the recorded stamp.\n *\n * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers\n * compose under spans. */\nexport const checkHolderLiveness = Effect.fn('cross-process.liveness.checkHolderLiveness')(\n\tfunction* (holder: RosterHolder, cache?: LivenessCache) {\n\t\tif (!isPidAlive(holder.pid)) return 'dead' as const;\n\t\tconst probedStart = processStartTime(holder.pid, cache);\n\t\t// pid alive but no stamp probable → conservative: ALIVE\n\t\t// (we have nothing to dispute the recorded startTime with).\n\t\tif (probedStart === null) return 'alive' as const;\n\t\t// Holder recorded a `null` startTime (writer's platform couldn't\n\t\t// probe at the time). The (pid, hostname) pair carries the\n\t\t// identity; same conservative policy as the probedStart-null\n\t\t// branch. Mismatching a real probed stamp against a recorded\n\t\t// `null` would otherwise harvest live holders as \"dead\".\n\t\tif (holder.startTime === null) return 'alive' as const;\n\t\treturn probedStart === holder.startTime ? ('alive' as const) : ('dead' as const);\n\t},\n);\n\n/** Build a holder snapshot for THIS process. The intent defaults to\n * `'normal'`; the snapshot bounce flips it to `'snapshot'`\n * under the stack lock and back when the bounce completes.\n *\n * A `null` `startTime` propagates verbatim — readers (`isOwnEntry`\n * in `roster.ts`, `checkHolderLiveness` above) honor the null-\n * conservative branch. Writing `0` for \"unprobable\" would cause a\n * false-dead harvest: a subsequent probe yielding a real stamp would\n * mismatch the recorded `0` and the process would fail to recognize\n * its own entry. */\nexport const ownHolder = (intent: 'normal' | 'snapshot' = 'normal'): RosterHolder => {\n\tconst pid = selfPid();\n\tconst startTime = processStartTime(pid);\n\treturn {\n\t\tpid,\n\t\tstartTime,\n\t\thostname: nodeHostname(),\n\t\tclaimedAt: Date.now(),\n\t\theartbeatAt: Date.now(),\n\t\tintent,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// LivenessProbeScope — per-sweep cache, lifted out of the Map parameter.\n// -----------------------------------------------------------------------------\n//\n// The bare `LivenessCache = Map<pid, stamp | null>` was threaded through\n// `roster.sweepStaleHolders` as an explicit parameter, which is fine for\n// one site but doesn't scale to the other sweep loops (dispatch routes,\n// doctor probes, lifecycle-prune). Promoting the cache into a service\n// lets a sweep loop `yield* LivenessProbeScope` once and call the probe\n// methods without re-passing the Map. The optional-Map params on\n// `processStartTime` / `checkHolderLiveness` stay so unmigrated callers\n// can keep working from outside an Effect.\n\n/** Methods a per-sweep scope exposes — the captured cache backs both\n * so the same `pid` forks `ps`/`tasklist` at most once per scope. */\nexport interface LivenessProbeScopeShape {\n\treadonly probeStartTime: (pid: number) => number | null;\n\treadonly probeHolderLiveness: (holder: RosterHolder) => Effect.Effect<'alive' | 'dead'>;\n}\n\n/** A per-sweep liveness scope. Callers `yield* LivenessProbeScope`\n * once per sweep (typically inside an `Effect.scoped` block that\n * wraps the loop) and call `probeStartTime` / `probeHolderLiveness`\n * on each holder without threading the Map manually. */\nexport class LivenessProbeScope extends Context.Service<\n\tLivenessProbeScope,\n\tLivenessProbeScopeShape\n>()('@devstack/substrate/cross-process/LivenessProbeScope') {}\n\n/** Construct a fresh `LivenessProbeScope` whose underlying cache is\n * private to this layer — every yielding scope gets its own Map. */\nexport const layerLivenessProbeScope: Layer.Layer<LivenessProbeScope> = Layer.effect(\n\tLivenessProbeScope,\n\tEffect.sync(() => {\n\t\tconst cache: LivenessCache = new Map();\n\t\treturn LivenessProbeScope.of({\n\t\t\tprobeStartTime: (pid) => processStartTime(pid, cache),\n\t\t\tprobeHolderLiveness: (holder) => checkHolderLiveness(holder, cache),\n\t\t});\n\t}),\n);\n"],"mappings":";;;;;;;;;AA2BA,MAAa,cAAc,QAAyB;CACnD,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG,OAAO;CAC9C,IAAI;EACH,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACR,SAAS,KAAK;EAEb,OADc,IAA8B,SAC5B;CACjB;AACD;;;;;;;;;;;AAwBA,MAAa,oBAAoB,KAAa,UAAyC;CACtF,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG,OAAO;CAmB9C,IAAI,QAAQ,QAAQ,GAAG;EACtB,IAAI,sBAAsB,OAAO,OAAO;EACxC,MAAM,SAAS,uBAAuB,GAAG;EACzC,IAAI,WAAW,MAAM,oBAAoB;EACzC,OAAO;CACR;CACA,IAAI,OAAO,IAAI,GAAG,GAGjB,OAAO,MAAM,IAAI,GAAG,KAAK;CAE1B,MAAM,SAAS,uBAAuB,GAAG;CACzC,OAAO,IAAI,KAAK,MAAM;CACtB,OAAO;AACR;AAEA,MAAM,QAAuB,OAAO,OAAO;AAG3C,IAAI,oBAA2C;;;AAI/C,MAAM,0BAA0B,QAA+B;CAC9D,IAAI,QAAQ,aAAa,SACxB,IAAI;EACH,MAAM,MAAM,aAAa,YAAY;GAAC;GAAO,UAAU;GAAO;GAAO;GAAO;EAAK,GAAG;GACnF,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CAAC;EAID,OAAO,IAAI,KAAK,CAAC,CAAC,WAAW,IAAG,IAAI,mBAAmB,IAAI,KAAK,CAAC,IAAI;CACtE,QAAQ;EACP,OAAO;CACR;CAED,IAAI;EAMH,MAAM,UALM,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,GAAG;EAAC,GAAG;GACpE,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CACkB,CAAC,CAAC,KAAK;EACzB,OAAO,QAAQ,SAAS,IAAI,mBAAmB,OAAO,IAAI;CAC3D,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;AAQA,MAAM,sBAAsB,UAA0B;CACrD,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACtC,KAAK,MAAM,WAAW,CAAC;EACvB,IAAK,IAAI,aAAc;CACxB;CAGA,OAAO,MAAM;AACd;;;;;;;;;AAUA,MAAa,sBAAsB,OAAO,GAAG,4CAA4C,CAAC,CACzF,WAAW,QAAsB,OAAuB;CACvD,IAAI,CAAC,WAAW,OAAO,GAAG,GAAG,OAAO;CACpC,MAAM,cAAc,iBAAiB,OAAO,KAAK,KAAK;CAGtD,IAAI,gBAAgB,MAAM,OAAO;CAMjC,IAAI,OAAO,cAAc,MAAM,OAAO;CACtC,OAAO,gBAAgB,OAAO,YAAa,UAAqB;AACjE,CACD;;;;;;;;;;;AAYA,MAAa,aAAa,SAAgC,aAA2B;CACpF,MAAM,MAAM,QAAQ;CAEpB,OAAO;EACN;EACA,WAHiB,iBAAiB,GAG1B;EACR,UAAUA,SAAa;EACvB,WAAW,KAAK,IAAI;EACpB,aAAa,KAAK,IAAI;EACtB;CACD;AACD;;;;;AA0BA,IAAa,qBAAb,cAAwC,QAAQ,QAG9C,CAAC,CAAC,sDAAsD,CAAC,CAAC,CAAC;;;AAI7D,MAAa,0BAA2D,MAAM,OAC7E,oBACA,OAAO,WAAW;CACjB,MAAM,wBAAuB,IAAI,IAAI;CACrC,OAAO,mBAAmB,GAAG;EAC5B,iBAAiB,QAAQ,iBAAiB,KAAK,KAAK;EACpD,sBAAsB,WAAW,oBAAoB,QAAQ,KAAK;CACnE,CAAC;AACF,CAAC,CACF"}
|
|
1
|
+
{"version":3,"file":"liveness.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/liveness.ts"],"sourcesContent":["// PID + start-time liveness check.\n//\n// Architecture § Cross-process safety protocol § Claim protocol step 3:\n// \"process exists AND its start-time (as read from `/proc/<pid>/stat` on\n// Linux, `ps -o lstart` on macOS, or equivalent) matches the recorded\n// `startTime`.\"\n//\n// Devstack is single-host: two checks combine. `kill(pid, 0)`\n// determines pid-in-use; `ps -o lstart` confirms it's the SAME process\n// (defending against pid reuse on long-uptime machines).\n//\n// This module is the ONLY place in the substrate that calls into\n// `process.kill` and shells out for start times. Roster/snapshot\n// reservation/stack-lock all consult it through the typed predicates.\n\nimport { execFileSync } from 'node:child_process';\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { RosterHolder } from '../../cross-process.ts';\nimport { selfPid } from './self-pid.ts';\n\n/** Cheap \"send signal 0\" pid liveness. ESRCH → dead; EPERM → alive\n * (foreign-user pid on shared dev hosts). Any other errno is\n * conservatively treated as dead so we don't refuse cleanup on exotic\n * platforms. */\nexport const isPidAlive = (pid: number): boolean => {\n\tif (!Number.isFinite(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn code === 'EPERM';\n\t}\n};\n\n/** Per-sweep cache mapping `pid → start-time stamp` (or `null` when the\n * pid is gone/unprobable). Callers that issue many probes in a single\n * sweep (roster step-3 stale-eviction, container-claim ledger prune,\n * etc.) instantiate one cache per pass and thread it through every\n * probe call so the same pid forks `ps`/`tasklist` AT MOST once per\n * sweep. Single-shot callers omit the cache and get the no-cache\n * behavior.\n *\n * Discriminator is `Map.has(pid)` — a cached `null` is a real result\n * (the pid is missing) and skipping it would re-fork pointlessly. */\nexport type LivenessCache = Map<number, number | null>;\n\n/** Best-effort start-time stamp for `pid`. Returns `null` when the\n * process is gone OR the platform can't produce a time. The stamp\n * itself is opaque text — its only contract is bytewise equality with\n * a previously-recorded sibling stamp.\n *\n * Architecture § Cross-process safety protocol §3 — `ps -o lstart` on\n * macOS/Linux, `tasklist` confirmation on Windows.\n *\n * Pass `cache` to reuse a previously-probed result across a sweep\n * pass — same `pid` only forks the underlying utility once. */\nexport const processStartTime = (pid: number, cache?: LivenessCache): number | null => {\n\tif (!Number.isFinite(pid) || pid <= 0) return null;\n\t// Our own pid's startTime never changes during the process\n\t// lifetime — cache the first SUCCESSFUL probe forever. Eliminates\n\t// the `ps` spawn under high in-process contention (e.g. N fibers\n\t// fighting over a single cross-process lock all probing each\n\t// other's shared pid). Without this cache, 8 concurrent fibers\n\t// each fork `ps -o lstart` with a 2s timeout, compounding into\n\t// seconds of latency that exhaust the claim budget (review fix\n\t// phase 22f reclaim-stress reproducer caught it).\n\t//\n\t// Only a non-null result is memoized: a `null` here means the\n\t// probe transiently FAILED (spawn hiccup / 2s timeout under load),\n\t// not that the pid is gone — it's our own pid, which is alive by\n\t// definition. Caching that null forever would surrender this\n\t// process's PID-reuse protection (it would write `startTime: null`\n\t// to disk for its whole lifetime) on a single flaky fork. Leaving\n\t// the cache UNSET re-probes on the next call; an occasional extra\n\t// `ps` on a previously-failing probe is negligible versus losing\n\t// reuse protection for the lifetime.\n\tif (pid === selfPid()) {\n\t\tif (ownStartTimeCache !== UNSET) return ownStartTimeCache;\n\t\tconst probed = probeStartTimeUncached(pid);\n\t\tif (probed !== null) ownStartTimeCache = probed;\n\t\treturn probed;\n\t}\n\tif (cache?.has(pid)) {\n\t\t// Map.get is `T | undefined` — but `has` is true, so the value\n\t\t// is one of the cached results (a `number` or `null`).\n\t\treturn cache.get(pid) ?? null;\n\t}\n\tconst probed = probeStartTimeUncached(pid);\n\tcache?.set(pid, probed);\n\treturn probed;\n};\n\nconst UNSET: unique symbol = Symbol('UNSET');\n// Only a successful (non-null) self-pid probe is ever stored here; a\n// failed probe leaves this UNSET so the next call re-probes.\nlet ownStartTimeCache: number | typeof UNSET = UNSET;\n\n/** Inner probe — always forks the platform utility. Split out so the\n * cache branch in `processStartTime` stays a single read/write. */\nconst probeStartTimeUncached = (pid: number): number | null => {\n\tif (process.platform === 'win32') {\n\t\ttry {\n\t\t\tconst out = execFileSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\ttimeout: 2000,\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t});\n\t\t\t// Windows path: just hash the line. Windows PID reuse on uptime\n\t\t\t// is a known v1 trade-off; the architecture says the protocol\n\t\t\t// stays POSIX-first.\n\t\t\treturn out.trim().startsWith('\"') ? hashStartTimeStamp(out.trim()) : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\ttry {\n\t\tconst out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 2000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? hashStartTimeStamp(trimmed) : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Stable hash of a start-time string. Reduces the textual `ps -o\n * lstart=` output to a single number so the roster carries a fixed-\n * width integer (the architecture's `startTime` field is `number`).\n *\n * FNV-1a 32-bit; collisions are negligible at the (pid, host, second)\n * granularity we ever compare. */\nconst hashStartTimeStamp = (stamp: string): number => {\n\tlet h = 2166136261;\n\tfor (let i = 0; i < stamp.length; i++) {\n\t\th ^= stamp.charCodeAt(i);\n\t\th = (h * 16777619) >>> 0;\n\t}\n\t// Stay inside Number.MAX_SAFE_INTEGER's positive 32-bit range so the\n\t// number round-trips through JSON without precision loss.\n\treturn h >>> 0;\n};\n\n/** Liveness probe for a roster holder. Used by the claim-protocol\n * sweep AND the stack-lock one-shot orphan check.\n *\n * Discipline (devstack is single-host): pid must be live AND start-time\n * must match the recorded stamp.\n *\n * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers\n * compose under spans. */\nexport const checkHolderLiveness = Effect.fn('cross-process.liveness.checkHolderLiveness')(\n\t// eslint-disable-next-line require-yield -- Effect.fn requires a generator; this probe is pure synchronous logic wrapped only for span composition, so it has no yield\n\tfunction* (holder: RosterHolder, cache?: LivenessCache) {\n\t\tif (!isPidAlive(holder.pid)) return 'dead' as const;\n\t\tconst probedStart = processStartTime(holder.pid, cache);\n\t\t// pid alive but no stamp probable → conservative: ALIVE\n\t\t// (we have nothing to dispute the recorded startTime with).\n\t\tif (probedStart === null) return 'alive' as const;\n\t\t// Holder recorded a `null` startTime (writer's platform couldn't\n\t\t// probe at the time). The (pid, hostname) pair carries the\n\t\t// identity; same conservative policy as the probedStart-null\n\t\t// branch. Mismatching a real probed stamp against a recorded\n\t\t// `null` would otherwise harvest live holders as \"dead\".\n\t\tif (holder.startTime === null) return 'alive' as const;\n\t\treturn probedStart === holder.startTime ? ('alive' as const) : ('dead' as const);\n\t},\n);\n\n/** Build a holder snapshot for THIS process. The intent defaults to\n * `'normal'`; the snapshot bounce flips it to `'snapshot'`\n * under the stack lock and back when the bounce completes.\n *\n * A `null` `startTime` propagates verbatim — readers (`isOwnEntry`\n * in `roster.ts`, `checkHolderLiveness` above) honor the null-\n * conservative branch. Writing `0` for \"unprobable\" would cause a\n * false-dead harvest: a subsequent probe yielding a real stamp would\n * mismatch the recorded `0` and the process would fail to recognize\n * its own entry. */\nexport const ownHolder = (intent: 'normal' | 'snapshot' = 'normal'): RosterHolder => {\n\tconst pid = selfPid();\n\tconst startTime = processStartTime(pid);\n\treturn {\n\t\tpid,\n\t\tstartTime,\n\t\thostname: nodeHostname(),\n\t\tclaimedAt: Date.now(),\n\t\theartbeatAt: Date.now(),\n\t\tintent,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// LivenessProbeScope — per-sweep cache, lifted out of the Map parameter.\n// -----------------------------------------------------------------------------\n//\n// The bare `LivenessCache = Map<pid, stamp | null>` was threaded through\n// `roster.sweepStaleHolders` as an explicit parameter, which is fine for\n// one site but doesn't scale to the other sweep loops (dispatch routes,\n// doctor probes, lifecycle-prune). Promoting the cache into a service\n// lets a sweep loop `yield* LivenessProbeScope` once and call the probe\n// methods without re-passing the Map. The optional-Map params on\n// `processStartTime` / `checkHolderLiveness` stay so unmigrated callers\n// can keep working from outside an Effect.\n\n/** Methods a per-sweep scope exposes — the captured cache backs both\n * so the same `pid` forks `ps`/`tasklist` at most once per scope. */\nexport interface LivenessProbeScopeShape {\n\treadonly probeStartTime: (pid: number) => number | null;\n\treadonly probeHolderLiveness: (holder: RosterHolder) => Effect.Effect<'alive' | 'dead'>;\n}\n\n/** A per-sweep liveness scope. Callers `yield* LivenessProbeScope`\n * once per sweep (typically inside an `Effect.scoped` block that\n * wraps the loop) and call `probeStartTime` / `probeHolderLiveness`\n * on each holder without threading the Map manually. */\nexport class LivenessProbeScope extends Context.Service<\n\tLivenessProbeScope,\n\tLivenessProbeScopeShape\n>()('@devstack/substrate/cross-process/LivenessProbeScope') {}\n\n/** Construct a fresh `LivenessProbeScope` whose underlying cache is\n * private to this layer — every yielding scope gets its own Map. */\nexport const layerLivenessProbeScope: Layer.Layer<LivenessProbeScope> = Layer.effect(\n\tLivenessProbeScope,\n\tEffect.sync(() => {\n\t\tconst cache: LivenessCache = new Map();\n\t\treturn LivenessProbeScope.of({\n\t\t\tprobeStartTime: (pid) => processStartTime(pid, cache),\n\t\t\tprobeHolderLiveness: (holder) => checkHolderLiveness(holder, cache),\n\t\t});\n\t}),\n);\n"],"mappings":";;;;;;;;;AA2BA,MAAa,cAAc,QAAyB;CACnD,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG,OAAO;CAC9C,IAAI;EACH,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACR,SAAS,KAAK;EAEb,OADc,IAA8B,SAC5B;CACjB;AACD;;;;;;;;;;;AAwBA,MAAa,oBAAoB,KAAa,UAAyC;CACtF,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG,OAAO;CAmB9C,IAAI,QAAQ,QAAQ,GAAG;EACtB,IAAI,sBAAsB,OAAO,OAAO;EACxC,MAAM,SAAS,uBAAuB,GAAG;EACzC,IAAI,WAAW,MAAM,oBAAoB;EACzC,OAAO;CACR;CACA,IAAI,OAAO,IAAI,GAAG,GAGjB,OAAO,MAAM,IAAI,GAAG,KAAK;CAE1B,MAAM,SAAS,uBAAuB,GAAG;CACzC,OAAO,IAAI,KAAK,MAAM;CACtB,OAAO;AACR;AAEA,MAAM,QAAuB,OAAO,OAAO;AAG3C,IAAI,oBAA2C;;;AAI/C,MAAM,0BAA0B,QAA+B;CAC9D,IAAI,QAAQ,aAAa,SACxB,IAAI;EACH,MAAM,MAAM,aAAa,YAAY;GAAC;GAAO,UAAU;GAAO;GAAO;GAAO;EAAK,GAAG;GACnF,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CAAC;EAID,OAAO,IAAI,KAAK,CAAC,CAAC,WAAW,IAAG,IAAI,mBAAmB,IAAI,KAAK,CAAC,IAAI;CACtE,QAAQ;EACP,OAAO;CACR;CAED,IAAI;EAMH,MAAM,UALM,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,GAAG;EAAC,GAAG;GACpE,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CACkB,CAAC,CAAC,KAAK;EACzB,OAAO,QAAQ,SAAS,IAAI,mBAAmB,OAAO,IAAI;CAC3D,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;AAQA,MAAM,sBAAsB,UAA0B;CACrD,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACtC,KAAK,MAAM,WAAW,CAAC;EACvB,IAAK,IAAI,aAAc;CACxB;CAGA,OAAO,MAAM;AACd;;;;;;;;;AAUA,MAAa,sBAAsB,OAAO,GAAG,4CAA4C,CAAC,CAEzF,WAAW,QAAsB,OAAuB;CACvD,IAAI,CAAC,WAAW,OAAO,GAAG,GAAG,OAAO;CACpC,MAAM,cAAc,iBAAiB,OAAO,KAAK,KAAK;CAGtD,IAAI,gBAAgB,MAAM,OAAO;CAMjC,IAAI,OAAO,cAAc,MAAM,OAAO;CACtC,OAAO,gBAAgB,OAAO,YAAa,UAAqB;AACjE,CACD;;;;;;;;;;;AAYA,MAAa,aAAa,SAAgC,aAA2B;CACpF,MAAM,MAAM,QAAQ;CAEpB,OAAO;EACN;EACA,WAHiB,iBAAiB,GAG1B;EACR,UAAUA,SAAa;EACvB,WAAW,KAAK,IAAI;EACpB,aAAa,KAAK,IAAI;EACtB;CACD;AACD;;;;;AA0BA,IAAa,qBAAb,cAAwC,QAAQ,QAG9C,CAAC,CAAC,sDAAsD,CAAC,CAAC,CAAC;;;AAI7D,MAAa,0BAA2D,MAAM,OAC7E,oBACA,OAAO,WAAW;CACjB,MAAM,wBAAuB,IAAI,IAAI;CACrC,OAAO,mBAAmB,GAAG;EAC5B,iBAAiB,QAAQ,iBAAiB,KAAK,KAAK;EACpD,sBAAsB,WAAW,oBAAoB,QAAQ,KAAK;CACnE,CAAC;AACF,CAAC,CACF"}
|
|
@@ -3,6 +3,7 @@ import "./cross-process/command-channel/channel.mjs";
|
|
|
3
3
|
import "./cross-process/command-channel/index.mjs";
|
|
4
4
|
import "./cross-process/roster.mjs";
|
|
5
5
|
import "./supervisor/contribution-dispatcher.mjs";
|
|
6
|
+
import "./lifecycle/file-watcher.mjs";
|
|
6
7
|
import "./observability/logger.mjs";
|
|
7
8
|
import "./supervisor/start-supervisor.mjs";
|
|
8
9
|
import "./supervisor/index.mjs";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { Effect, Scope } from "effect";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { deriveWatchRoots } from "./watch-attribution.mjs";
|
|
2
|
+
import { Effect, Queue } from "effect";
|
|
3
|
+
import { watch } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/substrate/runtime/lifecycle/file-watcher.ts
|
|
6
|
+
/** Coalescing window for a burst of fs events. Editors save by
|
|
7
|
+
* rename-replace (temp write + rename), so a single save surfaces as
|
|
8
|
+
* several events within a few ms — one settle window collapses them
|
|
9
|
+
* into one restart per plugin. */
|
|
10
|
+
const DEFAULT_DEBOUNCE_MILLIS = 150;
|
|
11
|
+
/**
|
|
12
|
+
* Start the L0 file watcher. Acquires one recursive `fs.watch` per
|
|
13
|
+
* distinct root (released on scope close) and forks a debouncing drain
|
|
14
|
+
* loop that fires `notifyWatchFire` for each distinct changed path.
|
|
15
|
+
* Returns once the watchers + loop are installed; it lives for the
|
|
16
|
+
* lifetime of the surrounding scope, so callers `Effect.forkScoped` it
|
|
17
|
+
* (or just `yield*` it inside a scoped region).
|
|
18
|
+
*
|
|
19
|
+
* Watcher setup never fails the boot: a root that cannot be watched
|
|
20
|
+
* (e.g. a not-yet-created directory) is logged and skipped.
|
|
21
|
+
*/
|
|
22
|
+
const startFileWatcher = (params) => Effect.gen(function* () {
|
|
23
|
+
const roots = deriveWatchRoots(params.watchIndex);
|
|
24
|
+
if (roots.length === 0) return;
|
|
25
|
+
const debounceMillis = params.debounceMillis ?? DEFAULT_DEBOUNCE_MILLIS;
|
|
26
|
+
const events = yield* Queue.dropping(1024);
|
|
27
|
+
const supervisorContext = yield* Effect.context();
|
|
28
|
+
const runForkInherited = Effect.runForkWith(supervisorContext);
|
|
29
|
+
for (const root of roots) if ((yield* Effect.acquireRelease(Effect.sync(() => {
|
|
30
|
+
try {
|
|
31
|
+
const fsWatcher = watch(root, { recursive: true }, (_eventType, filename) => {
|
|
32
|
+
if (filename === null) return;
|
|
33
|
+
const abs = join(root, filename.toString());
|
|
34
|
+
runForkInherited(Queue.offer(events, abs));
|
|
35
|
+
});
|
|
36
|
+
fsWatcher.on("error", () => {});
|
|
37
|
+
return fsWatcher;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}), (watcher) => Effect.sync(() => watcher?.close()))) === null) yield* Effect.logWarning(`file-watcher: could not watch ${root}`);
|
|
42
|
+
const drain = Effect.gen(function* () {
|
|
43
|
+
const first = yield* Queue.take(events);
|
|
44
|
+
yield* Effect.sleep(`${debounceMillis} millis`);
|
|
45
|
+
const rest = yield* Queue.takeAll(events);
|
|
46
|
+
const paths = new Set([first, ...rest]);
|
|
47
|
+
for (const path of paths) yield* params.notifyWatchFire(path);
|
|
48
|
+
}).pipe(Effect.forever);
|
|
49
|
+
yield* Effect.forkScoped(drain);
|
|
50
|
+
});
|
|
51
|
+
//#endregion
|
|
52
|
+
export { startFileWatcher };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=file-watcher.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-watcher.mjs","names":["fsWatch"],"sources":["../../../../src/substrate/runtime/lifecycle/file-watcher.ts"],"sourcesContent":["// L0 file watcher — the \"thick watcher\" the lifecycle architecture\n// describes (§ L3 Watch dispatcher: \"receives watcher events from L0\n// which already debounced + dedup'd\").\n//\n// Generic: it watches the literal roots derived from the supervisor's\n// watch index — the union of every plugin's declared `watch` paths,\n// whatever they are — and feeds each changed path to `notifyWatchFire`,\n// which attributes it to the owning plugin(s) and issues a selective\n// restart (→ re-acquire → re-cache-key → cache-miss → re-produce). The\n// package plugin's Move sources are today's only declarer, but nothing\n// here is Move-specific. Without this, the watch index +\n// `notifyWatchFire` are built but never driven, so a `watch`-declaring\n// plugin never restarts on a source edit.\n//\n// Dev-only: wired from the `up` verb (see `cli/wirings/up.ts`). `apply`\n// and the snapshot verbs boot once and exit, so they never start it.\n//\n// Dependency-free by design — devstack pulls in no glob/watch library\n// (cf. the hand-rolled `collectHashedSources`). Node's recursive\n// `fs.watch` is supported on macOS and Windows, and on Linux from\n// Node 20; one watcher is placed per distinct root.\n\nimport { type FSWatcher, watch as fsWatch } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { Effect, Queue, Scope } from 'effect';\n\nimport { deriveWatchRoots, type WatchEntry } from './watch-attribution.ts';\n\n/** Coalescing window for a burst of fs events. Editors save by\n * rename-replace (temp write + rename), so a single save surfaces as\n * several events within a few ms — one settle window collapses them\n * into one restart per plugin. */\nconst DEFAULT_DEBOUNCE_MILLIS = 150;\n\nexport interface FileWatcherParams {\n\treadonly watchIndex: ReadonlyArray<WatchEntry>;\n\treadonly notifyWatchFire: (path: string) => Effect.Effect<void>;\n\treadonly debounceMillis?: number;\n}\n\n/**\n * Start the L0 file watcher. Acquires one recursive `fs.watch` per\n * distinct root (released on scope close) and forks a debouncing drain\n * loop that fires `notifyWatchFire` for each distinct changed path.\n * Returns once the watchers + loop are installed; it lives for the\n * lifetime of the surrounding scope, so callers `Effect.forkScoped` it\n * (or just `yield*` it inside a scoped region).\n *\n * Watcher setup never fails the boot: a root that cannot be watched\n * (e.g. a not-yet-created directory) is logged and skipped.\n */\nexport const startFileWatcher = (\n\tparams: FileWatcherParams,\n): Effect.Effect<void, never, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tconst roots = deriveWatchRoots(params.watchIndex);\n\t\tif (roots.length === 0) return;\n\n\t\tconst debounceMillis = params.debounceMillis ?? DEFAULT_DEBOUNCE_MILLIS;\n\t\t// Bounded so a pathological event storm (e.g. a `git checkout` touching\n\t\t// thousands of files) can't grow the queue unboundedly; offers into a\n\t\t// dropping queue never suspend, so the fs.watch callback stays cheap.\n\t\t// 1024 comfortably covers a normal multi-file save burst, whose\n\t\t// duplicates the drain loop collapses within the settle window into one\n\t\t// restart per plugin. If a burst ever exceeds the bound the overflow is\n\t\t// dropped — at worst a reload is missed until the next edit re-fires the\n\t\t// path (and dev reload stays manually recoverable).\n\t\tconst events = yield* Queue.dropping<string>(1024);\n\t\t// Capture the supervisor Context so the offer fork inherits its\n\t\t// logger Layer / fiber-refs (mirrors `installSignalHandler`); a bare\n\t\t// `Effect.runFork` would evaluate against an empty context.\n\t\tconst supervisorContext = yield* Effect.context<never>();\n\t\tconst runForkInherited = Effect.runForkWith(supervisorContext);\n\n\t\tfor (const root of roots) {\n\t\t\tconst watcher = yield* Effect.acquireRelease(\n\t\t\t\tEffect.sync((): FSWatcher | null => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst fsWatcher = fsWatch(root, { recursive: true }, (_eventType, filename) => {\n\t\t\t\t\t\t\tif (filename === null) return;\n\t\t\t\t\t\t\tconst abs = join(root, filename.toString());\n\t\t\t\t\t\t\t// Bridge the Node callback into the queue via the captured\n\t\t\t\t\t\t\t// Context. Dropping queue ⇒ offer never suspends.\n\t\t\t\t\t\t\trunForkInherited(Queue.offer(events, abs));\n\t\t\t\t\t\t});\n\t\t\t\t\t\t// A watcher-level error (e.g. the root was removed) must not\n\t\t\t\t\t\t// crash the supervisor; swallow it. Scope close still calls\n\t\t\t\t\t\t// `close()` on the finalizer below.\n\t\t\t\t\t\tfsWatcher.on('error', () => {});\n\t\t\t\t\t\treturn fsWatcher;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t\t(watcher) => Effect.sync(() => watcher?.close()),\n\t\t\t);\n\t\t\tif (watcher === null) {\n\t\t\t\tyield* Effect.logWarning(`file-watcher: could not watch ${root}`);\n\t\t\t}\n\t\t}\n\n\t\t// Drain loop: block for one change, settle, then drain + dedup the\n\t\t// burst and fire each distinct path once. `notifyWatchFire` itself\n\t\t// dedups owning plugins, so editing several files in one save yields\n\t\t// one restart per affected plugin.\n\t\tconst drain = Effect.gen(function* () {\n\t\t\tconst first = yield* Queue.take(events);\n\t\t\tyield* Effect.sleep(`${debounceMillis} millis`);\n\t\t\tconst rest = yield* Queue.takeAll(events);\n\t\t\tconst paths = new Set<string>([first, ...rest]);\n\t\t\tfor (const path of paths) {\n\t\t\t\tyield* params.notifyWatchFire(path);\n\t\t\t}\n\t\t}).pipe(Effect.forever);\n\n\t\tyield* Effect.forkScoped(drain);\n\t});\n"],"mappings":";;;;;;;;;AAiCA,MAAM,0BAA0B;;;;;;;;;;;;AAmBhC,MAAa,oBACZ,WAEA,OAAO,IAAI,aAAa;CACvB,MAAM,QAAQ,iBAAiB,OAAO,UAAU;CAChD,IAAI,MAAM,WAAW,GAAG;CAExB,MAAM,iBAAiB,OAAO,kBAAkB;CAShD,MAAM,SAAS,OAAO,MAAM,SAAiB,IAAI;CAIjD,MAAM,oBAAoB,OAAO,OAAO,QAAe;CACvD,MAAM,mBAAmB,OAAO,YAAY,iBAAiB;CAE7D,KAAK,MAAM,QAAQ,OAsBlB,KAAI,OArBmB,OAAO,eAC7B,OAAO,WAA6B;EACnC,IAAI;GACH,MAAM,YAAYA,MAAQ,MAAM,EAAE,WAAW,KAAK,IAAI,YAAY,aAAa;IAC9E,IAAI,aAAa,MAAM;IACvB,MAAM,MAAM,KAAK,MAAM,SAAS,SAAS,CAAC;IAG1C,iBAAiB,MAAM,MAAM,QAAQ,GAAG,CAAC;GAC1C,CAAC;GAID,UAAU,GAAG,eAAe,CAAC,CAAC;GAC9B,OAAO;EACR,QAAQ;GACP,OAAO;EACR;CACD,CAAC,IACA,YAAY,OAAO,WAAW,SAAS,MAAM,CAAC,CAChD,OACgB,MACf,OAAO,OAAO,WAAW,iCAAiC,MAAM;CAQlE,MAAM,QAAQ,OAAO,IAAI,aAAa;EACrC,MAAM,QAAQ,OAAO,MAAM,KAAK,MAAM;EACtC,OAAO,OAAO,MAAM,GAAG,eAAe,QAAQ;EAC9C,MAAM,OAAO,OAAO,MAAM,QAAQ,MAAM;EACxC,MAAM,QAAQ,IAAI,IAAY,CAAC,OAAO,GAAG,IAAI,CAAC;EAC9C,KAAK,MAAM,QAAQ,OAClB,OAAO,OAAO,gBAAgB,IAAI;CAEpC,CAAC,CAAC,CAAC,KAAK,OAAO,OAAO;CAEtB,OAAO,OAAO,WAAW,KAAK;AAC/B,CAAC"}
|
|
@@ -14,13 +14,83 @@ const buildWatchIndex = (nodes) => {
|
|
|
14
14
|
return out;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Given the firing `firedPath` (already debounced + content-hash dedup'd
|
|
18
|
+
* by L0), return the plugin keys whose declared paths match.
|
|
19
|
+
*
|
|
20
|
+
* Matching is glob-aware via minimatch semantics — the substrate-level
|
|
21
|
+
* attribution here delegates to a supplied `match` predicate to keep
|
|
22
|
+
* this module dependency-free. The supervisor wires the matcher in.
|
|
21
23
|
*/
|
|
22
|
-
const
|
|
24
|
+
const attribute = (index, firedPath, match) => {
|
|
25
|
+
const out = /* @__PURE__ */ new Set();
|
|
26
|
+
for (const entry of index) for (const pattern of entry.paths) if (match(pattern, firedPath)) {
|
|
27
|
+
out.add(entry.pluginKey);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
const REGEXP_SPECIALS = new Set(".+^${}()|[]\\".split(""));
|
|
33
|
+
/**
|
|
34
|
+
* Compile a watch glob to an anchored `RegExp`. Supports the only glob
|
|
35
|
+
* features the package plugin emits (architecture § WatchDecl):
|
|
36
|
+
* - `**\/` — zero or more path segments (so `root/**\/*.move` matches both
|
|
37
|
+
* `root/x.move` and `root/sources/x.move`)
|
|
38
|
+
* - `**` — any characters including `/` (trailing-segment form)
|
|
39
|
+
* - `*` — any characters except `/` (within one segment)
|
|
40
|
+
* - `?` — one character except `/`
|
|
41
|
+
* Everything else is matched literally. This is the dependency-free stand-in
|
|
42
|
+
* for the architecture's "minimatch" matcher — devstack pulls in no glob
|
|
43
|
+
* library (cf. the hand-rolled `collectHashedSources`).
|
|
44
|
+
*/
|
|
45
|
+
const globToRegExp = (glob) => {
|
|
46
|
+
let body = "";
|
|
47
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
48
|
+
const c = glob[i];
|
|
49
|
+
if (c === "*" && glob[i + 1] === "*") if (glob[i + 2] === "/") {
|
|
50
|
+
body += "(?:.*/)?";
|
|
51
|
+
i += 2;
|
|
52
|
+
} else {
|
|
53
|
+
body += ".*";
|
|
54
|
+
i += 1;
|
|
55
|
+
}
|
|
56
|
+
else if (c === "*") body += "[^/]*";
|
|
57
|
+
else if (c === "?") body += "[^/]";
|
|
58
|
+
else if (REGEXP_SPECIALS.has(c)) body += `\\${c}`;
|
|
59
|
+
else body += c;
|
|
60
|
+
}
|
|
61
|
+
return new RegExp(`^${body}$`);
|
|
62
|
+
};
|
|
63
|
+
/** Glob-aware matcher — the production matcher the supervisor wires into
|
|
64
|
+
* `attribute`. Honors `**`, `*`, `?`; everything else is literal. Both
|
|
65
|
+
* sides are normalized to POSIX separators first: the watcher feeds paths
|
|
66
|
+
* from `join(root, filename)`, which is `\`-separated on Windows, while
|
|
67
|
+
* glob patterns are always `/`-separated. */
|
|
68
|
+
const toPosix = (p) => p.replace(/\\/g, "/");
|
|
69
|
+
const globMatch = (pattern, path) => globToRegExp(toPosix(pattern)).test(toPosix(path));
|
|
70
|
+
/** Literal directory prefix of a watch glob — the path up to (but not
|
|
71
|
+
* including) the first segment containing a glob metacharacter. Both
|
|
72
|
+
* `root/**\/*.move` and the exact `root/Move.toml` collapse to `root`,
|
|
73
|
+
* so the L0 watcher places ONE recursive watch per source tree. A
|
|
74
|
+
* pattern whose literal prefix has no directory separator — a bare
|
|
75
|
+
* basename (`Move.toml`), a leading glob (`*.move` ⇒ empty prefix), or a
|
|
76
|
+
* same-segment glob (`src*`) — collapses to `.` (the cwd), never to a
|
|
77
|
+
* bare filename: `fs.watch` must be handed a directory, and `globMatch`
|
|
78
|
+
* still filters the events. */
|
|
79
|
+
const literalRoot = (pattern) => {
|
|
80
|
+
const metaIdx = pattern.search(/[*?]/);
|
|
81
|
+
const literal = metaIdx === -1 ? pattern : pattern.slice(0, metaIdx);
|
|
82
|
+
const slash = literal.lastIndexOf("/");
|
|
83
|
+
if (slash === 0) return "/";
|
|
84
|
+
return slash > 0 ? literal.slice(0, slash) : ".";
|
|
85
|
+
};
|
|
86
|
+
/** Distinct recursive-watch roots for a watch index — the directories the
|
|
87
|
+
* L0 file watcher hands to `fs.watch(root, { recursive: true })`. */
|
|
88
|
+
const deriveWatchRoots = (index) => {
|
|
89
|
+
const roots = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const entry of index) for (const pattern of entry.paths) roots.add(literalRoot(pattern));
|
|
91
|
+
return [...roots];
|
|
92
|
+
};
|
|
23
93
|
//#endregion
|
|
24
|
-
export { buildWatchIndex,
|
|
94
|
+
export { attribute, buildWatchIndex, deriveWatchRoots, globMatch };
|
|
25
95
|
|
|
26
96
|
//# sourceMappingURL=watch-attribution.mjs.map
|