@mysten-incubation/devstack 0.5.0 → 0.7.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/dashboard-ui/assets/{grpc-CpkDu4SA.js → grpc-s7Ztk9wv.js} +1 -1
- package/dashboard-ui/assets/{index-jLPRmjst.js → index-DrOd0m4F.js} +2 -2
- package/dashboard-ui/index.html +1 -1
- package/dist/contracts/routable.d.mts +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/orchestrators/boot.mjs +2 -1
- package/dist/orchestrators/boot.mjs.map +1 -1
- package/dist/orchestrators/router/file-provider.mjs +11 -8
- package/dist/orchestrators/router/file-provider.mjs.map +1 -1
- package/dist/orchestrators/router/service.mjs +2 -2
- package/dist/orchestrators/router/service.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +10 -10
- package/dist/plugins/walrus/client-services.d.mts +1 -0
- package/dist/plugins/walrus/client-services.mjs +123 -0
- package/dist/plugins/walrus/client-services.mjs.map +1 -0
- package/dist/plugins/walrus/codegen.d.mts +1 -0
- package/dist/plugins/walrus/codegen.mjs +1 -0
- package/dist/plugins/walrus/codegen.mjs.map +1 -1
- package/dist/plugins/walrus/deploy.mjs +11 -5
- package/dist/plugins/walrus/deploy.mjs.map +1 -1
- package/dist/plugins/walrus/errors.d.mts +1 -1
- package/dist/plugins/walrus/errors.mjs.map +1 -1
- package/dist/plugins/walrus/index.d.mts +11 -1
- package/dist/plugins/walrus/index.mjs +38 -12
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.d.mts +1 -0
- package/dist/plugins/walrus/mode/known-deploy.mjs +7 -3
- package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
- package/dist/plugins/walrus/mode/local-cluster.d.mts +18 -1
- package/dist/plugins/walrus/mode/local-cluster.mjs +57 -12
- package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
- package/dist/plugins/walrus/routable.mjs +29 -36
- package/dist/plugins/walrus/routable.mjs.map +1 -1
- package/dist/plugins/walrus/snapshot.mjs +3 -2
- package/dist/plugins/walrus/snapshot.mjs.map +1 -1
- package/dist/plugins/walrus/storage-nodes.mjs +31 -33
- package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
- package/dist/substrate/runtime/config-validation.mjs +1 -1
- package/images/walrus/Dockerfile +23 -6
- package/images/walrus/deploy-walrus.sh +7 -4
- package/images/walrus/run-walrus-client-service.sh +135 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["httpRequest"],"sources":["../../../src/orchestrators/router/service.ts"],"sourcesContent":["// Router orchestrator — the L3 service that walks `Routable`\n// contributions, mints hostnames + ids, resolves upstream URLs, and\n// writes file-provider config to a watched directory.\n//\n// Architecture distilled-doc §\"Responsibilities\":\n// 1. Ensure exactly one Traefik container and one docker network\n// exist for the router profile. → `bootstrap()`\n// 2. Maintain the profile dispatch directory mounted into that\n// singleton (one file per backend). → `contributeRoute()`\n// 3. Mint per-backend hostnames + ids from `(app, stack, service)`. → `hostname.ts`\n// 4. Provide a single shared, permissive CORS middleware. → `cors.ts`\n// 5. Tear down per-backend dispatch entries when their owning scope\n// closes; leave the shared container running. → scope finalizers\n//\n// What this orchestrator DOES NOT do (architecture §\"What's NOT in it\"):\n// - Hardcode service names. The orchestrator iterates Routable decls,\n// resolves them by upstream-kind, and renders YAML — no `if (decl.endpointName === 'wallet')`\n// anywhere.\n// - Talk to docker directly. The Traefik container lives behind\n// `TraefikContainerOpsService`. Upstream IP resolution lives behind\n// `UpstreamResolver`. Production composition wires both seams.\n// - Hold module-level mutable state. Contribution state is a\n// `SubscriptionRef`; the shared-network-id and boot decision live\n// in fiber-scoped Refs.\n//\n// Hot-reload protocol:\n// - Contributions are exposed as a `SubscriptionRef<Map<dispatchFileId, RoutableDecl>>`.\n// - Each `contributeRoute(decl)` updates the ref and is paired with a\n// scope finalizer that removes the decl on scope close.\n// - A background fiber watches the ref's change stream; on each\n// emission it diffs against the last applied set and:\n// * for each ADD: resolves upstream + atomically writes the\n// dispatch file.\n// * for each REMOVE: best-effort unlinks the dispatch file.\n// - All writes go through `atomicWriteFile` (tmp + rename) per\n// architecture invariant #5.\n\nimport { Context, Effect, FileSystem, Layer, Ref, SubscriptionRef } from 'effect';\nimport { request as httpRequest } from 'node:http';\nimport * as path from 'node:path';\n\nimport type { RoutableDecl } from '../../contracts/routable.ts';\nimport { connect, DockerHost, DockerSpawner, waitForIp } from '../../runtime/docker/index.ts';\nimport { atomicWriteFile } from '../../substrate/runtime/atomic-write.ts';\nimport { logWarningAndIgnore } from '../../substrate/runtime/observability/index.ts';\nimport type { Identity } from '../../substrate/identity.ts';\nimport {\n\tcheckHolderLiveness,\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\townHolder,\n} from '../../substrate/runtime/cross-process/liveness.ts';\nimport { acquireStackLock } from '../../substrate/runtime/cross-process/stack-lock.ts';\nimport { waitForHttpEndpoint, type HttpProbeFetch } from '../../substrate/runtime/http-probe.ts';\nimport { IdentityContext } from '../../substrate/runtime/paths.ts';\nimport { CORS_MIDDLEWARE_FILENAME, renderCorsMiddlewareYaml } from './cors.ts';\nimport { EntrypointRegistry, type EntrypointRegistryShape } from './entrypoints.ts';\nimport {\n\tDispatchWriteFailed,\n\tRouteCollision,\n\tRouteReadinessProbeFailed,\n\tRouterBootFailed,\n\tRouterDisabledRouteUnsupported,\n\tRouterValidationError,\n\ttype UnknownEntrypoint,\n\ttype RouterError,\n} from './errors.ts';\nimport {\n\tdispatchFilename,\n\tdispatchFileIdFromFilename,\n\ttype DispatchRouteMetadata,\n\ttype DispatchRouteDecodeDiagnostic,\n\ttype ResolvedRoute,\n\ttype ResolvedWireProtocol,\n\ttype RouteLeaseMetadata,\n\ttype UpstreamResolver,\n\tdetectCollisions,\n\tparseDispatchRouteFile,\n\trenderRouteYaml,\n\tROUTE_READINESS_HEADER,\n\tresolveRoute,\n\tROUTER_ROUTE_LEASE_VERSION,\n} from './file-provider.ts';\nimport { dispatchFileId } from './hostname.ts';\nimport type { RouterProfile } from './profile.ts';\nimport { bootstrap, type BootReport, TraefikContainerOpsService } from './traefik-container.ts';\n\n// Router operations are shared across every stack using the same Docker\n// context. The generic stack-lock default stays short for normal metadata\n// mutations; router boot and dispatch can legitimately queue behind other\n// devstack processes during multi-example CI or local parallel runs.\nconst ROUTER_LOCK_TIMEOUT_MILLIS = 120_000;\n\n// ---------------------------------------------------------------------------\n// RouterConfig — orchestrator-level knobs\n// ---------------------------------------------------------------------------\n\n/** Knobs surfaced through `defineDevstack({ router: {...} })` per\n * distilled-doc open question #5. We model them as a typed Context\n * service so the orchestrator reads from a single source and tests\n * can override per scenario. */\nexport interface RouterConfigShape {\n\t/** Disable the router entirely. Only host-loopback Routables can\n\t * produce direct URLs in this mode; container upstreams require\n\t * the router's Docker network + proxy entrypoint and fail\n\t * explicitly. */\n\treadonly disabled: boolean;\n\t/** User + Docker-daemon scoped router profile. Runtime roots own\n\t * route leases; the profile owns the singleton proxy process,\n\t * dispatch directory, network name, and cross-process locks. */\n\treadonly profile: RouterProfile;\n\t/** Traefik image (tag or digest). */\n\treadonly image: string;\n\t/** Optional production gate that waits for Traefik to serve each\n\t * public HTTP route before the endpoint is published. Tests that\n\t * use the stub Traefik layer omit this. */\n\treadonly routeReadinessProbe?: RouteReadinessProbeConfig;\n}\n\nexport interface RouteReadinessProbeConfig {\n\treadonly enabled: boolean;\n\treadonly timeoutMs?: number;\n\treadonly intervalMs?: number;\n\treadonly requestTimeoutMs?: number;\n\treadonly fetch?: HttpProbeFetch;\n}\n\nexport class RouterConfig extends Context.Service<RouterConfig, RouterConfigShape>()(\n\t'@devstack/orchestrators/router/RouterConfig',\n) {}\n\n/** Default-config layer for tests. Production wires this from\n * `orchestrators/boot.ts` at the engine boundary. */\nexport const layerRouterConfigLiteral = (cfg: RouterConfigShape): Layer.Layer<RouterConfig> =>\n\tLayer.succeed(RouterConfig)(cfg);\n\n// ---------------------------------------------------------------------------\n// UpstreamResolverService — Context wrapper around the seam\n// ---------------------------------------------------------------------------\n\nexport class UpstreamResolverService extends Context.Service<\n\tUpstreamResolverService,\n\tUpstreamResolver\n>()('@devstack/orchestrators/router/UpstreamResolver') {}\n\nexport const layerDockerUpstreamResolver = (\n\tprofile: RouterProfile,\n): Layer.Layer<UpstreamResolverService, never, DockerHost | DockerSpawner> =>\n\tLayer.effect(\n\t\tUpstreamResolverService,\n\t\tEffect.gen(function* () {\n\t\t\tconst dockerHost = yield* DockerHost;\n\t\t\tconst dockerSpawner = yield* DockerSpawner;\n\t\t\tconst networkName = profile.networkName;\n\t\t\tconst provideDocker = <A, E>(\n\t\t\t\teffect: Effect.Effect<A, E, DockerHost | DockerSpawner>,\n\t\t\t): Effect.Effect<A, E, never> =>\n\t\t\t\teffect.pipe(\n\t\t\t\t\tEffect.provideService(DockerHost, dockerHost),\n\t\t\t\t\tEffect.provideService(DockerSpawner, dockerSpawner),\n\t\t\t\t);\n\n\t\t\treturn UpstreamResolverService.of({\n\t\t\t\tresolveContainer: (target) =>\n\t\t\t\t\tprovideDocker(\n\t\t\t\t\t\tconnect(target.containerName, networkName).pipe(\n\t\t\t\t\t\t\tEffect.andThen(waitForIp(target.containerName, networkName)),\n\t\t\t\t\t\t),\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.map((host) => ({ host, port: target.containerPort })),\n\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\t\tnew RouterValidationError({\n\t\t\t\t\t\t\t\t\tfield: 'upstreamUrl',\n\t\t\t\t\t\t\t\t\tvalue: target.containerName,\n\t\t\t\t\t\t\t\t\tdetail: `failed to attach/read container upstream on router network: ${String(cause)}`,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\tresolveHostLoopback: (target) =>\n\t\t\t\t\tEffect.succeed({ host: 'host.docker.internal', port: target.port }),\n\t\t\t});\n\t\t}),\n\t);\n\n// ---------------------------------------------------------------------------\n// Router service surface\n// ---------------------------------------------------------------------------\n\nexport interface RouterServiceShape {\n\t/** Boot the Traefik container once per supervisor lifetime.\n\t * Idempotent. Architecture invariant #11 — caller mounts this on\n\t * the long-lived outer scope, NOT inside the hot-reload loop. */\n\treadonly boot: () => Effect.Effect<BootReport, RouterError>;\n\n\t/** Contribute a `RoutableDecl`. The orchestrator resolves the\n\t * upstream URL, renders the file-provider YAML, and writes it\n\t * atomically. The returned `ResolvedRoute` is the post-mint route —\n\t * the single source of truth the boot adapter (`endpointSinksFromRoute`)\n\t * derives both the manifest entry and the projection event from.\n\t *\n\t * Scope-bound: when the caller's scope closes, the dispatch file\n\t * is removed (best-effort) and the contribution is dropped from the\n\t * subscribable map. */\n\treadonly contributeRoute: (\n\t\tdecl: RoutableDecl,\n\t) => Effect.Effect<ResolvedRoute, RouterError, import('effect').Scope.Scope>;\n\n\t/** Subscribable view of currently-applied routes. Surfaces +\n\t * diagnostics consume this. */\n\treadonly applied: SubscriptionRef.SubscriptionRef<ReadonlyArray<ResolvedRoute>>;\n}\n\nexport class RouterService extends Context.Service<RouterService, RouterServiceShape>()(\n\t'@devstack/orchestrators/router/Router',\n) {}\n\ninterface DispatchRouteScanDiagnostic extends DispatchRouteDecodeDiagnostic {\n\treadonly path: string;\n}\n\ninterface DispatchRouteScan {\n\treadonly routes: ReadonlyArray<DispatchRouteMetadata>;\n\treadonly unknownRouteFileIds: ReadonlyArray<string>;\n\treadonly diagnostics: ReadonlyArray<DispatchRouteScanDiagnostic>;\n}\n\nconst warnDispatchDecodeDiagnostic = (\n\tdiagnostic: DispatchRouteScanDiagnostic,\n): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tyield* Effect.logWarning(\n\t\t\t`router dispatch route file ${diagnostic.path} could not be fully decoded; ` +\n\t\t\t\t`reason=${diagnostic.reason}; treating it as an unknown route lease where destructive bootstrap safety matters`,\n\t\t);\n\t});\n\nconst readDispatchRouteScan = (\n\tfs: FileSystem.FileSystem,\n\tdispatchDir: string,\n\tdispatchFileId: string,\n\toptions: { readonly strict?: boolean } = {},\n): Effect.Effect<DispatchRouteScan, DispatchWriteFailed> =>\n\tEffect.gen(function* () {\n\t\tconst files = yield* fs.readDirectory(dispatchDir).pipe(\n\t\t\tEffect.mapError(\n\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\tdispatchFileId,\n\t\t\t\t\t\tpath: dispatchDir,\n\t\t\t\t\t\tdetail: `readDirectory(${dispatchDir}) failed`,\n\t\t\t\t\t\tcause,\n\t\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tconst routeFiles = files\n\t\t\t.map((filename) => ({ filename, fileId: dispatchFileIdFromFilename(filename) }))\n\t\t\t.filter(\n\t\t\t\t(entry): entry is { readonly filename: string; readonly fileId: string } =>\n\t\t\t\t\tentry.fileId !== null,\n\t\t\t);\n\t\tconst parsed = yield* Effect.forEach(\n\t\t\trouteFiles,\n\t\t\t(entry) => {\n\t\t\t\tconst filePath = path.join(dispatchDir, entry.filename);\n\t\t\t\treturn fs.readFileString(filePath).pipe(\n\t\t\t\t\tEffect.map((body) => ({\n\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\tresult: parseDispatchRouteFile(body, entry.fileId),\n\t\t\t\t\t})),\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\tdispatchFileId,\n\t\t\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\t\t\tdetail: `readFileString(${filePath}) failed`,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t},\n\t\t\t{ concurrency: 'unbounded' },\n\t\t);\n\t\tconst routes: DispatchRouteMetadata[] = [];\n\t\tconst unknownRouteFileIds: string[] = [];\n\t\tconst diagnostics: DispatchRouteScanDiagnostic[] = [];\n\t\tfor (const entry of parsed) {\n\t\t\tfor (const diagnostic of entry.result.diagnostics) {\n\t\t\t\tconst withPath = { ...diagnostic, path: entry.path };\n\t\t\t\tdiagnostics.push(withPath);\n\t\t\t\tyield* warnDispatchDecodeDiagnostic(withPath);\n\t\t\t}\n\t\t\tif (entry.result._tag === 'valid') {\n\t\t\t\troutes.push(entry.result.route);\n\t\t\t} else {\n\t\t\t\tunknownRouteFileIds.push(entry.result.dispatchFileId);\n\t\t\t}\n\t\t}\n\t\tif (options.strict === true && diagnostics.length > 0) {\n\t\t\tconst first = diagnostics[0];\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\tdispatchFileId: first?.dispatchFileId ?? dispatchFileId,\n\t\t\t\t\tpath: first?.path ?? dispatchDir,\n\t\t\t\t\tdetail: first?.detail ?? 'dispatch route decode failed',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn { routes, unknownRouteFileIds, diagnostics };\n\t});\n\ntype DispatchLeaseStatus = 'live' | 'stale' | 'unknown-owner';\ntype RoutePublishOwnership = 'direct' | 'owned' | 'reused-live';\n\nconst classifyDispatchLease = (\n\troute: DispatchRouteMetadata,\n): Effect.Effect<DispatchLeaseStatus> => {\n\tif (route.lease === null) return Effect.succeed('unknown-owner');\n\treturn checkHolderLiveness(route.lease.owner).pipe(\n\t\tEffect.map((status) => (status === 'dead' ? 'stale' : 'live')),\n\t\tEffect.catch(() => Effect.succeed('live' as const)),\n\t);\n};\n\nconst sweepStaleDispatchRoutes = (\n\tfs: FileSystem.FileSystem,\n\tprofile: RouterProfile,\n\troutes: ReadonlyArray<DispatchRouteMetadata>,\n\tdispatchFileId: string,\n): Effect.Effect<ReadonlyArray<DispatchRouteMetadata>, DispatchWriteFailed> =>\n\t// Yield a fresh per-sweep `LivenessProbeScope` so a recycled lease\n\t// owner (multiple stale routes pointing at the same dead pid) forks\n\t// `ps`/`tasklist` AT MOST once across the loop — matches the roster\n\t// sweep migration in Phase 9A.\n\tEffect.gen(function* () {\n\t\tconst probe = yield* LivenessProbeScope;\n\t\tconst active: DispatchRouteMetadata[] = [];\n\t\tfor (const route of routes) {\n\t\t\tconst status: DispatchLeaseStatus =\n\t\t\t\troute.lease === null\n\t\t\t\t\t? 'unknown-owner'\n\t\t\t\t\t: yield* probe.probeHolderLiveness(route.lease.owner).pipe(\n\t\t\t\t\t\t\tEffect.map((s) => (s === 'dead' ? ('stale' as const) : ('live' as const))),\n\t\t\t\t\t\t\tEffect.catch(() => Effect.succeed('live' as const)),\n\t\t\t\t\t\t);\n\t\t\tif (status === 'stale') {\n\t\t\t\tconst filePath = path.join(profile.dispatchDir, dispatchFilename(route.dispatchFileId));\n\t\t\t\tyield* fs.remove(filePath).pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\tdispatchFileId,\n\t\t\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\t\t\tdetail: `failed to remove stale route lease ${route.dispatchFileId}`,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tactive.push(route);\n\t\t}\n\t\treturn active;\n\t}).pipe(Effect.provide(layerLivenessProbeScope));\n\nconst makeRouteLease = (profile: RouterProfile, identity: Identity): RouteLeaseMetadata => ({\n\tversion: ROUTER_ROUTE_LEASE_VERSION,\n\trouterProfileId: profile.id,\n\tapp: String(identity.app),\n\tstack: String(identity.stack),\n\towner: ownHolder(),\n});\n\nconst sameRouteSurface = (\n\ta: Pick<DispatchRouteMetadata, 'hostname' | 'entrypointName' | 'entrypointPort' | 'wireProtocol'>,\n\tb: Pick<ResolvedRoute, 'hostname' | 'entrypointName' | 'entrypointPort' | 'wireProtocol'>,\n): boolean =>\n\ta.hostname === b.hostname &&\n\ta.entrypointName === b.entrypointName &&\n\ta.entrypointPort === b.entrypointPort &&\n\ta.wireProtocol === b.wireProtocol;\n\nconst liveRouteLeaseMismatch = (\n\texisting: DispatchRouteMetadata,\n\tresolved: ResolvedRoute,\n): RouteCollision =>\n\tnew RouteCollision({\n\t\tmessage:\n\t\t\t`router route ${resolved.dispatchFileId} is already leased by a live process ` +\n\t\t\t`with a different public route (existing ${existing.entrypointName}/` +\n\t\t\t`${existing.hostname}, attempted ${resolved.entrypointName}/${resolved.hostname})`,\n\t\thostname: resolved.hostname,\n\t\tentrypoint: resolved.entrypointName,\n\t\tdispatchIds: [existing.dispatchFileId, resolved.dispatchFileId],\n\t});\n\nconst resolvedWireProtocolFor = (decl: RoutableDecl): ResolvedWireProtocol =>\n\tdecl.wireProtocol === 'tcp' ? 'tcp' : decl.wireProtocol === 'h2c' ? 'h2c' : 'http';\n\nconst validateWireProtocolFamily = (\n\tdecl: RoutableDecl,\n\tentrypoint: { readonly name: string; readonly protocol: 'http' | 'h2c' | 'tcp' },\n): Effect.Effect<ResolvedWireProtocol, RouterValidationError> => {\n\tconst wireProtocol = resolvedWireProtocolFor(decl);\n\tconst expectFamily: 'tcp' | 'http' = wireProtocol === 'tcp' ? 'tcp' : 'http';\n\tconst entrypointFamily: 'tcp' | 'http' = entrypoint.protocol === 'tcp' ? 'tcp' : 'http';\n\tif (expectFamily === entrypointFamily) return Effect.succeed(wireProtocol);\n\treturn Effect.fail(\n\t\tnew RouterValidationError({\n\t\t\tfield: 'entrypointName',\n\t\t\tvalue: entrypoint.name,\n\t\t\tdetail:\n\t\t\t\t`wireProtocol family mismatch: decl is '${wireProtocol}' but ` +\n\t\t\t\t`entrypoint '${entrypoint.name}' is '${entrypoint.protocol}'`,\n\t\t}),\n\t);\n};\n\nconst directLoopbackHost = '127.0.0.1';\nexport const DEFAULT_ROUTE_READINESS_TIMEOUT_MS = 60_000;\nconst DEFAULT_ROUTE_READINESS_INTERVAL_MS = 100;\nconst DEFAULT_ROUTE_READINESS_REQUEST_TIMEOUT_MS = 750;\nconst proxyGatewayStatuses = new Set([502, 503, 504]);\n\nconst responseHasReadyRoute = (response: Response, resolved: ResolvedRoute): boolean =>\n\tresponse.headers.get(ROUTE_READINESS_HEADER) === resolved.dispatchFileId &&\n\t!proxyGatewayStatuses.has(response.status);\n\nconst fetchHttpRouteViaLoopback: HttpProbeFetch = (input, init) =>\n\tnew Promise<Response>((resolveResponse, rejectResponse) => {\n\t\tconst url = new URL(String(input));\n\t\tconst headers = new Headers(init?.headers);\n\t\tconst signal = init?.signal ?? undefined;\n\t\tconst hostHeader = headers.get('host') ?? headers.get('Host') ?? url.host;\n\t\theaders.delete('host');\n\t\theaders.delete('Host');\n\t\tconst requestHeaders: Record<string, string> = {};\n\t\theaders.forEach((value, key) => {\n\t\t\trequestHeaders[key] = value;\n\t\t});\n\t\trequestHeaders.host = hostHeader;\n\t\tlet settled = false;\n\t\tlet req: ReturnType<typeof httpRequest> | null = null;\n\t\tfunction onAbort(): void {\n\t\t\tconst cause = new Error('route readiness probe aborted');\n\t\t\treq?.destroy(cause);\n\t\t\tsettle(rejectResponse, cause);\n\t\t}\n\t\tconst settle = <T>(fn: (value: T) => void, value: T): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tif (signal !== undefined) {\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t}\n\t\t\tfn(value);\n\t\t};\n\t\treq = httpRequest(\n\t\t\t{\n\t\t\t\thostname: directLoopbackHost,\n\t\t\t\tport: url.port === '' ? 80 : Number(url.port),\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\tmethod: init?.method ?? 'GET',\n\t\t\t\theaders: requestHeaders,\n\t\t\t},\n\t\t\t(res) => {\n\t\t\t\tconst chunks: Uint8Array[] = [];\n\t\t\t\tres.on('data', (chunk: Buffer | string) => {\n\t\t\t\t\tchunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n\t\t\t\t});\n\t\t\t\tres.on('end', () => {\n\t\t\t\t\tconst responseHeaders = new Headers();\n\t\t\t\t\tfor (const [key, value] of Object.entries(res.headers)) {\n\t\t\t\t\t\tif (value === undefined) continue;\n\t\t\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\t\t\tfor (const item of value) responseHeaders.append(key, item);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresponseHeaders.set(key, value);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsettle(\n\t\t\t\t\t\tresolveResponse,\n\t\t\t\t\t\tnew Response(new Uint8Array(Buffer.concat(chunks)), {\n\t\t\t\t\t\t\tstatus: res.statusCode ?? 599,\n\t\t\t\t\t\t\tstatusText: res.statusMessage,\n\t\t\t\t\t\t\theaders: responseHeaders,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t\tif (signal?.aborted === true) {\n\t\t\tonAbort();\n\t\t\treturn;\n\t\t}\n\t\tsignal?.addEventListener('abort', onAbort, { once: true });\n\t\treq.on('error', (cause) => settle(rejectResponse, cause));\n\t\treq.end();\n\t});\n\nconst resolveDisabledDirectRoute = (\n\tidentity: Identity,\n\tdecl: RoutableDecl,\n\tregistry: EntrypointRegistryShape,\n): Effect.Effect<\n\tResolvedRoute,\n\tUnknownEntrypoint | RouterDisabledRouteUnsupported | RouterValidationError\n> =>\n\tEffect.gen(function* () {\n\t\tif (decl.upstream.type === 'container') {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterDisabledRouteUnsupported({\n\t\t\t\t\tendpointName: decl.endpointName,\n\t\t\t\t\tupstreamKind: 'container',\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t'router is disabled, but container upstreams are only reachable through the router network/proxy; ' +\n\t\t\t\t\t\t'use a host-loopback upstream or publish an explicit direct endpoint instead',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst port = decl.upstream.port;\n\t\tif (!Number.isInteger(port) || port <= 0 || port > 65535) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'upstreamUrl',\n\t\t\t\t\tvalue: `${directLoopbackHost}:${port}`,\n\t\t\t\t\tdetail: 'disabled-router direct host-loopback routes require a concrete TCP port 1-65535',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst entrypoint = yield* registry.byName(decl.endpointName);\n\t\tconst wireProtocol = yield* validateWireProtocolFamily(decl, entrypoint);\n\t\tconst fileId = yield* dispatchFileId({ identity, dispatch: decl.dispatchId });\n\t\tconst scheme = wireProtocol === 'tcp' ? 'tcp' : 'http';\n\t\treturn {\n\t\t\tdispatchFileId: fileId,\n\t\t\thostname: directLoopbackHost,\n\t\t\tentrypointName: entrypoint.name,\n\t\t\tentrypointPort: port,\n\t\t\tupstreamUrl: `${scheme}://${directLoopbackHost}:${port}`,\n\t\t\tcors: decl.wireProtocol === 'tcp' ? false : decl.cors,\n\t\t\twireProtocol,\n\t\t};\n\t});\n\nconst removeDispatchFile = (\n\tfs: FileSystem.FileSystem,\n\tprofile: RouterProfile,\n\tresolved: ResolvedRoute,\n): Effect.Effect<void> =>\n\tfs\n\t\t.remove(path.join(profile.dispatchDir, dispatchFilename(resolved.dispatchFileId)))\n\t\t.pipe(Effect.ignore);\n\nconst waitForPublicRouteReadiness = (\n\tcfg: RouterConfigShape,\n\tdecl: RoutableDecl,\n\tresolved: ResolvedRoute,\n): Effect.Effect<void, RouteReadinessProbeFailed> => {\n\tconst options = cfg.routeReadinessProbe;\n\tif (\n\t\toptions?.enabled !== true ||\n\t\tcfg.disabled ||\n\t\tresolved.wireProtocol === 'tcp' ||\n\t\tdecl.readiness === 'deferred'\n\t) {\n\t\treturn Effect.void;\n\t}\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_ROUTE_READINESS_TIMEOUT_MS;\n\tconst intervalMs = options.intervalMs ?? DEFAULT_ROUTE_READINESS_INTERVAL_MS;\n\tconst requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_ROUTE_READINESS_REQUEST_TIMEOUT_MS;\n\tconst probeUrl = `http://${directLoopbackHost}:${resolved.entrypointPort}`;\n\t// The probe only runs for non-tcp routes, so the public endpoint URL is\n\t// the http form — same derivation the boot adapter surfaces.\n\tconst publicUrl = `http://${resolved.hostname}:${resolved.entrypointPort}`;\n\treturn waitForHttpEndpoint({\n\t\tendpoint: probeUrl,\n\t\ttimeoutMs,\n\t\tintervalMs,\n\t\trequestTimeoutMs,\n\t\trequestInit: { headers: { host: resolved.hostname } },\n\t\tfetch: options.fetch ?? fetchHttpRouteViaLoopback,\n\t\tvalidate: (response) => responseHasReadyRoute(response, resolved),\n\t}).pipe(\n\t\tEffect.mapError(\n\t\t\t(cause): RouteReadinessProbeFailed =>\n\t\t\t\tnew RouteReadinessProbeFailed({\n\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\turl: publicUrl,\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`public router endpoint ${publicUrl} did not serve route ` +\n\t\t\t\t\t\t`${resolved.dispatchFileId} within ${timeoutMs}ms ` +\n\t\t\t\t\t\t`(probeUrl=${probeUrl}, hostHeader=${resolved.hostname})`,\n\t\t\t\t\tcause,\n\t\t\t\t}),\n\t\t),\n\t);\n};\n\n// ---------------------------------------------------------------------------\n// Layer — wire the orchestrator\n// ---------------------------------------------------------------------------\n\nexport const layerRouterService: Layer.Layer<\n\tRouterService,\n\tnever,\n\t| RouterConfig\n\t| IdentityContext\n\t| EntrypointRegistry\n\t| TraefikContainerOpsService\n\t| UpstreamResolverService\n\t| FileSystem.FileSystem\n> = Layer.effect(\n\tRouterService,\n\tEffect.gen(function* () {\n\t\tconst cfg = yield* RouterConfig;\n\t\tconst identity = yield* IdentityContext;\n\t\tconst registry = yield* EntrypointRegistry;\n\t\tconst upstreams = yield* UpstreamResolverService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t// Capture the Traefik ops service in the layer's outer scope so\n\t\t// `bootstrap` (which reads it from Context) doesn't surface\n\t\t// requirements through `boot()`'s return type. Provided to\n\t\t// downstream calls via `Effect.provideService`.\n\t\tconst traefikOps = yield* TraefikContainerOpsService;\n\n\t\t// Cached boot report — set on first `boot()` call.\n\t\tconst bootRef = yield* Ref.make<BootReport | null>(null);\n\n\t\t// Applied routes — subscribable for diagnostics. Empty until the\n\t\t// first `contributeRoute()`. The orchestrator publishes the\n\t\t// *resolved* shape (post URL-resolution) rather than the raw\n\t\t// decls so downstream surfaces don't have to re-resolve.\n\t\tconst applied = yield* SubscriptionRef.make<ReadonlyArray<ResolvedRoute>>([]);\n\n\t\t// ---------------------------------------------------------------\n\t\t// boot — adopt-or-create the Traefik container.\n\t\t// ---------------------------------------------------------------\n\t\tconst boot: RouterServiceShape['boot'] = () =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst profile = cfg.profile;\n\t\t\t\tif (cfg.disabled) {\n\t\t\t\t\tconst report: BootReport = {\n\t\t\t\t\t\tdecision: 'opt-out',\n\t\t\t\t\t\tcontainerId: null,\n\t\t\t\t\t\tnetworkId: null,\n\t\t\t\t\t\timageMatches: true,\n\t\t\t\t\t};\n\t\t\t\t\tyield* Ref.set(bootRef, report);\n\t\t\t\t\treturn report;\n\t\t\t\t}\n\t\t\t\tconst cached = yield* Ref.get(bootRef);\n\t\t\t\tif (cached !== null) return cached;\n\t\t\t\t// Single outer scope holds BOTH locks for the entire boot duration.\n\t\t\t\t// Acquire dispatch lock first (outer), then bootstrap lock (inner) so\n\t\t\t\t// scope finalizers release in reverse order: bootstrap lock first, then\n\t\t\t\t// dispatch lock. `protectedRouteLeaseIds` is computed under the\n\t\t\t\t// dispatch lock and consumed by `bootstrap` while still under the same\n\t\t\t\t// lock — no peer-write window can publish a new dispatch route file\n\t\t\t\t// between the scan and the bootstrap-time forceRemove decision.\n\t\t\t\t// STYLE_GUIDE §18 cross-process protocol — router boot must hold the\n\t\t\t\t// dispatch lock across the scan + bootstrap critical section, exactly\n\t\t\t\t// the same way `contributeRoute` holds it across write + probe.\n\t\t\t\tconst report = yield* Effect.scoped(\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tyield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'ensure-container',\n\t\t\t\t\t\t\t\t\t\tdetail: `failed to acquire router dispatch lock ${profile.dispatchLockFile}`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\t// Re-check after acquiring the dispatch lock: a peer fiber\n\t\t\t\t\t\t// (parallel plugin acquire) may have finished boot while we\n\t\t\t\t\t\t// waited for this lock. The O_EXCL lock serializes in-process\n\t\t\t\t\t\t// fibers too, and the winner sets `bootRef` below while still\n\t\t\t\t\t\t// holding the lock — so a non-null read here means boot is\n\t\t\t\t\t\t// done; skip the redundant bootstrap (docker inspect + decision).\n\t\t\t\t\t\tconst bootedByPeer = yield* Ref.get(bootRef);\n\t\t\t\t\t\tif (bootedByPeer !== null) return bootedByPeer;\n\t\t\t\t\t\tyield* fs.makeDirectory(profile.dispatchDir, { recursive: true }).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'write-shared-config',\n\t\t\t\t\t\t\t\t\t\tdetail: `makeDirectory(${profile.dispatchDir}) failed`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tyield* atomicWriteFile(\n\t\t\t\t\t\t\tpath.join(profile.dispatchDir, CORS_MIDDLEWARE_FILENAME),\n\t\t\t\t\t\t\tnew TextEncoder().encode(renderCorsMiddlewareYaml()),\n\t\t\t\t\t\t\t{ mode: 0o644 },\n\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'write-shared-config',\n\t\t\t\t\t\t\t\t\t\tdetail: `atomicWriteFile(${CORS_MIDDLEWARE_FILENAME}) failed at stage ${cause.stage}`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst existingDispatchScan = yield* readDispatchRouteScan(\n\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\tprofile.dispatchDir,\n\t\t\t\t\t\t\t'router-boot',\n\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'inspect',\n\t\t\t\t\t\t\t\t\t\tdetail: cause.detail,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst activeDispatchRoutes = yield* sweepStaleDispatchRoutes(\n\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\tprofile,\n\t\t\t\t\t\t\texistingDispatchScan.routes,\n\t\t\t\t\t\t\t'router-boot',\n\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'write-shared-config',\n\t\t\t\t\t\t\t\t\t\tdetail: cause.detail,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst protectedRouteLeaseIds = [\n\t\t\t\t\t\t\t...activeDispatchRoutes.map((route) => route.dispatchFileId),\n\t\t\t\t\t\t\t...existingDispatchScan.unknownRouteFileIds,\n\t\t\t\t\t\t];\n\t\t\t\t\t\tyield* acquireStackLock(profile.bootstrapLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'ensure-container',\n\t\t\t\t\t\t\t\t\t\tdetail: `failed to acquire router bootstrap lock ${profile.bootstrapLockFile}`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst booted = yield* bootstrap({\n\t\t\t\t\t\t\timage: cfg.image,\n\t\t\t\t\t\t\tentrypoints: registry.all(),\n\t\t\t\t\t\t\tprofile,\n\t\t\t\t\t\t\tprotectedRouteLeaseIds,\n\t\t\t\t\t\t}).pipe(Effect.provideService(TraefikContainerOpsService, traefikOps));\n\t\t\t\t\t\t// Publish the cached report WHILE still holding the locks so a\n\t\t\t\t\t\t// peer fiber's post-lock re-check (above) observes it — the\n\t\t\t\t\t\t// locks release at scope close, which is after this set. On\n\t\t\t\t\t\t// bootstrap failure we never reach here, so `bootRef` stays\n\t\t\t\t\t\t// null and a later boot() retries (retry-on-failure preserved).\n\t\t\t\t\t\tyield* Ref.set(bootRef, booted);\n\t\t\t\t\t\treturn booted;\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn report;\n\t\t\t});\n\n\t\t// ---------------------------------------------------------------\n\t\t// contributeRoute — resolve + write dispatch file + scope finalizer.\n\t\t// ---------------------------------------------------------------\n\t\tconst contributeRoute: RouterServiceShape['contributeRoute'] = (decl) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst profile = cfg.profile;\n\t\t\t\tconst resolved = cfg.disabled\n\t\t\t\t\t? yield* resolveDisabledDirectRoute(identity, decl, registry)\n\t\t\t\t\t: yield* resolveRoute(identity, decl, registry, upstreams);\n\t\t\t\tconst lease = makeRouteLease(profile, identity);\n\t\t\t\tconst publishRouteFile: Effect.Effect<\n\t\t\t\t\tRoutePublishOwnership,\n\t\t\t\t\tDispatchWriteFailed | RouteCollision\n\t\t\t\t> = Effect.gen(function* () {\n\t\t\t\t\t// Collision check against this process's applied set\n\t\t\t\t\t// plus files already present in the shared dispatch\n\t\t\t\t\t// directory. The dispatch lock makes the scan + write a\n\t\t\t\t\t// cross-process critical section.\n\t\t\t\t\t// WHY: disabled mode resolves direct-loopback routes with no\n\t\t\t\t\t// proxy dispatch file, so there is no shared resource a\n\t\t\t\t\t// duplicate could clobber — intentionally skip\n\t\t\t\t\t// `detectCollisions` and short-circuit before the\n\t\t\t\t\t// dispatch-dir scan/write below.\n\t\t\t\t\tif (cfg.disabled) return 'direct';\n\t\t\t\t\tif (!cfg.disabled) {\n\t\t\t\t\t\tyield* fs.makeDirectory(profile.dispatchDir, { recursive: true }).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\t\tpath: profile.dispatchDir,\n\t\t\t\t\t\t\t\t\t\tdetail: `makeDirectory(${profile.dispatchDir}) failed`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst currentApplied = yield* SubscriptionRef.get(applied);\n\t\t\t\t\tconst currentIds = new Set(currentApplied.map((route) => route.dispatchFileId));\n\t\t\t\t\tlet reuseLiveRoute = false;\n\t\t\t\t\tconst readScan = yield* readDispatchRouteScan(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tprofile.dispatchDir,\n\t\t\t\t\t\tresolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tconst activeDispatchRoutes = yield* sweepStaleDispatchRoutes(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tprofile,\n\t\t\t\t\t\treadScan.routes,\n\t\t\t\t\t\tresolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tconst existingSameDispatchRoute = activeDispatchRoutes.find(\n\t\t\t\t\t\t(route) => route.dispatchFileId === resolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tif (\n\t\t\t\t\t\texistingSameDispatchRoute !== undefined &&\n\t\t\t\t\t\t!currentIds.has(existingSameDispatchRoute.dispatchFileId)\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst status = yield* classifyDispatchLease(existingSameDispatchRoute);\n\t\t\t\t\t\tif (status === 'live') {\n\t\t\t\t\t\t\tif (!sameRouteSurface(existingSameDispatchRoute, resolved)) {\n\t\t\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\t\t\tliveRouteLeaseMismatch(existingSameDispatchRoute, resolved),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treuseLiveRoute = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst existingDispatchRoutes = activeDispatchRoutes.filter(\n\t\t\t\t\t\t(route) =>\n\t\t\t\t\t\t\t!currentIds.has(route.dispatchFileId) &&\n\t\t\t\t\t\t\troute.dispatchFileId !== resolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tconst collision = detectCollisions([\n\t\t\t\t\t\t...existingDispatchRoutes,\n\t\t\t\t\t\t...currentApplied,\n\t\t\t\t\t\tresolved,\n\t\t\t\t\t]);\n\t\t\t\t\tif (collision) return yield* Effect.fail(collision);\n\t\t\t\t\tif (reuseLiveRoute) return 'reused-live';\n\n\t\t\t\t\t// Atomic write — invariant #5. tmp + rename via the\n\t\t\t\t\t// substrate's `atomicWriteFile` helper; Traefik's\n\t\t\t\t\t// watcher tolerates rename atomically.\n\t\t\t\t\tconst filePath = path.join(\n\t\t\t\t\t\tprofile.dispatchDir,\n\t\t\t\t\t\tdispatchFilename(resolved.dispatchFileId),\n\t\t\t\t\t);\n\t\t\t\t\tyield* atomicWriteFile(\n\t\t\t\t\t\tfilePath,\n\t\t\t\t\t\tnew TextEncoder().encode(renderRouteYaml(resolved, lease)),\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmode: 0o644,\n\t\t\t\t\t\t},\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\t\t\t\tdetail: `atomicWriteFile failed at stage ${cause.stage}`,\n\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\t\t\t);\n\t\t\t\t\treturn 'owned';\n\t\t\t\t});\n\n\t\t\t\tlet publishOwnership: RoutePublishOwnership;\n\t\t\t\tif (cfg.disabled) {\n\t\t\t\t\tpublishOwnership = yield* publishRouteFile;\n\t\t\t\t\tyield* waitForPublicRouteReadiness(cfg, decl, resolved);\n\t\t\t\t\tif (publishOwnership !== 'reused-live') {\n\t\t\t\t\t\tyield* SubscriptionRef.update(applied, (arr) => [...arr, resolved]);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// The dispatch lock MUST be held across (a) the on-disk\n\t\t\t\t\t// scan + write inside `publishRouteFile`, (b) the\n\t\t\t\t\t// readiness probe, AND (c) the in-process\n\t\t\t\t\t// `SubscriptionRef.update(applied, …)` that publishes the\n\t\t\t\t\t// new route to peer fibers in this process. Releasing\n\t\t\t\t\t// the lock before the SubscriptionRef update would let a\n\t\t\t\t\t// concurrent in-process `contributeRoute` sample the\n\t\t\t\t\t// stale `applied` set at line 803 even though our\n\t\t\t\t\t// dispatch file is already on disk — the on-disk scan\n\t\t\t\t\t// covers cross-process visibility but the lock must also\n\t\t\t\t\t// gate the in-process publish so the two views agree.\n\t\t\t\t\t// See STYLE_GUIDE §18 cross-process protocol — router\n\t\t\t\t\t// contributeRoute serialization rule.\n\t\t\t\t\tpublishOwnership = yield* Effect.scoped(\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tyield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(\n\t\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\t\t\tpath: profile.dispatchLockFile,\n\t\t\t\t\t\t\t\t\t\t\tdetail: `failed to acquire dispatch lock ${profile.dispatchLockFile}`,\n\t\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst ownership = yield* publishRouteFile;\n\t\t\t\t\t\t\tyield* waitForPublicRouteReadiness(cfg, decl, resolved).pipe(\n\t\t\t\t\t\t\t\tEffect.onError(() =>\n\t\t\t\t\t\t\t\t\townership !== 'owned' ? Effect.void : removeDispatchFile(fs, profile, resolved),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (ownership !== 'reused-live') {\n\t\t\t\t\t\t\t\tyield* SubscriptionRef.update(applied, (arr) => [...arr, resolved]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn ownership;\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (publishOwnership !== 'reused-live') {\n\t\t\t\t\t// Scope finalizer — remove the file + drop from applied\n\t\t\t\t\t// when the caller's scope closes. Best-effort: \"already\n\t\t\t\t\t// gone\" is fine, but lock contention / IO failures surface\n\t\t\t\t\t// via logWarning so leaked dispatch files don't go silent\n\t\t\t\t\t// (STYLE_GUIDE §18 — `Effect.ignore` on `acquireStackLock`\n\t\t\t\t\t// without a tap is forbidden).\n\t\t\t\t\tyield* Effect.addFinalizer(() =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tif (publishOwnership === 'owned') {\n\t\t\t\t\t\t\t\tyield* Effect.scoped(\n\t\t\t\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\t\t\t\tyield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS);\n\t\t\t\t\t\t\t\t\t\tyield* removeDispatchFile(fs, profile, resolved);\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\t\t\tlogWarningAndIgnore('router scope-close cleanup failed', {\n\t\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tyield* SubscriptionRef.update(applied, (arr) =>\n\t\t\t\t\t\t\t\tarr.filter((r) => r.dispatchFileId !== resolved.dispatchFileId),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn resolved;\n\t\t\t});\n\n\t\treturn RouterService.of({ boot, contributeRoute, applied });\n\t}),\n);\n\n// ---------------------------------------------------------------------------\n// Helpers for tests + composition\n// ---------------------------------------------------------------------------\n\n// Re-export for ergonomics — callers reach for the orchestrator\n// without having to spell out the sibling module paths.\nexport { type ResolvedRoute, type UpstreamResolver } from './file-provider.ts';\nexport type { BootReport } from './traefik-container.ts';\nexport type { UpstreamResolveTimeout } from './errors.ts';\nexport type { Identity } from '../../substrate/identity.ts';\nexport type { RoutableDecl } from '../../contracts/routable.ts';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["httpRequest"],"sources":["../../../src/orchestrators/router/service.ts"],"sourcesContent":["// Router orchestrator — the L3 service that walks `Routable`\n// contributions, mints hostnames + ids, resolves upstream URLs, and\n// writes file-provider config to a watched directory.\n//\n// Architecture distilled-doc §\"Responsibilities\":\n// 1. Ensure exactly one Traefik container and one docker network\n// exist for the router profile. → `bootstrap()`\n// 2. Maintain the profile dispatch directory mounted into that\n// singleton (one file per backend). → `contributeRoute()`\n// 3. Mint per-backend hostnames + ids from `(app, stack, service)`. → `hostname.ts`\n// 4. Provide a single shared, permissive CORS middleware. → `cors.ts`\n// 5. Tear down per-backend dispatch entries when their owning scope\n// closes; leave the shared container running. → scope finalizers\n//\n// What this orchestrator DOES NOT do (architecture §\"What's NOT in it\"):\n// - Hardcode service names. The orchestrator iterates Routable decls,\n// resolves them by upstream-kind, and renders YAML — no `if (decl.endpointName === 'wallet')`\n// anywhere.\n// - Talk to docker directly. The Traefik container lives behind\n// `TraefikContainerOpsService`. Upstream IP resolution lives behind\n// `UpstreamResolver`. Production composition wires both seams.\n// - Hold module-level mutable state. Contribution state is a\n// `SubscriptionRef`; the shared-network-id and boot decision live\n// in fiber-scoped Refs.\n//\n// Hot-reload protocol:\n// - Contributions are exposed as a `SubscriptionRef<Map<dispatchFileId, RoutableDecl>>`.\n// - Each `contributeRoute(decl)` updates the ref and is paired with a\n// scope finalizer that removes the decl on scope close.\n// - A background fiber watches the ref's change stream; on each\n// emission it diffs against the last applied set and:\n// * for each ADD: resolves upstream + atomically writes the\n// dispatch file.\n// * for each REMOVE: best-effort unlinks the dispatch file.\n// - All writes go through `atomicWriteFile` (tmp + rename) per\n// architecture invariant #5.\n\nimport { Context, Effect, FileSystem, Layer, Ref, SubscriptionRef } from 'effect';\nimport { request as httpRequest } from 'node:http';\nimport * as path from 'node:path';\n\nimport type { RoutableDecl } from '../../contracts/routable.ts';\nimport { connect, DockerHost, DockerSpawner, waitForIp } from '../../runtime/docker/index.ts';\nimport { atomicWriteFile } from '../../substrate/runtime/atomic-write.ts';\nimport { logWarningAndIgnore } from '../../substrate/runtime/observability/index.ts';\nimport type { Identity } from '../../substrate/identity.ts';\nimport {\n\tcheckHolderLiveness,\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\townHolder,\n} from '../../substrate/runtime/cross-process/liveness.ts';\nimport { acquireStackLock } from '../../substrate/runtime/cross-process/stack-lock.ts';\nimport { waitForHttpEndpoint, type HttpProbeFetch } from '../../substrate/runtime/http-probe.ts';\nimport { IdentityContext } from '../../substrate/runtime/paths.ts';\nimport { CORS_MIDDLEWARE_FILENAME, renderCorsMiddlewareYaml } from './cors.ts';\nimport { EntrypointRegistry, type EntrypointRegistryShape } from './entrypoints.ts';\nimport {\n\tDispatchWriteFailed,\n\tRouteCollision,\n\tRouteReadinessProbeFailed,\n\tRouterBootFailed,\n\tRouterDisabledRouteUnsupported,\n\tRouterValidationError,\n\ttype UnknownEntrypoint,\n\ttype RouterError,\n} from './errors.ts';\nimport {\n\tdispatchFilename,\n\tdispatchFileIdFromFilename,\n\ttype DispatchRouteMetadata,\n\ttype DispatchRouteDecodeDiagnostic,\n\ttype ResolvedRoute,\n\ttype ResolvedWireProtocol,\n\ttype RouteLeaseMetadata,\n\ttype UpstreamResolver,\n\tdetectCollisions,\n\tparseDispatchRouteFile,\n\trenderRouteYaml,\n\tROUTE_READINESS_HEADER,\n\tresolveRoute,\n\tROUTER_ROUTE_LEASE_VERSION,\n} from './file-provider.ts';\nimport { dispatchFileId } from './hostname.ts';\nimport type { RouterProfile } from './profile.ts';\nimport { bootstrap, type BootReport, TraefikContainerOpsService } from './traefik-container.ts';\n\n// Router operations are shared across every stack using the same Docker\n// context. The generic stack-lock default stays short for normal metadata\n// mutations; router boot and dispatch can legitimately queue behind other\n// devstack processes during multi-example CI or local parallel runs.\nconst ROUTER_LOCK_TIMEOUT_MILLIS = 120_000;\n\n// ---------------------------------------------------------------------------\n// RouterConfig — orchestrator-level knobs\n// ---------------------------------------------------------------------------\n\n/** Knobs surfaced through `defineDevstack({ router: {...} })` per\n * distilled-doc open question #5. We model them as a typed Context\n * service so the orchestrator reads from a single source and tests\n * can override per scenario. */\nexport interface RouterConfigShape {\n\t/** Disable the router entirely. Only host-loopback Routables can\n\t * produce direct URLs in this mode; container upstreams require\n\t * the router's Docker network + proxy entrypoint and fail\n\t * explicitly. */\n\treadonly disabled: boolean;\n\t/** User + Docker-daemon scoped router profile. Runtime roots own\n\t * route leases; the profile owns the singleton proxy process,\n\t * dispatch directory, network name, and cross-process locks. */\n\treadonly profile: RouterProfile;\n\t/** Traefik image (tag or digest). */\n\treadonly image: string;\n\t/** Optional production gate that waits for Traefik to serve each\n\t * public HTTP route before the endpoint is published. Tests that\n\t * use the stub Traefik layer omit this. */\n\treadonly routeReadinessProbe?: RouteReadinessProbeConfig;\n}\n\nexport interface RouteReadinessProbeConfig {\n\treadonly enabled: boolean;\n\treadonly timeoutMs?: number;\n\treadonly intervalMs?: number;\n\treadonly requestTimeoutMs?: number;\n\treadonly fetch?: HttpProbeFetch;\n}\n\nexport class RouterConfig extends Context.Service<RouterConfig, RouterConfigShape>()(\n\t'@devstack/orchestrators/router/RouterConfig',\n) {}\n\n/** Default-config layer for tests. Production wires this from\n * `orchestrators/boot.ts` at the engine boundary. */\nexport const layerRouterConfigLiteral = (cfg: RouterConfigShape): Layer.Layer<RouterConfig> =>\n\tLayer.succeed(RouterConfig)(cfg);\n\n// ---------------------------------------------------------------------------\n// UpstreamResolverService — Context wrapper around the seam\n// ---------------------------------------------------------------------------\n\nexport class UpstreamResolverService extends Context.Service<\n\tUpstreamResolverService,\n\tUpstreamResolver\n>()('@devstack/orchestrators/router/UpstreamResolver') {}\n\nexport const layerDockerUpstreamResolver = (\n\tprofile: RouterProfile,\n): Layer.Layer<UpstreamResolverService, never, DockerHost | DockerSpawner> =>\n\tLayer.effect(\n\t\tUpstreamResolverService,\n\t\tEffect.gen(function* () {\n\t\t\tconst dockerHost = yield* DockerHost;\n\t\t\tconst dockerSpawner = yield* DockerSpawner;\n\t\t\tconst networkName = profile.networkName;\n\t\t\tconst provideDocker = <A, E>(\n\t\t\t\teffect: Effect.Effect<A, E, DockerHost | DockerSpawner>,\n\t\t\t): Effect.Effect<A, E, never> =>\n\t\t\t\teffect.pipe(\n\t\t\t\t\tEffect.provideService(DockerHost, dockerHost),\n\t\t\t\t\tEffect.provideService(DockerSpawner, dockerSpawner),\n\t\t\t\t);\n\n\t\t\treturn UpstreamResolverService.of({\n\t\t\t\tresolveContainer: (target) =>\n\t\t\t\t\tprovideDocker(\n\t\t\t\t\t\tconnect(target.containerName, networkName).pipe(\n\t\t\t\t\t\t\tEffect.andThen(waitForIp(target.containerName, networkName)),\n\t\t\t\t\t\t),\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.map((host) => ({ host, port: target.containerPort })),\n\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\t\tnew RouterValidationError({\n\t\t\t\t\t\t\t\t\tfield: 'upstreamUrl',\n\t\t\t\t\t\t\t\t\tvalue: target.containerName,\n\t\t\t\t\t\t\t\t\tdetail: `failed to attach/read container upstream on router network: ${String(cause)}`,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\tresolveHostLoopback: (target) =>\n\t\t\t\t\tEffect.succeed({ host: 'host.docker.internal', port: target.port }),\n\t\t\t});\n\t\t}),\n\t);\n\n// ---------------------------------------------------------------------------\n// Router service surface\n// ---------------------------------------------------------------------------\n\nexport interface RouterServiceShape {\n\t/** Boot the Traefik container once per supervisor lifetime.\n\t * Idempotent. Architecture invariant #11 — caller mounts this on\n\t * the long-lived outer scope, NOT inside the hot-reload loop. */\n\treadonly boot: () => Effect.Effect<BootReport, RouterError>;\n\n\t/** Contribute a `RoutableDecl`. The orchestrator resolves the\n\t * upstream URL, renders the file-provider YAML, and writes it\n\t * atomically. The returned `ResolvedRoute` is the post-mint route —\n\t * the single source of truth the boot adapter (`endpointSinksFromRoute`)\n\t * derives both the manifest entry and the projection event from.\n\t *\n\t * Scope-bound: when the caller's scope closes, the dispatch file\n\t * is removed (best-effort) and the contribution is dropped from the\n\t * subscribable map. */\n\treadonly contributeRoute: (\n\t\tdecl: RoutableDecl,\n\t) => Effect.Effect<ResolvedRoute, RouterError, import('effect').Scope.Scope>;\n\n\t/** Subscribable view of currently-applied routes. Surfaces +\n\t * diagnostics consume this. */\n\treadonly applied: SubscriptionRef.SubscriptionRef<ReadonlyArray<ResolvedRoute>>;\n}\n\nexport class RouterService extends Context.Service<RouterService, RouterServiceShape>()(\n\t'@devstack/orchestrators/router/Router',\n) {}\n\ninterface DispatchRouteScanDiagnostic extends DispatchRouteDecodeDiagnostic {\n\treadonly path: string;\n}\n\ninterface DispatchRouteScan {\n\treadonly routes: ReadonlyArray<DispatchRouteMetadata>;\n\treadonly unknownRouteFileIds: ReadonlyArray<string>;\n\treadonly diagnostics: ReadonlyArray<DispatchRouteScanDiagnostic>;\n}\n\nconst warnDispatchDecodeDiagnostic = (\n\tdiagnostic: DispatchRouteScanDiagnostic,\n): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tyield* Effect.logWarning(\n\t\t\t`router dispatch route file ${diagnostic.path} could not be fully decoded; ` +\n\t\t\t\t`reason=${diagnostic.reason}; treating it as an unknown route lease where destructive bootstrap safety matters`,\n\t\t);\n\t});\n\nconst readDispatchRouteScan = (\n\tfs: FileSystem.FileSystem,\n\tdispatchDir: string,\n\tdispatchFileId: string,\n\toptions: { readonly strict?: boolean } = {},\n): Effect.Effect<DispatchRouteScan, DispatchWriteFailed> =>\n\tEffect.gen(function* () {\n\t\tconst files = yield* fs.readDirectory(dispatchDir).pipe(\n\t\t\tEffect.mapError(\n\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\tdispatchFileId,\n\t\t\t\t\t\tpath: dispatchDir,\n\t\t\t\t\t\tdetail: `readDirectory(${dispatchDir}) failed`,\n\t\t\t\t\t\tcause,\n\t\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tconst routeFiles = files\n\t\t\t.map((filename) => ({ filename, fileId: dispatchFileIdFromFilename(filename) }))\n\t\t\t.filter(\n\t\t\t\t(entry): entry is { readonly filename: string; readonly fileId: string } =>\n\t\t\t\t\tentry.fileId !== null,\n\t\t\t);\n\t\tconst parsed = yield* Effect.forEach(\n\t\t\trouteFiles,\n\t\t\t(entry) => {\n\t\t\t\tconst filePath = path.join(dispatchDir, entry.filename);\n\t\t\t\treturn fs.readFileString(filePath).pipe(\n\t\t\t\t\tEffect.map((body) => ({\n\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\tresult: parseDispatchRouteFile(body, entry.fileId),\n\t\t\t\t\t})),\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\tdispatchFileId,\n\t\t\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\t\t\tdetail: `readFileString(${filePath}) failed`,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t},\n\t\t\t{ concurrency: 'unbounded' },\n\t\t);\n\t\tconst routes: DispatchRouteMetadata[] = [];\n\t\tconst unknownRouteFileIds: string[] = [];\n\t\tconst diagnostics: DispatchRouteScanDiagnostic[] = [];\n\t\tfor (const entry of parsed) {\n\t\t\tfor (const diagnostic of entry.result.diagnostics) {\n\t\t\t\tconst withPath = { ...diagnostic, path: entry.path };\n\t\t\t\tdiagnostics.push(withPath);\n\t\t\t\tyield* warnDispatchDecodeDiagnostic(withPath);\n\t\t\t}\n\t\t\tif (entry.result._tag === 'valid') {\n\t\t\t\troutes.push(entry.result.route);\n\t\t\t} else {\n\t\t\t\tunknownRouteFileIds.push(entry.result.dispatchFileId);\n\t\t\t}\n\t\t}\n\t\tif (options.strict === true && diagnostics.length > 0) {\n\t\t\tconst first = diagnostics[0];\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\tdispatchFileId: first?.dispatchFileId ?? dispatchFileId,\n\t\t\t\t\tpath: first?.path ?? dispatchDir,\n\t\t\t\t\tdetail: first?.detail ?? 'dispatch route decode failed',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn { routes, unknownRouteFileIds, diagnostics };\n\t});\n\ntype DispatchLeaseStatus = 'live' | 'stale' | 'unknown-owner';\ntype RoutePublishOwnership = 'direct' | 'owned' | 'reused-live';\n\nconst classifyDispatchLease = (\n\troute: DispatchRouteMetadata,\n): Effect.Effect<DispatchLeaseStatus> => {\n\tif (route.lease === null) return Effect.succeed('unknown-owner');\n\treturn checkHolderLiveness(route.lease.owner).pipe(\n\t\tEffect.map((status) => (status === 'dead' ? 'stale' : 'live')),\n\t\tEffect.catch(() => Effect.succeed('live' as const)),\n\t);\n};\n\nconst sweepStaleDispatchRoutes = (\n\tfs: FileSystem.FileSystem,\n\tprofile: RouterProfile,\n\troutes: ReadonlyArray<DispatchRouteMetadata>,\n\tdispatchFileId: string,\n): Effect.Effect<ReadonlyArray<DispatchRouteMetadata>, DispatchWriteFailed> =>\n\t// Yield a fresh per-sweep `LivenessProbeScope` so a recycled lease\n\t// owner (multiple stale routes pointing at the same dead pid) forks\n\t// `ps`/`tasklist` AT MOST once across the loop — matches the roster\n\t// sweep migration in Phase 9A.\n\tEffect.gen(function* () {\n\t\tconst probe = yield* LivenessProbeScope;\n\t\tconst active: DispatchRouteMetadata[] = [];\n\t\tfor (const route of routes) {\n\t\t\tconst status: DispatchLeaseStatus =\n\t\t\t\troute.lease === null\n\t\t\t\t\t? 'unknown-owner'\n\t\t\t\t\t: yield* probe.probeHolderLiveness(route.lease.owner).pipe(\n\t\t\t\t\t\t\tEffect.map((s) => (s === 'dead' ? ('stale' as const) : ('live' as const))),\n\t\t\t\t\t\t\tEffect.catch(() => Effect.succeed('live' as const)),\n\t\t\t\t\t\t);\n\t\t\tif (status === 'stale') {\n\t\t\t\tconst filePath = path.join(profile.dispatchDir, dispatchFilename(route.dispatchFileId));\n\t\t\t\tyield* fs.remove(filePath).pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\tdispatchFileId,\n\t\t\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\t\t\tdetail: `failed to remove stale route lease ${route.dispatchFileId}`,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tactive.push(route);\n\t\t}\n\t\treturn active;\n\t}).pipe(Effect.provide(layerLivenessProbeScope));\n\nconst makeRouteLease = (profile: RouterProfile, identity: Identity): RouteLeaseMetadata => ({\n\tversion: ROUTER_ROUTE_LEASE_VERSION,\n\trouterProfileId: profile.id,\n\tapp: String(identity.app),\n\tstack: String(identity.stack),\n\towner: ownHolder(),\n});\n\nconst sameRouteSurface = (\n\ta: Pick<DispatchRouteMetadata, 'hostname' | 'entrypointName' | 'entrypointPort' | 'wireProtocol'>,\n\tb: Pick<ResolvedRoute, 'hostname' | 'entrypointName' | 'entrypointPort' | 'wireProtocol'>,\n): boolean =>\n\ta.hostname === b.hostname &&\n\ta.entrypointName === b.entrypointName &&\n\ta.entrypointPort === b.entrypointPort &&\n\ta.wireProtocol === b.wireProtocol;\n\nconst liveRouteLeaseMismatch = (\n\texisting: DispatchRouteMetadata,\n\tresolved: ResolvedRoute,\n): RouteCollision =>\n\tnew RouteCollision({\n\t\tmessage:\n\t\t\t`router route ${resolved.dispatchFileId} is already leased by a live process ` +\n\t\t\t`with a different public route (existing ${existing.entrypointName}/` +\n\t\t\t`${existing.hostname}, attempted ${resolved.entrypointName}/${resolved.hostname})`,\n\t\thostname: resolved.hostname,\n\t\tentrypoint: resolved.entrypointName,\n\t\tdispatchIds: [existing.dispatchFileId, resolved.dispatchFileId],\n\t});\n\nconst resolvedWireProtocolFor = (decl: RoutableDecl): ResolvedWireProtocol =>\n\tdecl.wireProtocol === 'tcp'\n\t\t? 'tcp'\n\t\t: decl.wireProtocol === 'h2c'\n\t\t\t? 'h2c'\n\t\t\t: decl.wireProtocol === 'https'\n\t\t\t\t? 'https'\n\t\t\t\t: 'http';\n\nconst validateWireProtocolFamily = (\n\tdecl: RoutableDecl,\n\tentrypoint: { readonly name: string; readonly protocol: 'http' | 'h2c' | 'tcp' },\n): Effect.Effect<ResolvedWireProtocol, RouterValidationError> => {\n\tconst wireProtocol = resolvedWireProtocolFor(decl);\n\tconst expectFamily: 'tcp' | 'http' = wireProtocol === 'tcp' ? 'tcp' : 'http';\n\tconst entrypointFamily: 'tcp' | 'http' = entrypoint.protocol === 'tcp' ? 'tcp' : 'http';\n\tif (expectFamily === entrypointFamily) return Effect.succeed(wireProtocol);\n\treturn Effect.fail(\n\t\tnew RouterValidationError({\n\t\t\tfield: 'entrypointName',\n\t\t\tvalue: entrypoint.name,\n\t\t\tdetail:\n\t\t\t\t`wireProtocol family mismatch: decl is '${wireProtocol}' but ` +\n\t\t\t\t`entrypoint '${entrypoint.name}' is '${entrypoint.protocol}'`,\n\t\t}),\n\t);\n};\n\nconst directLoopbackHost = '127.0.0.1';\nexport const DEFAULT_ROUTE_READINESS_TIMEOUT_MS = 60_000;\nconst DEFAULT_ROUTE_READINESS_INTERVAL_MS = 100;\nconst DEFAULT_ROUTE_READINESS_REQUEST_TIMEOUT_MS = 750;\nconst proxyGatewayStatuses = new Set([502, 503, 504]);\n\nconst responseHasReadyRoute = (response: Response, resolved: ResolvedRoute): boolean =>\n\tresponse.headers.get(ROUTE_READINESS_HEADER) === resolved.dispatchFileId &&\n\t!proxyGatewayStatuses.has(response.status);\n\nconst fetchHttpRouteViaLoopback: HttpProbeFetch = (input, init) =>\n\tnew Promise<Response>((resolveResponse, rejectResponse) => {\n\t\tconst url = new URL(String(input));\n\t\tconst headers = new Headers(init?.headers);\n\t\tconst signal = init?.signal ?? undefined;\n\t\tconst hostHeader = headers.get('host') ?? headers.get('Host') ?? url.host;\n\t\theaders.delete('host');\n\t\theaders.delete('Host');\n\t\tconst requestHeaders: Record<string, string> = {};\n\t\theaders.forEach((value, key) => {\n\t\t\trequestHeaders[key] = value;\n\t\t});\n\t\trequestHeaders.host = hostHeader;\n\t\tlet settled = false;\n\t\tlet req: ReturnType<typeof httpRequest> | null = null;\n\t\tfunction onAbort(): void {\n\t\t\tconst cause = new Error('route readiness probe aborted');\n\t\t\treq?.destroy(cause);\n\t\t\tsettle(rejectResponse, cause);\n\t\t}\n\t\tconst settle = <T>(fn: (value: T) => void, value: T): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tif (signal !== undefined) {\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t}\n\t\t\tfn(value);\n\t\t};\n\t\treq = httpRequest(\n\t\t\t{\n\t\t\t\thostname: directLoopbackHost,\n\t\t\t\tport: url.port === '' ? 80 : Number(url.port),\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\tmethod: init?.method ?? 'GET',\n\t\t\t\theaders: requestHeaders,\n\t\t\t},\n\t\t\t(res) => {\n\t\t\t\tconst chunks: Uint8Array[] = [];\n\t\t\t\tres.on('data', (chunk: Buffer | string) => {\n\t\t\t\t\tchunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n\t\t\t\t});\n\t\t\t\tres.on('end', () => {\n\t\t\t\t\tconst responseHeaders = new Headers();\n\t\t\t\t\tfor (const [key, value] of Object.entries(res.headers)) {\n\t\t\t\t\t\tif (value === undefined) continue;\n\t\t\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\t\t\tfor (const item of value) responseHeaders.append(key, item);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresponseHeaders.set(key, value);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsettle(\n\t\t\t\t\t\tresolveResponse,\n\t\t\t\t\t\tnew Response(new Uint8Array(Buffer.concat(chunks)), {\n\t\t\t\t\t\t\tstatus: res.statusCode ?? 599,\n\t\t\t\t\t\t\tstatusText: res.statusMessage,\n\t\t\t\t\t\t\theaders: responseHeaders,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t\tif (signal?.aborted === true) {\n\t\t\tonAbort();\n\t\t\treturn;\n\t\t}\n\t\tsignal?.addEventListener('abort', onAbort, { once: true });\n\t\treq.on('error', (cause) => settle(rejectResponse, cause));\n\t\treq.end();\n\t});\n\nconst resolveDisabledDirectRoute = (\n\tidentity: Identity,\n\tdecl: RoutableDecl,\n\tregistry: EntrypointRegistryShape,\n): Effect.Effect<\n\tResolvedRoute,\n\tUnknownEntrypoint | RouterDisabledRouteUnsupported | RouterValidationError\n> =>\n\tEffect.gen(function* () {\n\t\tif (decl.upstream.type === 'container') {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterDisabledRouteUnsupported({\n\t\t\t\t\tendpointName: decl.endpointName,\n\t\t\t\t\tupstreamKind: 'container',\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t'router is disabled, but container upstreams are only reachable through the router network/proxy; ' +\n\t\t\t\t\t\t'use a host-loopback upstream or publish an explicit direct endpoint instead',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst port = decl.upstream.port;\n\t\tif (!Number.isInteger(port) || port <= 0 || port > 65535) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'upstreamUrl',\n\t\t\t\t\tvalue: `${directLoopbackHost}:${port}`,\n\t\t\t\t\tdetail: 'disabled-router direct host-loopback routes require a concrete TCP port 1-65535',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst entrypoint = yield* registry.byName(decl.endpointName);\n\t\tconst wireProtocol = yield* validateWireProtocolFamily(decl, entrypoint);\n\t\tconst fileId = yield* dispatchFileId({ identity, dispatch: decl.dispatchId });\n\t\tconst scheme = wireProtocol === 'tcp' ? 'tcp' : wireProtocol === 'https' ? 'https' : 'http';\n\t\treturn {\n\t\t\tdispatchFileId: fileId,\n\t\t\thostname: directLoopbackHost,\n\t\t\tentrypointName: entrypoint.name,\n\t\t\tentrypointPort: port,\n\t\t\tupstreamUrl: `${scheme}://${directLoopbackHost}:${port}`,\n\t\t\tcors: decl.wireProtocol === 'tcp' ? false : decl.cors,\n\t\t\twireProtocol,\n\t\t};\n\t});\n\nconst removeDispatchFile = (\n\tfs: FileSystem.FileSystem,\n\tprofile: RouterProfile,\n\tresolved: ResolvedRoute,\n): Effect.Effect<void> =>\n\tfs\n\t\t.remove(path.join(profile.dispatchDir, dispatchFilename(resolved.dispatchFileId)))\n\t\t.pipe(Effect.ignore);\n\nconst waitForPublicRouteReadiness = (\n\tcfg: RouterConfigShape,\n\tdecl: RoutableDecl,\n\tresolved: ResolvedRoute,\n): Effect.Effect<void, RouteReadinessProbeFailed> => {\n\tconst options = cfg.routeReadinessProbe;\n\tif (\n\t\toptions?.enabled !== true ||\n\t\tcfg.disabled ||\n\t\tresolved.wireProtocol === 'tcp' ||\n\t\tdecl.readiness === 'deferred'\n\t) {\n\t\treturn Effect.void;\n\t}\n\tconst timeoutMs = options.timeoutMs ?? DEFAULT_ROUTE_READINESS_TIMEOUT_MS;\n\tconst intervalMs = options.intervalMs ?? DEFAULT_ROUTE_READINESS_INTERVAL_MS;\n\tconst requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_ROUTE_READINESS_REQUEST_TIMEOUT_MS;\n\tconst probeUrl = `http://${directLoopbackHost}:${resolved.entrypointPort}`;\n\t// The probe only runs for non-tcp routes, so the public endpoint URL is\n\t// the http form — same derivation the boot adapter surfaces.\n\tconst publicUrl = `http://${resolved.hostname}:${resolved.entrypointPort}`;\n\treturn waitForHttpEndpoint({\n\t\tendpoint: probeUrl,\n\t\ttimeoutMs,\n\t\tintervalMs,\n\t\trequestTimeoutMs,\n\t\trequestInit: { headers: { host: resolved.hostname } },\n\t\tfetch: options.fetch ?? fetchHttpRouteViaLoopback,\n\t\tvalidate: (response) => responseHasReadyRoute(response, resolved),\n\t}).pipe(\n\t\tEffect.mapError(\n\t\t\t(cause): RouteReadinessProbeFailed =>\n\t\t\t\tnew RouteReadinessProbeFailed({\n\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\turl: publicUrl,\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`public router endpoint ${publicUrl} did not serve route ` +\n\t\t\t\t\t\t`${resolved.dispatchFileId} within ${timeoutMs}ms ` +\n\t\t\t\t\t\t`(probeUrl=${probeUrl}, hostHeader=${resolved.hostname})`,\n\t\t\t\t\tcause,\n\t\t\t\t}),\n\t\t),\n\t);\n};\n\n// ---------------------------------------------------------------------------\n// Layer — wire the orchestrator\n// ---------------------------------------------------------------------------\n\nexport const layerRouterService: Layer.Layer<\n\tRouterService,\n\tnever,\n\t| RouterConfig\n\t| IdentityContext\n\t| EntrypointRegistry\n\t| TraefikContainerOpsService\n\t| UpstreamResolverService\n\t| FileSystem.FileSystem\n> = Layer.effect(\n\tRouterService,\n\tEffect.gen(function* () {\n\t\tconst cfg = yield* RouterConfig;\n\t\tconst identity = yield* IdentityContext;\n\t\tconst registry = yield* EntrypointRegistry;\n\t\tconst upstreams = yield* UpstreamResolverService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t// Capture the Traefik ops service in the layer's outer scope so\n\t\t// `bootstrap` (which reads it from Context) doesn't surface\n\t\t// requirements through `boot()`'s return type. Provided to\n\t\t// downstream calls via `Effect.provideService`.\n\t\tconst traefikOps = yield* TraefikContainerOpsService;\n\n\t\t// Cached boot report — set on first `boot()` call.\n\t\tconst bootRef = yield* Ref.make<BootReport | null>(null);\n\n\t\t// Applied routes — subscribable for diagnostics. Empty until the\n\t\t// first `contributeRoute()`. The orchestrator publishes the\n\t\t// *resolved* shape (post URL-resolution) rather than the raw\n\t\t// decls so downstream surfaces don't have to re-resolve.\n\t\tconst applied = yield* SubscriptionRef.make<ReadonlyArray<ResolvedRoute>>([]);\n\n\t\t// ---------------------------------------------------------------\n\t\t// boot — adopt-or-create the Traefik container.\n\t\t// ---------------------------------------------------------------\n\t\tconst boot: RouterServiceShape['boot'] = () =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst profile = cfg.profile;\n\t\t\t\tif (cfg.disabled) {\n\t\t\t\t\tconst report: BootReport = {\n\t\t\t\t\t\tdecision: 'opt-out',\n\t\t\t\t\t\tcontainerId: null,\n\t\t\t\t\t\tnetworkId: null,\n\t\t\t\t\t\timageMatches: true,\n\t\t\t\t\t};\n\t\t\t\t\tyield* Ref.set(bootRef, report);\n\t\t\t\t\treturn report;\n\t\t\t\t}\n\t\t\t\tconst cached = yield* Ref.get(bootRef);\n\t\t\t\tif (cached !== null) return cached;\n\t\t\t\t// Single outer scope holds BOTH locks for the entire boot duration.\n\t\t\t\t// Acquire dispatch lock first (outer), then bootstrap lock (inner) so\n\t\t\t\t// scope finalizers release in reverse order: bootstrap lock first, then\n\t\t\t\t// dispatch lock. `protectedRouteLeaseIds` is computed under the\n\t\t\t\t// dispatch lock and consumed by `bootstrap` while still under the same\n\t\t\t\t// lock — no peer-write window can publish a new dispatch route file\n\t\t\t\t// between the scan and the bootstrap-time forceRemove decision.\n\t\t\t\t// STYLE_GUIDE §18 cross-process protocol — router boot must hold the\n\t\t\t\t// dispatch lock across the scan + bootstrap critical section, exactly\n\t\t\t\t// the same way `contributeRoute` holds it across write + probe.\n\t\t\t\tconst report = yield* Effect.scoped(\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tyield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'ensure-container',\n\t\t\t\t\t\t\t\t\t\tdetail: `failed to acquire router dispatch lock ${profile.dispatchLockFile}`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\t// Re-check after acquiring the dispatch lock: a peer fiber\n\t\t\t\t\t\t// (parallel plugin acquire) may have finished boot while we\n\t\t\t\t\t\t// waited for this lock. The O_EXCL lock serializes in-process\n\t\t\t\t\t\t// fibers too, and the winner sets `bootRef` below while still\n\t\t\t\t\t\t// holding the lock — so a non-null read here means boot is\n\t\t\t\t\t\t// done; skip the redundant bootstrap (docker inspect + decision).\n\t\t\t\t\t\tconst bootedByPeer = yield* Ref.get(bootRef);\n\t\t\t\t\t\tif (bootedByPeer !== null) return bootedByPeer;\n\t\t\t\t\t\tyield* fs.makeDirectory(profile.dispatchDir, { recursive: true }).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'write-shared-config',\n\t\t\t\t\t\t\t\t\t\tdetail: `makeDirectory(${profile.dispatchDir}) failed`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tyield* atomicWriteFile(\n\t\t\t\t\t\t\tpath.join(profile.dispatchDir, CORS_MIDDLEWARE_FILENAME),\n\t\t\t\t\t\t\tnew TextEncoder().encode(renderCorsMiddlewareYaml()),\n\t\t\t\t\t\t\t{ mode: 0o644 },\n\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'write-shared-config',\n\t\t\t\t\t\t\t\t\t\tdetail: `atomicWriteFile(${CORS_MIDDLEWARE_FILENAME}) failed at stage ${cause.stage}`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst existingDispatchScan = yield* readDispatchRouteScan(\n\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\tprofile.dispatchDir,\n\t\t\t\t\t\t\t'router-boot',\n\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'inspect',\n\t\t\t\t\t\t\t\t\t\tdetail: cause.detail,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst activeDispatchRoutes = yield* sweepStaleDispatchRoutes(\n\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\tprofile,\n\t\t\t\t\t\t\texistingDispatchScan.routes,\n\t\t\t\t\t\t\t'router-boot',\n\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'write-shared-config',\n\t\t\t\t\t\t\t\t\t\tdetail: cause.detail,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst protectedRouteLeaseIds = [\n\t\t\t\t\t\t\t...activeDispatchRoutes.map((route) => route.dispatchFileId),\n\t\t\t\t\t\t\t...existingDispatchScan.unknownRouteFileIds,\n\t\t\t\t\t\t];\n\t\t\t\t\t\tyield* acquireStackLock(profile.bootstrapLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): RouterBootFailed =>\n\t\t\t\t\t\t\t\t\tnew RouterBootFailed({\n\t\t\t\t\t\t\t\t\t\tstage: 'ensure-container',\n\t\t\t\t\t\t\t\t\t\tdetail: `failed to acquire router bootstrap lock ${profile.bootstrapLockFile}`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst booted = yield* bootstrap({\n\t\t\t\t\t\t\timage: cfg.image,\n\t\t\t\t\t\t\tentrypoints: registry.all(),\n\t\t\t\t\t\t\tprofile,\n\t\t\t\t\t\t\tprotectedRouteLeaseIds,\n\t\t\t\t\t\t}).pipe(Effect.provideService(TraefikContainerOpsService, traefikOps));\n\t\t\t\t\t\t// Publish the cached report WHILE still holding the locks so a\n\t\t\t\t\t\t// peer fiber's post-lock re-check (above) observes it — the\n\t\t\t\t\t\t// locks release at scope close, which is after this set. On\n\t\t\t\t\t\t// bootstrap failure we never reach here, so `bootRef` stays\n\t\t\t\t\t\t// null and a later boot() retries (retry-on-failure preserved).\n\t\t\t\t\t\tyield* Ref.set(bootRef, booted);\n\t\t\t\t\t\treturn booted;\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn report;\n\t\t\t});\n\n\t\t// ---------------------------------------------------------------\n\t\t// contributeRoute — resolve + write dispatch file + scope finalizer.\n\t\t// ---------------------------------------------------------------\n\t\tconst contributeRoute: RouterServiceShape['contributeRoute'] = (decl) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst profile = cfg.profile;\n\t\t\t\tconst resolved = cfg.disabled\n\t\t\t\t\t? yield* resolveDisabledDirectRoute(identity, decl, registry)\n\t\t\t\t\t: yield* resolveRoute(identity, decl, registry, upstreams);\n\t\t\t\tconst lease = makeRouteLease(profile, identity);\n\t\t\t\tconst publishRouteFile: Effect.Effect<\n\t\t\t\t\tRoutePublishOwnership,\n\t\t\t\t\tDispatchWriteFailed | RouteCollision\n\t\t\t\t> = Effect.gen(function* () {\n\t\t\t\t\t// Collision check against this process's applied set\n\t\t\t\t\t// plus files already present in the shared dispatch\n\t\t\t\t\t// directory. The dispatch lock makes the scan + write a\n\t\t\t\t\t// cross-process critical section.\n\t\t\t\t\t// WHY: disabled mode resolves direct-loopback routes with no\n\t\t\t\t\t// proxy dispatch file, so there is no shared resource a\n\t\t\t\t\t// duplicate could clobber — intentionally skip\n\t\t\t\t\t// `detectCollisions` and short-circuit before the\n\t\t\t\t\t// dispatch-dir scan/write below.\n\t\t\t\t\tif (cfg.disabled) return 'direct';\n\t\t\t\t\tif (!cfg.disabled) {\n\t\t\t\t\t\tyield* fs.makeDirectory(profile.dispatchDir, { recursive: true }).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\t\tpath: profile.dispatchDir,\n\t\t\t\t\t\t\t\t\t\tdetail: `makeDirectory(${profile.dispatchDir}) failed`,\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst currentApplied = yield* SubscriptionRef.get(applied);\n\t\t\t\t\tconst currentIds = new Set(currentApplied.map((route) => route.dispatchFileId));\n\t\t\t\t\tlet reuseLiveRoute = false;\n\t\t\t\t\tconst readScan = yield* readDispatchRouteScan(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tprofile.dispatchDir,\n\t\t\t\t\t\tresolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tconst activeDispatchRoutes = yield* sweepStaleDispatchRoutes(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tprofile,\n\t\t\t\t\t\treadScan.routes,\n\t\t\t\t\t\tresolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tconst existingSameDispatchRoute = activeDispatchRoutes.find(\n\t\t\t\t\t\t(route) => route.dispatchFileId === resolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tif (\n\t\t\t\t\t\texistingSameDispatchRoute !== undefined &&\n\t\t\t\t\t\t!currentIds.has(existingSameDispatchRoute.dispatchFileId)\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst status = yield* classifyDispatchLease(existingSameDispatchRoute);\n\t\t\t\t\t\tif (status === 'live') {\n\t\t\t\t\t\t\tif (!sameRouteSurface(existingSameDispatchRoute, resolved)) {\n\t\t\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\t\t\tliveRouteLeaseMismatch(existingSameDispatchRoute, resolved),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treuseLiveRoute = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst existingDispatchRoutes = activeDispatchRoutes.filter(\n\t\t\t\t\t\t(route) =>\n\t\t\t\t\t\t\t!currentIds.has(route.dispatchFileId) &&\n\t\t\t\t\t\t\troute.dispatchFileId !== resolved.dispatchFileId,\n\t\t\t\t\t);\n\t\t\t\t\tconst collision = detectCollisions([\n\t\t\t\t\t\t...existingDispatchRoutes,\n\t\t\t\t\t\t...currentApplied,\n\t\t\t\t\t\tresolved,\n\t\t\t\t\t]);\n\t\t\t\t\tif (collision) return yield* Effect.fail(collision);\n\t\t\t\t\tif (reuseLiveRoute) return 'reused-live';\n\n\t\t\t\t\t// Atomic write — invariant #5. tmp + rename via the\n\t\t\t\t\t// substrate's `atomicWriteFile` helper; Traefik's\n\t\t\t\t\t// watcher tolerates rename atomically.\n\t\t\t\t\tconst filePath = path.join(\n\t\t\t\t\t\tprofile.dispatchDir,\n\t\t\t\t\t\tdispatchFilename(resolved.dispatchFileId),\n\t\t\t\t\t);\n\t\t\t\t\tyield* atomicWriteFile(\n\t\t\t\t\t\tfilePath,\n\t\t\t\t\t\tnew TextEncoder().encode(renderRouteYaml(resolved, lease)),\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tmode: 0o644,\n\t\t\t\t\t\t},\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\tpath: filePath,\n\t\t\t\t\t\t\t\t\tdetail: `atomicWriteFile failed at stage ${cause.stage}`,\n\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\t\t\t);\n\t\t\t\t\treturn 'owned';\n\t\t\t\t});\n\n\t\t\t\tlet publishOwnership: RoutePublishOwnership;\n\t\t\t\tif (cfg.disabled) {\n\t\t\t\t\tpublishOwnership = yield* publishRouteFile;\n\t\t\t\t\tyield* waitForPublicRouteReadiness(cfg, decl, resolved);\n\t\t\t\t\tif (publishOwnership !== 'reused-live') {\n\t\t\t\t\t\tyield* SubscriptionRef.update(applied, (arr) => [...arr, resolved]);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// The dispatch lock MUST be held across (a) the on-disk\n\t\t\t\t\t// scan + write inside `publishRouteFile`, (b) the\n\t\t\t\t\t// readiness probe, AND (c) the in-process\n\t\t\t\t\t// `SubscriptionRef.update(applied, …)` that publishes the\n\t\t\t\t\t// new route to peer fibers in this process. Releasing\n\t\t\t\t\t// the lock before the SubscriptionRef update would let a\n\t\t\t\t\t// concurrent in-process `contributeRoute` sample the\n\t\t\t\t\t// stale `applied` set at line 803 even though our\n\t\t\t\t\t// dispatch file is already on disk — the on-disk scan\n\t\t\t\t\t// covers cross-process visibility but the lock must also\n\t\t\t\t\t// gate the in-process publish so the two views agree.\n\t\t\t\t\t// See STYLE_GUIDE §18 cross-process protocol — router\n\t\t\t\t\t// contributeRoute serialization rule.\n\t\t\t\t\tpublishOwnership = yield* Effect.scoped(\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tyield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(\n\t\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t\t(cause): DispatchWriteFailed =>\n\t\t\t\t\t\t\t\t\t\tnew DispatchWriteFailed({\n\t\t\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\t\t\tpath: profile.dispatchLockFile,\n\t\t\t\t\t\t\t\t\t\t\tdetail: `failed to acquire dispatch lock ${profile.dispatchLockFile}`,\n\t\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst ownership = yield* publishRouteFile;\n\t\t\t\t\t\t\tyield* waitForPublicRouteReadiness(cfg, decl, resolved).pipe(\n\t\t\t\t\t\t\t\tEffect.onError(() =>\n\t\t\t\t\t\t\t\t\townership !== 'owned' ? Effect.void : removeDispatchFile(fs, profile, resolved),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (ownership !== 'reused-live') {\n\t\t\t\t\t\t\t\tyield* SubscriptionRef.update(applied, (arr) => [...arr, resolved]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn ownership;\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (publishOwnership !== 'reused-live') {\n\t\t\t\t\t// Scope finalizer — remove the file + drop from applied\n\t\t\t\t\t// when the caller's scope closes. Best-effort: \"already\n\t\t\t\t\t// gone\" is fine, but lock contention / IO failures surface\n\t\t\t\t\t// via logWarning so leaked dispatch files don't go silent\n\t\t\t\t\t// (STYLE_GUIDE §18 — `Effect.ignore` on `acquireStackLock`\n\t\t\t\t\t// without a tap is forbidden).\n\t\t\t\t\tyield* Effect.addFinalizer(() =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tif (publishOwnership === 'owned') {\n\t\t\t\t\t\t\t\tyield* Effect.scoped(\n\t\t\t\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\t\t\t\tyield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS);\n\t\t\t\t\t\t\t\t\t\tyield* removeDispatchFile(fs, profile, resolved);\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t).pipe(\n\t\t\t\t\t\t\t\t\tlogWarningAndIgnore('router scope-close cleanup failed', {\n\t\t\t\t\t\t\t\t\t\tdispatchFileId: resolved.dispatchFileId,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tyield* SubscriptionRef.update(applied, (arr) =>\n\t\t\t\t\t\t\t\tarr.filter((r) => r.dispatchFileId !== resolved.dispatchFileId),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\treturn resolved;\n\t\t\t});\n\n\t\treturn RouterService.of({ boot, contributeRoute, applied });\n\t}),\n);\n\n// ---------------------------------------------------------------------------\n// Helpers for tests + composition\n// ---------------------------------------------------------------------------\n\n// Re-export for ergonomics — callers reach for the orchestrator\n// without having to spell out the sibling module paths.\nexport { type ResolvedRoute, type UpstreamResolver } from './file-provider.ts';\nexport type { BootReport } from './traefik-container.ts';\nexport type { UpstreamResolveTimeout } from './errors.ts';\nexport type { Identity } from '../../substrate/identity.ts';\nexport type { RoutableDecl } from '../../contracts/routable.ts';\n"],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ import { Effect, Scope } from "effect";
|
|
|
20
20
|
declare const sui: <const O extends SuiOptions = {
|
|
21
21
|
mode: "local";
|
|
22
22
|
}>(opts?: O) => Plugin<"sui", {
|
|
23
|
-
mode: "local" | "
|
|
23
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
24
24
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
25
25
|
sdk: SuiSdkShim;
|
|
26
26
|
rpcUrl: string;
|
|
@@ -51,7 +51,7 @@ declare const sui: <const O extends SuiOptions = {
|
|
|
51
51
|
declare const suiFor: ModeNamespace<{
|
|
52
52
|
local: {
|
|
53
53
|
local: <const O extends Omit<SuiLocalOptions, "mode">>(opts?: O) => Plugin<"sui", {
|
|
54
|
-
mode: "local" | "
|
|
54
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
55
55
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
56
56
|
sdk: SuiSdkShim;
|
|
57
57
|
rpcUrl: string;
|
|
@@ -70,7 +70,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
70
70
|
buildImage: ImageRef | null;
|
|
71
71
|
}, readonly []>;
|
|
72
72
|
localRpc: (opts: Omit<SuiLocalRpcOptions, "mode">) => Plugin<"sui", {
|
|
73
|
-
mode: "local" | "
|
|
73
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
74
74
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
75
75
|
sdk: SuiSdkShim;
|
|
76
76
|
rpcUrl: string;
|
|
@@ -91,7 +91,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
91
91
|
};
|
|
92
92
|
live: {
|
|
93
93
|
testnet: (opts?: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
|
|
94
|
-
mode: "local" | "
|
|
94
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
95
95
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
96
96
|
sdk: SuiSdkShim;
|
|
97
97
|
rpcUrl: string;
|
|
@@ -110,7 +110,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
110
110
|
buildImage: ImageRef | null;
|
|
111
111
|
}, readonly []>;
|
|
112
112
|
mainnet: (opts?: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
|
|
113
|
-
mode: "local" | "
|
|
113
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
114
114
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
115
115
|
sdk: SuiSdkShim;
|
|
116
116
|
rpcUrl: string;
|
|
@@ -129,7 +129,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
129
129
|
buildImage: ImageRef | null;
|
|
130
130
|
}, readonly []>;
|
|
131
131
|
devnet: (opts?: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
|
|
132
|
-
mode: "local" | "
|
|
132
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
133
133
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
134
134
|
sdk: SuiSdkShim;
|
|
135
135
|
rpcUrl: string;
|
|
@@ -148,7 +148,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
148
148
|
buildImage: ImageRef | null;
|
|
149
149
|
}, readonly []>;
|
|
150
150
|
custom: (opts: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
|
|
151
|
-
mode: "local" | "
|
|
151
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
152
152
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
153
153
|
sdk: SuiSdkShim;
|
|
154
154
|
rpcUrl: string;
|
|
@@ -169,7 +169,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
169
169
|
};
|
|
170
170
|
fork: {
|
|
171
171
|
mainnet: (opts?: Omit<SuiForkOptions, "mode" | "upstream">) => Plugin<"sui", {
|
|
172
|
-
mode: "local" | "
|
|
172
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
173
173
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
174
174
|
sdk: SuiSdkShim;
|
|
175
175
|
rpcUrl: string;
|
|
@@ -188,7 +188,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
188
188
|
buildImage: ImageRef | null;
|
|
189
189
|
}, readonly []>;
|
|
190
190
|
testnet: (opts?: Omit<SuiForkOptions, "mode" | "upstream">) => Plugin<"sui", {
|
|
191
|
-
mode: "local" | "
|
|
191
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
192
192
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
193
193
|
sdk: SuiSdkShim;
|
|
194
194
|
rpcUrl: string;
|
|
@@ -207,7 +207,7 @@ declare const suiFor: ModeNamespace<{
|
|
|
207
207
|
buildImage: ImageRef | null;
|
|
208
208
|
}, readonly []>;
|
|
209
209
|
devnet: (opts?: Omit<SuiForkOptions, "mode" | "upstream">) => Plugin<"sui", {
|
|
210
|
-
mode: "local" | "
|
|
210
|
+
mode: "local" | "live" | "fork" | "local-rpc";
|
|
211
211
|
fundingFaucetStrategy: FaucetStrategy | null;
|
|
212
212
|
sdk: SuiSdkShim;
|
|
213
213
|
rpcUrl: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { Effect, Scope } from "effect";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { setCurrentPluginPhase } from "../../substrate/runtime/current-plugin.mjs";
|
|
2
|
+
import { ProbeTimeoutError, exitCodeProbeResult, waitForProbe } from "../../substrate/runtime/probes.mjs";
|
|
3
|
+
import { HOST_GATEWAY_EXTRA_HOSTS } from "../../substrate/runtime/host-gateway.mjs";
|
|
4
|
+
import { ensureManagedContainer } from "../../substrate/runtime/managed-container.mjs";
|
|
5
|
+
import { walrusDeployMountPaths } from "./deploy-paths.mjs";
|
|
6
|
+
import { walrusPluginError } from "./errors.mjs";
|
|
7
|
+
import { Effect, Scope } from "effect";
|
|
8
|
+
//#region src/plugins/walrus/client-services.ts
|
|
9
|
+
const DEFAULT_WALRUS_CLIENT_SERVICE_PORT = 31415;
|
|
10
|
+
const DEFAULT_WALRUS_UPLOAD_RELAY_SERVICE_PORT = 3e3;
|
|
11
|
+
const WALRUS_CLIENT_SERVICE_STOP_SIGNAL = "SIGINT";
|
|
12
|
+
const WALRUS_CLIENT_CONFIG_FILE = "client_config.yaml";
|
|
13
|
+
const WALRUS_CLIENT_WALLET_FILE = "sui_client.yaml";
|
|
14
|
+
const WALRUS_CLIENT_KEYSTORE_FILE = "sui_client.keystore";
|
|
15
|
+
const walrusClientServiceContainerName = (parts) => `devstack-${parts.app}-${parts.stack}-walrus-${parts.walrusName}-${parts.role}`;
|
|
16
|
+
const walrusClientServiceConfigHash = (parts) => [
|
|
17
|
+
"walrus-client-service-v2",
|
|
18
|
+
`role=${parts.role}`,
|
|
19
|
+
parts.deployConfigHash,
|
|
20
|
+
`client=${WALRUS_CLIENT_CONFIG_FILE},${WALRUS_CLIENT_WALLET_FILE},${WALRUS_CLIENT_KEYSTORE_FILE}`,
|
|
21
|
+
`mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,
|
|
22
|
+
`port=${parts.containerPort}`,
|
|
23
|
+
`net=${parts.walrusNetworkName},${parts.suiNetworkName}`,
|
|
24
|
+
`rpc=${parts.suiRpcUrlInNetwork}`
|
|
25
|
+
].join("|");
|
|
26
|
+
const SERVICE_READY_PROBE_INTERVAL_MS = 500;
|
|
27
|
+
const walrusClientServiceReadyPath = (role) => role === "upload-relay" ? "/v1/tip-config" : "/status";
|
|
28
|
+
const startWalrusClientServices = (runtime, spec) => Effect.gen(function* () {
|
|
29
|
+
const outerScope = yield* Effect.scope;
|
|
30
|
+
const serviceStopScope = yield* Scope.fork(outerScope, "parallel");
|
|
31
|
+
const deployMount = walrusDeployMountPaths({
|
|
32
|
+
stackRoot: spec.stackRoot,
|
|
33
|
+
deployOutputDirHostPath: spec.deployHostMountPath,
|
|
34
|
+
mountTarget: "/opt/walrus/runtime"
|
|
35
|
+
});
|
|
36
|
+
const readyTimeout = spec.readyTimeoutMs ?? 6e4;
|
|
37
|
+
const stopGraceSeconds = spec.stopGraceSeconds ?? 10;
|
|
38
|
+
const startOne = (role, image, options) => Effect.gen(function* () {
|
|
39
|
+
const containerName = walrusClientServiceContainerName({
|
|
40
|
+
app: spec.app,
|
|
41
|
+
stack: spec.stack,
|
|
42
|
+
walrusName: spec.walrusName,
|
|
43
|
+
role
|
|
44
|
+
});
|
|
45
|
+
yield* setCurrentPluginPhase(`creating Walrus ${role} container ${containerName}`);
|
|
46
|
+
const ensureService = ensureManagedContainer({
|
|
47
|
+
runtime,
|
|
48
|
+
identity: {
|
|
49
|
+
app: spec.app,
|
|
50
|
+
stack: spec.stack
|
|
51
|
+
},
|
|
52
|
+
plugin: "walrus",
|
|
53
|
+
role,
|
|
54
|
+
spec: {
|
|
55
|
+
name: containerName,
|
|
56
|
+
image,
|
|
57
|
+
recreate: "on-config-change",
|
|
58
|
+
configHash: walrusClientServiceConfigHash({
|
|
59
|
+
role,
|
|
60
|
+
deployConfigHash: spec.deployConfigHash,
|
|
61
|
+
deploySourceHostPath: deployMount.sourceHostPath,
|
|
62
|
+
deployMountTarget: deployMount.mountTarget,
|
|
63
|
+
containerPort: options.port,
|
|
64
|
+
walrusNetworkName: spec.walrusNetworkName,
|
|
65
|
+
suiNetworkName: spec.suiNetworkName,
|
|
66
|
+
suiRpcUrlInNetwork: spec.suiRpcUrlInNetwork
|
|
67
|
+
}),
|
|
68
|
+
env: {
|
|
69
|
+
DEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer,
|
|
70
|
+
SUI_RPC_URL: spec.suiRpcUrlInNetwork,
|
|
71
|
+
WALRUS_CLIENT_SERVICE_BIND_ADDRESS: `0.0.0.0:${options.port}`,
|
|
72
|
+
WALRUS_CONTAINER_PORT: String(options.port)
|
|
73
|
+
},
|
|
74
|
+
command: [role],
|
|
75
|
+
networkAttach: [spec.walrusNetworkName, spec.suiNetworkName],
|
|
76
|
+
extraHosts: HOST_GATEWAY_EXTRA_HOSTS,
|
|
77
|
+
mounts: [{
|
|
78
|
+
source: deployMount.sourceHostPath,
|
|
79
|
+
target: deployMount.mountTarget,
|
|
80
|
+
readonly: true
|
|
81
|
+
}],
|
|
82
|
+
stopGraceSeconds,
|
|
83
|
+
stopSignal: WALRUS_CLIENT_SERVICE_STOP_SIGNAL
|
|
84
|
+
},
|
|
85
|
+
mapError: (cause) => walrusPluginError(role, `walrus ${role} ensureContainer failed: ${cause.reason}: ${cause.detail}`, { cause })
|
|
86
|
+
});
|
|
87
|
+
const handle = yield* Scope.provide(ensureService, serviceStopScope);
|
|
88
|
+
yield* setCurrentPluginPhase(`waiting for Walrus ${role} HTTP status on 127.0.0.1:${options.port}`);
|
|
89
|
+
const readyPath = walrusClientServiceReadyPath(role);
|
|
90
|
+
yield* waitForProbe({
|
|
91
|
+
label: `walrus.${role}`,
|
|
92
|
+
timeoutMs: readyTimeout,
|
|
93
|
+
intervalMs: SERVICE_READY_PROBE_INTERVAL_MS,
|
|
94
|
+
probe: () => runtime.exec(handle, [
|
|
95
|
+
"sh",
|
|
96
|
+
"-c",
|
|
97
|
+
`curl -fsS --max-time 2 http://127.0.0.1:${options.port}${readyPath} >/dev/null`
|
|
98
|
+
]).pipe(Effect.map(exitCodeProbeResult))
|
|
99
|
+
}).pipe(Effect.mapError((cause) => {
|
|
100
|
+
if (cause instanceof ProbeTimeoutError) return walrusPluginError(role, `walrus ${role} never became ready within ${readyTimeout}ms (container=${containerName}, probe=http://127.0.0.1:${options.port}${readyPath}).`, { cause: cause.lastError ?? cause.lastNotReady ?? cause });
|
|
101
|
+
return walrusPluginError(role, `walrus ${role} ready-probe exec failed: ${cause.reason}: ${cause.detail}`, { cause });
|
|
102
|
+
}));
|
|
103
|
+
return {
|
|
104
|
+
role,
|
|
105
|
+
containerName,
|
|
106
|
+
containerPort: options.port
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const [aggregator, publisher, uploadRelay] = yield* Effect.all([
|
|
110
|
+
spec.options.aggregator === null || spec.images.aggregator === null ? Effect.succeed(null) : startOne("aggregator", spec.images.aggregator, spec.options.aggregator),
|
|
111
|
+
spec.options.publisher === null || spec.images.publisher === null ? Effect.succeed(null) : startOne("publisher", spec.images.publisher, spec.options.publisher),
|
|
112
|
+
spec.options.uploadRelay === null || spec.images.uploadRelay === null ? Effect.succeed(null) : startOne("upload-relay", spec.images.uploadRelay, spec.options.uploadRelay)
|
|
113
|
+
], { concurrency: "unbounded" });
|
|
114
|
+
return {
|
|
115
|
+
aggregator,
|
|
116
|
+
publisher,
|
|
117
|
+
uploadRelay
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
//#endregion
|
|
121
|
+
export { DEFAULT_WALRUS_CLIENT_SERVICE_PORT, DEFAULT_WALRUS_UPLOAD_RELAY_SERVICE_PORT, startWalrusClientServices };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=client-services.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-services.mjs","names":[],"sources":["../../../src/plugins/walrus/client-services.ts"],"sourcesContent":["// Walrus local client-service lifecycle.\n//\n// These are the release-provided `walrus aggregator` / `walrus publisher`\n// subcommands plus the standalone `walrus-upload-relay` binary, wrapped only\n// enough to feed them the local deploy outputs and make them routable through\n// devstack.\n\nimport { Effect, Scope } from 'effect';\n\nimport type {\n\tContainerHandle,\n\tContainerRuntime,\n\tImageRef,\n} from '../../contracts/container-runtime.ts';\nimport { HOST_GATEWAY_EXTRA_HOSTS } from '../../substrate/runtime/host-gateway.ts';\nimport { ensureManagedContainer } from '../../substrate/runtime/managed-container.ts';\nimport {\n\tProbeTimeoutError,\n\texitCodeProbeResult,\n\twaitForProbe,\n} from '../../substrate/runtime/probes.ts';\nimport { setCurrentPluginPhase } from '../../substrate/runtime/current-plugin.ts';\nimport { walrusDeployMountPaths } from './deploy-paths.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\n\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_PORT = 31_415;\nexport const DEFAULT_WALRUS_UPLOAD_RELAY_SERVICE_PORT = 3_000;\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_READY_TIMEOUT_MS = 60_000;\nexport const DEFAULT_WALRUS_CLIENT_SERVICE_STOP_GRACE_SECONDS = 10;\nexport const WALRUS_CLIENT_SERVICE_STOP_SIGNAL = 'SIGINT';\nexport const WALRUS_CLIENT_CONFIG_FILE = 'client_config.yaml';\nexport const WALRUS_CLIENT_WALLET_FILE = 'sui_client.yaml';\nexport const WALRUS_CLIENT_KEYSTORE_FILE = 'sui_client.keystore';\n\nexport type WalrusClientServiceRole = 'aggregator' | 'publisher' | 'upload-relay';\n\nexport interface WalrusClientServiceOptions {\n\treadonly port: number;\n}\n\nexport interface WalrusClientService {\n\treadonly role: WalrusClientServiceRole;\n\treadonly containerName: string;\n\treadonly containerPort: number;\n}\n\nexport interface WalrusClientServices {\n\treadonly aggregator: WalrusClientService | null;\n\treadonly publisher: WalrusClientService | null;\n\treadonly uploadRelay: WalrusClientService | null;\n}\n\nexport interface StartWalrusClientServicesSpec {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly images: {\n\t\treadonly aggregator: ImageRef | null;\n\t\treadonly publisher: ImageRef | null;\n\t\treadonly uploadRelay: ImageRef | null;\n\t};\n\treadonly options: {\n\t\treadonly aggregator: WalrusClientServiceOptions | null;\n\t\treadonly publisher: WalrusClientServiceOptions | null;\n\t\treadonly uploadRelay: WalrusClientServiceOptions | null;\n\t};\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\treadonly stackRoot: string;\n\treadonly deployConfigHash: string;\n\treadonly suiRpcUrlInNetwork: string;\n\treadonly stopGraceSeconds?: number;\n\treadonly readyTimeoutMs?: number;\n}\n\nexport const walrusClientServiceContainerName = (parts: {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\treadonly role: WalrusClientServiceRole;\n}): string => `devstack-${parts.app}-${parts.stack}-walrus-${parts.walrusName}-${parts.role}`;\n\nexport const walrusClientServiceConfigHash = (parts: {\n\treadonly role: WalrusClientServiceRole;\n\treadonly deployConfigHash: string;\n\treadonly deploySourceHostPath: string;\n\treadonly deployMountTarget: string;\n\treadonly containerPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly suiRpcUrlInNetwork: string;\n}): string =>\n\t[\n\t\t'walrus-client-service-v2',\n\t\t`role=${parts.role}`,\n\t\tparts.deployConfigHash,\n\t\t`client=${WALRUS_CLIENT_CONFIG_FILE},${WALRUS_CLIENT_WALLET_FILE},${WALRUS_CLIENT_KEYSTORE_FILE}`,\n\t\t`mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,\n\t\t`port=${parts.containerPort}`,\n\t\t`net=${parts.walrusNetworkName},${parts.suiNetworkName}`,\n\t\t`rpc=${parts.suiRpcUrlInNetwork}`,\n\t].join('|');\n\nconst SERVICE_READY_PROBE_INTERVAL_MS = 500;\n\nconst walrusClientServiceReadyPath = (role: WalrusClientServiceRole): string =>\n\trole === 'upload-relay' ? '/v1/tip-config' : '/status';\n\nexport const startWalrusClientServices = (\n\truntime: ContainerRuntime,\n\tspec: StartWalrusClientServicesSpec,\n): Effect.Effect<WalrusClientServices, WalrusPluginError, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tconst outerScope = yield* Effect.scope;\n\t\tconst serviceStopScope = yield* Scope.fork(outerScope, 'parallel');\n\t\tconst deployMount = walrusDeployMountPaths({\n\t\t\tstackRoot: spec.stackRoot,\n\t\t\tdeployOutputDirHostPath: spec.deployHostMountPath,\n\t\t\tmountTarget: '/opt/walrus/runtime',\n\t\t});\n\t\tconst readyTimeout = spec.readyTimeoutMs ?? DEFAULT_WALRUS_CLIENT_SERVICE_READY_TIMEOUT_MS;\n\t\tconst stopGraceSeconds =\n\t\t\tspec.stopGraceSeconds ?? DEFAULT_WALRUS_CLIENT_SERVICE_STOP_GRACE_SECONDS;\n\n\t\tconst startOne = (\n\t\t\trole: WalrusClientServiceRole,\n\t\t\timage: ImageRef,\n\t\t\toptions: WalrusClientServiceOptions,\n\t\t): Effect.Effect<WalrusClientService, WalrusPluginError, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst containerName = walrusClientServiceContainerName({\n\t\t\t\t\tapp: spec.app,\n\t\t\t\t\tstack: spec.stack,\n\t\t\t\t\twalrusName: spec.walrusName,\n\t\t\t\t\trole,\n\t\t\t\t});\n\n\t\t\t\tyield* setCurrentPluginPhase(`creating Walrus ${role} container ${containerName}`);\n\t\t\t\tconst ensureService: Effect.Effect<ContainerHandle, WalrusPluginError, Scope.Scope> =\n\t\t\t\t\tensureManagedContainer<WalrusPluginError>({\n\t\t\t\t\t\truntime,\n\t\t\t\t\t\tidentity: { app: spec.app, stack: spec.stack },\n\t\t\t\t\t\tplugin: 'walrus',\n\t\t\t\t\t\trole,\n\t\t\t\t\t\tspec: {\n\t\t\t\t\t\t\tname: containerName,\n\t\t\t\t\t\t\timage,\n\t\t\t\t\t\t\trecreate: 'on-config-change',\n\t\t\t\t\t\t\tconfigHash: walrusClientServiceConfigHash({\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\tdeployConfigHash: spec.deployConfigHash,\n\t\t\t\t\t\t\t\tdeploySourceHostPath: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\tdeployMountTarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\tcontainerPort: options.port,\n\t\t\t\t\t\t\t\twalrusNetworkName: spec.walrusNetworkName,\n\t\t\t\t\t\t\t\tsuiNetworkName: spec.suiNetworkName,\n\t\t\t\t\t\t\t\tsuiRpcUrlInNetwork: spec.suiRpcUrlInNetwork,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tenv: {\n\t\t\t\t\t\t\t\tDEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer,\n\t\t\t\t\t\t\t\tSUI_RPC_URL: spec.suiRpcUrlInNetwork,\n\t\t\t\t\t\t\t\tWALRUS_CLIENT_SERVICE_BIND_ADDRESS: `0.0.0.0:${options.port}`,\n\t\t\t\t\t\t\t\tWALRUS_CONTAINER_PORT: String(options.port),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcommand: [role],\n\t\t\t\t\t\t\tnetworkAttach: [spec.walrusNetworkName, spec.suiNetworkName],\n\t\t\t\t\t\t\textraHosts: HOST_GATEWAY_EXTRA_HOSTS,\n\t\t\t\t\t\t\tmounts: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tsource: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\t\ttarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\t\treadonly: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tstopGraceSeconds,\n\t\t\t\t\t\t\tstopSignal: WALRUS_CLIENT_SERVICE_STOP_SIGNAL,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmapError: (cause) =>\n\t\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\t`walrus ${role} ensureContainer failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t});\n\t\t\t\tconst handle = yield* Scope.provide(ensureService, serviceStopScope);\n\n\t\t\t\tyield* setCurrentPluginPhase(\n\t\t\t\t\t`waiting for Walrus ${role} HTTP status on 127.0.0.1:${options.port}`,\n\t\t\t\t);\n\t\t\t\tconst readyPath = walrusClientServiceReadyPath(role);\n\t\t\t\tyield* waitForProbe({\n\t\t\t\t\tlabel: `walrus.${role}`,\n\t\t\t\t\ttimeoutMs: readyTimeout,\n\t\t\t\t\tintervalMs: SERVICE_READY_PROBE_INTERVAL_MS,\n\t\t\t\t\tprobe: () =>\n\t\t\t\t\t\truntime\n\t\t\t\t\t\t\t.exec(handle, [\n\t\t\t\t\t\t\t\t'sh',\n\t\t\t\t\t\t\t\t'-c',\n\t\t\t\t\t\t\t\t`curl -fsS --max-time 2 http://127.0.0.1:${options.port}${readyPath} >/dev/null`,\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.pipe(Effect.map(exitCodeProbeResult)),\n\t\t\t\t}).pipe(\n\t\t\t\t\tEffect.mapError((cause) => {\n\t\t\t\t\t\tif (cause instanceof ProbeTimeoutError) {\n\t\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t\t`walrus ${role} never became ready within ${readyTimeout}ms ` +\n\t\t\t\t\t\t\t\t\t`(container=${containerName}, probe=http://127.0.0.1:${options.port}${readyPath}).`,\n\t\t\t\t\t\t\t\t{ cause: cause.lastError ?? cause.lastNotReady ?? cause },\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\trole,\n\t\t\t\t\t\t\t`walrus ${role} ready-probe exec failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\treturn { role, containerName, containerPort: options.port };\n\t\t\t});\n\n\t\tconst [aggregator, publisher, uploadRelay] = yield* Effect.all(\n\t\t\t[\n\t\t\t\tspec.options.aggregator === null || spec.images.aggregator === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('aggregator', spec.images.aggregator, spec.options.aggregator),\n\t\t\t\tspec.options.publisher === null || spec.images.publisher === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('publisher', spec.images.publisher, spec.options.publisher),\n\t\t\t\tspec.options.uploadRelay === null || spec.images.uploadRelay === null\n\t\t\t\t\t? Effect.succeed(null)\n\t\t\t\t\t: startOne('upload-relay', spec.images.uploadRelay, spec.options.uploadRelay),\n\t\t\t],\n\t\t\t{ concurrency: 'unbounded' },\n\t\t);\n\n\t\treturn { aggregator, publisher, uploadRelay };\n\t});\n"],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ interface WalrusBindings {
|
|
|
20
20
|
readonly proxyUrl: string | null;
|
|
21
21
|
readonly aggregatorUrl: string | null;
|
|
22
22
|
readonly publisherUrl: string | null;
|
|
23
|
+
readonly uploadRelayUrl: string | null;
|
|
23
24
|
readonly nodes: ReadonlyArray<WalrusNodeBinding>;
|
|
24
25
|
}
|
|
25
26
|
//#endregion
|
|
@@ -75,6 +75,7 @@ const walrusConfigBindings = (structural) => {
|
|
|
75
75
|
field("proxyUrl", (i) => i.proxyUrl),
|
|
76
76
|
field("aggregatorUrl", (i) => i.aggregatorUrl),
|
|
77
77
|
field("publisherUrl", (i) => i.publisherUrl),
|
|
78
|
+
field("uploadRelayUrl", (i) => i.uploadRelayUrl),
|
|
78
79
|
nodesBinding
|
|
79
80
|
]
|
|
80
81
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegen.mjs","names":[],"sources":["../../../src/plugins/walrus/codegen.ts"],"sourcesContent":["// Walrus plugin — Codegenable contribution, via the UNIFIED config-binding\n// declaration.\n//\n// Walrus's contribution is the SDK-ready `packageConfig` shape that the\n// `@mysten/walrus` SDK consumes — `{systemObjectId, stakingPoolId,\n// exchangeIds}` — plus the proxy / aggregator / publisher
|
|
1
|
+
{"version":3,"file":"codegen.mjs","names":[],"sources":["../../../src/plugins/walrus/codegen.ts"],"sourcesContent":["// Walrus plugin — Codegenable contribution, via the UNIFIED config-binding\n// declaration.\n//\n// Walrus's contribution is the SDK-ready `packageConfig` shape that the\n// `@mysten/walrus` SDK consumes — `{systemObjectId, stakingPoolId,\n// exchangeIds}` — plus the proxy / aggregator / publisher / upload-relay URLs\n// for HTTP consumers. Walrus is single-instance per stack, so it exports `walrus`\n// directly (a FLAT bucket, not name-keyed like coin/seal).\n//\n// ONE declaration, TWO derivations (see `contracts/config-bindings.ts`):\n// - LIVE (boot): bakes the resolved ids / URLs into the ephemeral tree AND\n// feeds the generic deployment `values` channel.\n// - STATIC (committed tree): emits `requireValue(dep, 'walrus', '<key>')` so the\n// committed `walrus.ts` carries NO baked object id / endpoint URL.\n//\n// STRUCTURAL fields (`mode`, `network`) stay literals; the on-chain ids,\n// coin type, endpoint URLs, and the storage-node committee are RUNTIME\n// (loaded config data). The composite `packageConfig` / `nodes` values are\n// resolved as whole-value blobs (the array/optional shapes don't split into\n// the flat config-path model).\n\nimport type { CodegenableDecl } from '../../contracts/codegenable.ts';\nimport {\n\tconfigCodegenable,\n\ttype ConfigBinding,\n\ttype ConfigBindingSet,\n} from '../../contracts/config-bindings.ts';\nimport type { JsonValue } from '../../orchestrators/codegen/deployment.ts';\n\n/** Per-node descriptor. */\nexport interface WalrusNodeBinding {\n\treadonly nodeIndex: number;\n\treadonly publicHostname: string;\n\treadonly rpcUrl: string;\n}\n\n/** The typed shape the emitted file exports. */\nexport interface WalrusBindings {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly packageConfig: {\n\t\treadonly systemObjectId: string;\n\t\treadonly stakingPoolId: string;\n\t\treadonly exchangeIds?: ReadonlyArray<string>;\n\t};\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly uploadRelayUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** Inputs to the LIVE codegen contribution — supplied at acquire-time. */\nexport interface MakeCodegenableInputs {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly systemObjectId: string;\n\treadonly stakingPoolId: string;\n\treadonly exchangeIds: ReadonlyArray<string>;\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly uploadRelayUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** User-declared known-deployment ids / URLs, available at factory time. A\n * KNOWN deployment's values are DECLARED config (not loaded-at-runtime data),\n * so the committed `walrus.ts` bakes them as LITERALS. Absent for a `local`\n * (dev-deployed) cluster whose ids/URLs are dynamic. */\nexport interface WalrusKnownConfig {\n\treadonly walrusPackageId: string | null;\n\treadonly walPackageId: string | null;\n\treadonly walCoinType: string | null;\n\treadonly packageConfig: WalrusBindings['packageConfig'];\n\treadonly proxyUrl: string | null;\n\treadonly aggregatorUrl: string | null;\n\treadonly publisherUrl: string | null;\n\treadonly uploadRelayUrl: string | null;\n\treadonly nodes: ReadonlyArray<WalrusNodeBinding>;\n}\n\n/** Static-config shape — what walrus knows BEFORE acquire. When `known` is\n * present (known mode), its declared ids/URLs are baked as literals;\n * otherwise (local mode) every id/URL resolves at app build/dev time. */\nexport interface WalrusStaticConfig {\n\treadonly mode: 'local' | 'known';\n\treadonly network: string;\n\treadonly known?: WalrusKnownConfig;\n}\n\nconst NAMESPACE = 'walrus';\n\n/** TS source-type strings for the resolved walrus fields — keeps the committed\n * `walrus.ts` typed as `WalrusBindings` declares (the generic `requireValue`\n * channel would otherwise return `unknown`). Composite blobs inline their\n * structural literal types so no emitted type-import is needed. */\nconst PACKAGE_CONFIG_TS_TYPE =\n\t'{ readonly systemObjectId: string; readonly stakingPoolId: string; readonly exchangeIds?: ReadonlyArray<string> }';\nconst NODES_TS_TYPE =\n\t'ReadonlyArray<{ readonly nodeIndex: number; readonly publicHostname: string; readonly rpcUrl: string }>';\n\n/** The walrus config bindings, declared ONCE. `mode` / `network` are\n * structural literals; every id / coin type / URL / committee value is a\n * RESOLVED binding on the generic `requireValue(dep, 'walrus', '<key>')` channel.\n * Both the live boot decl and the static committed-tree decl derive from it. */\nconst walrusConfigBindings = (\n\tstructural: WalrusStaticConfig,\n): ConfigBindingSet<MakeCodegenableInputs> => {\n\tconst known = structural.known;\n\t// A known deployment's declared ids/URLs are config (literal); a local\n\t// cluster's are dynamically deployed (resolved at app build/dev time).\n\tconst field = (\n\t\tkey:\n\t\t\t| 'walrusPackageId'\n\t\t\t| 'walPackageId'\n\t\t\t| 'walCoinType'\n\t\t\t| 'proxyUrl'\n\t\t\t| 'aggregatorUrl'\n\t\t\t| 'publisherUrl'\n\t\t\t| 'uploadRelayUrl',\n\t\tlive: (i: MakeCodegenableInputs) => JsonValue,\n\t): ConfigBinding<MakeCodegenableInputs> =>\n\t\tknown !== undefined\n\t\t\t? { variant: 'literal', configPath: [key], value: known[key] }\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: [key],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey,\n\t\t\t\t\ttsType: 'string | null',\n\t\t\t\t\tlive,\n\t\t\t\t};\n\tconst packageConfigBinding: ConfigBinding<MakeCodegenableInputs> =\n\t\tknown !== undefined\n\t\t\t? {\n\t\t\t\t\tvariant: 'literal',\n\t\t\t\t\tconfigPath: ['packageConfig'],\n\t\t\t\t\tvalue: known.packageConfig as JsonValue,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: ['packageConfig'],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey: 'packageConfig',\n\t\t\t\t\ttsType: PACKAGE_CONFIG_TS_TYPE,\n\t\t\t\t\tlive: (i) =>\n\t\t\t\t\t\t({\n\t\t\t\t\t\t\tsystemObjectId: i.systemObjectId,\n\t\t\t\t\t\t\tstakingPoolId: i.stakingPoolId,\n\t\t\t\t\t\t\t...(i.exchangeIds.length > 0 ? { exchangeIds: [...i.exchangeIds] } : {}),\n\t\t\t\t\t\t}) as JsonValue,\n\t\t\t\t};\n\tconst nodesBinding: ConfigBinding<MakeCodegenableInputs> =\n\t\tknown !== undefined\n\t\t\t? { variant: 'literal', configPath: ['nodes'], value: known.nodes as unknown as JsonValue }\n\t\t\t: {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath: ['nodes'],\n\t\t\t\t\tnamespace: NAMESPACE,\n\t\t\t\t\tkey: 'nodes',\n\t\t\t\t\ttsType: NODES_TS_TYPE,\n\t\t\t\t\tlive: (i) => i.nodes as unknown as JsonValue,\n\t\t\t\t};\n\tconst bindings: ReadonlyArray<ConfigBinding<MakeCodegenableInputs>> = [\n\t\t{ variant: 'literal', configPath: ['mode'], value: structural.mode },\n\t\t{ variant: 'literal', configPath: ['network'], value: structural.network },\n\t\tfield('walrusPackageId', (i) => i.walrusPackageId),\n\t\tfield('walPackageId', (i) => i.walPackageId),\n\t\tfield('walCoinType', (i) => i.walCoinType),\n\t\tpackageConfigBinding,\n\t\tfield('proxyUrl', (i) => i.proxyUrl),\n\t\tfield('aggregatorUrl', (i) => i.aggregatorUrl),\n\t\tfield('publisherUrl', (i) => i.publisherUrl),\n\t\tfield('uploadRelayUrl', (i) => i.uploadRelayUrl),\n\t\tnodesBinding,\n\t];\n\treturn {\n\t\tbucket: 'walrus.ts',\n\t\tkind: 'walrus',\n\t\temitterName: 'walrus-network',\n\t\tbindings,\n\t};\n};\n\n/** Construct the LIVE Codegenable contribution. Bakes the resolved ids /\n * URLs into the ephemeral tree + feeds the generic deployment `values`\n * channel. */\nexport const makeCodegenable = (inputs: MakeCodegenableInputs): CodegenableDecl =>\n\tconfigCodegenable(walrusConfigBindings({ mode: inputs.mode, network: inputs.network }), {\n\t\tmode: 'live',\n\t\tstate: inputs,\n\t});\n\n/** Construct the STATIC (stack-free) Codegenable contribution. Emits\n * `requireValue(dep, 'walrus', '<key>')` for the runtime fields; the committed\n * `walrus.ts` carries no baked object id / endpoint URL. */\nexport const makeWalrusStaticCodegen = (config: WalrusStaticConfig): CodegenableDecl =>\n\tconfigCodegenable(walrusConfigBindings(config), 'static');\n"],"mappings":";;AAiGA,MAAM,YAAY;;;;;AAMlB,MAAM,yBACL;AACD,MAAM,gBACL;;;;;AAMD,MAAM,wBACL,eAC6C;CAC7C,MAAM,QAAQ,WAAW;CAGzB,MAAM,SACL,KAQA,SAEA,UAAU,KAAA,IACP;EAAE,SAAS;EAAW,YAAY,CAAC,GAAG;EAAG,OAAO,MAAM;CAAK,IAC3D;EACA,SAAS;EACT,YAAY,CAAC,GAAG;EAChB,WAAW;EACX;EACA,QAAQ;EACR;CACD;CACH,MAAM,uBACL,UAAU,KAAA,IACP;EACA,SAAS;EACT,YAAY,CAAC,eAAe;EAC5B,OAAO,MAAM;CACd,IACC;EACA,SAAS;EACT,YAAY,CAAC,eAAe;EAC5B,WAAW;EACX,KAAK;EACL,QAAQ;EACR,OAAO,OACL;GACA,gBAAgB,EAAE;GAClB,eAAe,EAAE;GACjB,GAAI,EAAE,YAAY,SAAS,IAAI,EAAE,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC;EACvE;CACF;CACH,MAAM,eACL,UAAU,KAAA,IACP;EAAE,SAAS;EAAW,YAAY,CAAC,OAAO;EAAG,OAAO,MAAM;CAA8B,IACxF;EACA,SAAS;EACT,YAAY,CAAC,OAAO;EACpB,WAAW;EACX,KAAK;EACL,QAAQ;EACR,OAAO,MAAM,EAAE;CAChB;CAcH,OAAO;EACN,QAAQ;EACR,MAAM;EACN,aAAa;EACb,UAAA;GAhBA;IAAE,SAAS;IAAW,YAAY,CAAC,MAAM;IAAG,OAAO,WAAW;GAAK;GACnE;IAAE,SAAS;IAAW,YAAY,CAAC,SAAS;IAAG,OAAO,WAAW;GAAQ;GACzE,MAAM,oBAAoB,MAAM,EAAE,eAAe;GACjD,MAAM,iBAAiB,MAAM,EAAE,YAAY;GAC3C,MAAM,gBAAgB,MAAM,EAAE,WAAW;GACzC;GACA,MAAM,aAAa,MAAM,EAAE,QAAQ;GACnC,MAAM,kBAAkB,MAAM,EAAE,aAAa;GAC7C,MAAM,iBAAiB,MAAM,EAAE,YAAY;GAC3C,MAAM,mBAAmB,MAAM,EAAE,cAAc;GAC/C;EAMO;CACR;AACD;;;;AAKA,MAAa,mBAAmB,WAC/B,kBAAkB,qBAAqB;CAAE,MAAM,OAAO;CAAM,SAAS,OAAO;AAAQ,CAAC,GAAG;CACvF,MAAM;CACN,OAAO;AACR,CAAC;;;;AAKF,MAAa,2BAA2B,WACvC,kBAAkB,qBAAqB,MAAM,GAAG,QAAQ"}
|
|
@@ -39,11 +39,17 @@ const SuiObjectExistsShape = Schema.Struct({ objectId: Schema.String });
|
|
|
39
39
|
* Budget: 5 probes × 3s = 15s. */
|
|
40
40
|
const WALRUS_DEPLOY_VERIFY_READINESS_RETRIES = 5;
|
|
41
41
|
const WALRUS_DEPLOY_VERIFY_READINESS_DELAY = "3 seconds";
|
|
42
|
-
const requiredDeployOutputFiles = (inputs) => [
|
|
43
|
-
join(inputs.outputDirHostPath,
|
|
44
|
-
join(inputs.outputDirHostPath,
|
|
45
|
-
join(inputs.outputDirHostPath,
|
|
46
|
-
|
|
42
|
+
const requiredDeployOutputFiles = (inputs) => [
|
|
43
|
+
join(inputs.outputDirHostPath, "deploy"),
|
|
44
|
+
join(inputs.outputDirHostPath, "client_config.yaml"),
|
|
45
|
+
join(inputs.outputDirHostPath, "sui_client.yaml"),
|
|
46
|
+
join(inputs.outputDirHostPath, "sui_client.keystore"),
|
|
47
|
+
...Array.from({ length: inputs.committeeSize }, (_, nodeIndex) => [
|
|
48
|
+
join(inputs.outputDirHostPath, `dryrun-node-${nodeIndex}.yaml`),
|
|
49
|
+
join(inputs.outputDirHostPath, `dryrun-node-${nodeIndex}-sui.yaml`),
|
|
50
|
+
join(inputs.outputDirHostPath, `dryrun-node-${nodeIndex}.keystore`)
|
|
51
|
+
]).flat()
|
|
52
|
+
];
|
|
47
53
|
const deployOutputFilesComplete = (inputs) => Effect.tryPromise({
|
|
48
54
|
try: async () => {
|
|
49
55
|
await Promise.all(requiredDeployOutputFiles(inputs).map((file) => access(file)));
|