@mysten-incubation/devstack 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- 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 +3 -3
- 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/deepbook/index.d.mts +36 -6
- package/dist/plugins/deepbook/index.mjs +52 -16
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/seal/codegen.d.mts +5 -0
- package/dist/plugins/seal/codegen.mjs +53 -10
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/index.d.mts +64 -19
- package/dist/plugins/seal/index.mjs +26 -36
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/seal/mode/fork-known.d.mts +0 -2
- package/dist/plugins/seal/mode/fork-known.mjs +11 -22
- package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
- package/dist/plugins/seal/mode/live.d.mts +36 -13
- package/dist/plugins/seal/mode/live.mjs +104 -43
- package/dist/plugins/seal/mode/live.mjs.map +1 -1
- package/dist/plugins/seal/mode/local-keygen.mjs +2 -1
- package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
- package/dist/plugins/seal/registry-publish.d.mts +15 -3
- package/dist/plugins/seal/registry-publish.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 +115 -0
- package/dist/plugins/walrus/client-services.mjs.map +1 -0
- 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 +138 -9
- package/dist/plugins/walrus/index.mjs +72 -20
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.d.mts +5 -1
- package/dist/plugins/walrus/mode/known-deploy.mjs +14 -11
- package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
- package/dist/plugins/walrus/mode/local-cluster.d.mts +14 -1
- package/dist/plugins/walrus/mode/local-cluster.mjs +54 -12
- package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
- package/dist/plugins/walrus/routable.mjs +12 -15
- 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 +17 -4
- package/images/walrus/deploy-walrus.sh +7 -4
- package/images/walrus/run-walrus-client-service.sh +103 -0
- package/package.json +2 -2
- package/dist/plugins/seal/service.mjs +0 -17
- package/dist/plugins/seal/service.mjs.map +0 -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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"}
|
|
@@ -94,6 +94,11 @@ interface DeepbookKnownExplicitOptions extends DeepbookKnownCommonOptions {
|
|
|
94
94
|
readonly network?: DeepbookKnownNetwork;
|
|
95
95
|
}
|
|
96
96
|
type DeepbookKnownOptions = DeepbookKnownNetworkOptions | DeepbookKnownExplicitOptions;
|
|
97
|
+
/** Per-network factory options for `deepbookFor(net).testnet()`/`.mainnet()`.
|
|
98
|
+
* The `network` is injected by the namespace method, so the caller only
|
|
99
|
+
* supplies optional per-field overrides (`packageId`/`registryId`/`name`).
|
|
100
|
+
* Plugin-INTERNAL — deliberately NOT re-exported from `src/index.ts`. */
|
|
101
|
+
type DeepbookKnownByNetworkOptions = Omit<DeepbookKnownNetworkOptions, 'network'>;
|
|
97
102
|
type DeepbookOptions<Publisher extends AccountMemberAlias = AccountMemberAlias, Pyth extends PythOptions | undefined = PythOptions | undefined> = ({
|
|
98
103
|
readonly mode: 'local';
|
|
99
104
|
} & DeepbookLocalOptions<Publisher, DeepbookPackageMember, ReadonlyArray<DeepbookPoolSpec>, Pyth>) | ({
|
|
@@ -134,14 +139,21 @@ declare function deepbookCore<const Publisher extends AccountMemberAlias, const
|
|
|
134
139
|
* const local = { mode: 'local', network: 'localnet' } as const;
|
|
135
140
|
* deepbookFor(local).local({publisher, package, pools}) // OK
|
|
136
141
|
* deepbookFor(local).override({packageId, registryId, adminCapId}) // OK
|
|
137
|
-
* deepbookFor(local).
|
|
142
|
+
* deepbookFor(local).testnet() // OK (known testnet)
|
|
143
|
+
* deepbookFor(local).known({packageId, registryId}) // OK (raw-id override)
|
|
144
|
+
*
|
|
145
|
+
* const live = { mode: 'live', chainId: 'sui:testnet' } as const;
|
|
146
|
+
* deepbookFor(live).mainnet() // OK (known mainnet)
|
|
138
147
|
*
|
|
139
148
|
* const fork = { mode: 'fork', network: 'mainnet-fork', upstream: 'mainnet' } as const;
|
|
140
149
|
* deepbookFor(fork).local({...}) // COMPILE ERROR
|
|
141
150
|
* deepbookFor(fork).override({...}) // COMPILE ERROR
|
|
142
151
|
*
|
|
143
|
-
*
|
|
144
|
-
* is
|
|
152
|
+
* Per-network methods (`.testnet`/`.mainnet`) inject the network into
|
|
153
|
+
* `buildKnownPlugin`; `.known(...)` is now the raw-id explicit-override form
|
|
154
|
+
* only (`DeepbookKnownExplicitOptions`) — the `network`-in-`.known()` path was
|
|
155
|
+
* HARD CUT. The fork branch has NO `.local` or `.override` entry —
|
|
156
|
+
* `deepbookFor(forkNetwork).local` is a compile-time refusal. */
|
|
145
157
|
declare const deepbookFor: ModeNamespace<{
|
|
146
158
|
local: {
|
|
147
159
|
local: <const Publisher extends AccountMemberAlias, const Package extends DeepbookPackageMember, const Pools extends ReadonlyArray<DeepbookPoolSpec> = readonly [], const Pyth extends PythOptions | undefined = undefined>(opts: DeepbookLocalOptions<Publisher, Package, Pools, Pyth>) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
@@ -150,17 +162,35 @@ declare const deepbookFor: ModeNamespace<{
|
|
|
150
162
|
override: (opts: DeepbookOverrideOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
151
163
|
readonly mode: SuiOptions["mode"];
|
|
152
164
|
}>]>;
|
|
153
|
-
known: (opts:
|
|
165
|
+
known: (opts: DeepbookKnownExplicitOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
166
|
+
readonly mode: SuiOptions["mode"];
|
|
167
|
+
}>]>;
|
|
168
|
+
testnet: (opts?: DeepbookKnownByNetworkOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
169
|
+
readonly mode: SuiOptions["mode"];
|
|
170
|
+
}>]>;
|
|
171
|
+
mainnet: (opts?: DeepbookKnownByNetworkOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
154
172
|
readonly mode: SuiOptions["mode"];
|
|
155
173
|
}>]>;
|
|
156
174
|
};
|
|
157
175
|
live: {
|
|
158
|
-
known: (opts:
|
|
176
|
+
known: (opts: DeepbookKnownExplicitOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
177
|
+
readonly mode: SuiOptions["mode"];
|
|
178
|
+
}>]>;
|
|
179
|
+
testnet: (opts?: DeepbookKnownByNetworkOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
180
|
+
readonly mode: SuiOptions["mode"];
|
|
181
|
+
}>]>;
|
|
182
|
+
mainnet: (opts?: DeepbookKnownByNetworkOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
159
183
|
readonly mode: SuiOptions["mode"];
|
|
160
184
|
}>]>;
|
|
161
185
|
};
|
|
162
186
|
fork: {
|
|
163
|
-
known: (opts:
|
|
187
|
+
known: (opts: DeepbookKnownExplicitOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
188
|
+
readonly mode: SuiOptions["mode"];
|
|
189
|
+
}>]>;
|
|
190
|
+
testnet: (opts?: DeepbookKnownByNetworkOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
191
|
+
readonly mode: SuiOptions["mode"];
|
|
192
|
+
}>]>;
|
|
193
|
+
mainnet: (opts?: DeepbookKnownByNetworkOptions) => Plugin<`deepbook/${string}`, DeepbookResolved, readonly [ResourceRef<"sui", SuiClient & {
|
|
164
194
|
readonly mode: SuiOptions["mode"];
|
|
165
195
|
}>]>;
|
|
166
196
|
};
|
|
@@ -19,31 +19,32 @@ import { createDeepbookPools, seedDeepbookPools } from "./deploy.mjs";
|
|
|
19
19
|
import "./types.mjs";
|
|
20
20
|
import { initLocalPythFeeds } from "./pyth/index.mjs";
|
|
21
21
|
import { Effect } from "effect";
|
|
22
|
+
import { mainnetPackageIds, mainnetPythConfigs, testnetPackageIds, testnetPythConfigs } from "@mysten/deepbook-v3";
|
|
22
23
|
//#region src/plugins/deepbook/index.ts
|
|
23
24
|
const makeDeepbookResource = (name) => resource(`deepbook/${name}`);
|
|
24
25
|
const DEFAULT_NAME = "deepbook";
|
|
25
26
|
const KNOWN_DEEPBOOK_DEPLOYMENTS = {
|
|
26
27
|
testnet: {
|
|
27
28
|
network: "testnet",
|
|
28
|
-
packageId:
|
|
29
|
-
registryId:
|
|
30
|
-
deepTreasuryId:
|
|
29
|
+
packageId: testnetPackageIds.DEEPBOOK_PACKAGE_ID,
|
|
30
|
+
registryId: testnetPackageIds.REGISTRY_ID,
|
|
31
|
+
deepTreasuryId: testnetPackageIds.DEEP_TREASURY_ID,
|
|
31
32
|
pyth: {
|
|
32
33
|
packageId: null,
|
|
33
|
-
stateId:
|
|
34
|
-
wormholeStateId:
|
|
34
|
+
stateId: testnetPythConfigs.pythStateId,
|
|
35
|
+
wormholeStateId: testnetPythConfigs.wormholeStateId,
|
|
35
36
|
feeds: []
|
|
36
37
|
}
|
|
37
38
|
},
|
|
38
39
|
mainnet: {
|
|
39
40
|
network: "mainnet",
|
|
40
|
-
packageId:
|
|
41
|
-
registryId:
|
|
42
|
-
deepTreasuryId:
|
|
41
|
+
packageId: mainnetPackageIds.DEEPBOOK_PACKAGE_ID,
|
|
42
|
+
registryId: mainnetPackageIds.REGISTRY_ID,
|
|
43
|
+
deepTreasuryId: mainnetPackageIds.DEEP_TREASURY_ID,
|
|
43
44
|
pyth: {
|
|
44
45
|
packageId: null,
|
|
45
|
-
stateId:
|
|
46
|
-
wormholeStateId:
|
|
46
|
+
stateId: mainnetPythConfigs.pythStateId,
|
|
47
|
+
wormholeStateId: mainnetPythConfigs.wormholeStateId,
|
|
47
48
|
feeds: []
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -408,22 +409,57 @@ function deepbookCore(opts) {
|
|
|
408
409
|
* const local = { mode: 'local', network: 'localnet' } as const;
|
|
409
410
|
* deepbookFor(local).local({publisher, package, pools}) // OK
|
|
410
411
|
* deepbookFor(local).override({packageId, registryId, adminCapId}) // OK
|
|
411
|
-
* deepbookFor(local).
|
|
412
|
+
* deepbookFor(local).testnet() // OK (known testnet)
|
|
413
|
+
* deepbookFor(local).known({packageId, registryId}) // OK (raw-id override)
|
|
414
|
+
*
|
|
415
|
+
* const live = { mode: 'live', chainId: 'sui:testnet' } as const;
|
|
416
|
+
* deepbookFor(live).mainnet() // OK (known mainnet)
|
|
412
417
|
*
|
|
413
418
|
* const fork = { mode: 'fork', network: 'mainnet-fork', upstream: 'mainnet' } as const;
|
|
414
419
|
* deepbookFor(fork).local({...}) // COMPILE ERROR
|
|
415
420
|
* deepbookFor(fork).override({...}) // COMPILE ERROR
|
|
416
421
|
*
|
|
417
|
-
*
|
|
418
|
-
* is
|
|
422
|
+
* Per-network methods (`.testnet`/`.mainnet`) inject the network into
|
|
423
|
+
* `buildKnownPlugin`; `.known(...)` is now the raw-id explicit-override form
|
|
424
|
+
* only (`DeepbookKnownExplicitOptions`) — the `network`-in-`.known()` path was
|
|
425
|
+
* HARD CUT. The fork branch has NO `.local` or `.override` entry —
|
|
426
|
+
* `deepbookFor(forkNetwork).local` is a compile-time refusal. */
|
|
419
427
|
const deepbookFor = defineModeNamespace({
|
|
420
428
|
local: {
|
|
421
429
|
local: (opts) => buildLocalPluginPublic(opts),
|
|
422
430
|
override: (opts) => buildOverridePlugin(opts),
|
|
423
|
-
known: (opts) => buildKnownPlugin(opts)
|
|
431
|
+
known: (opts) => buildKnownPlugin(opts),
|
|
432
|
+
testnet: (opts = {}) => buildKnownPlugin({
|
|
433
|
+
network: "testnet",
|
|
434
|
+
...opts
|
|
435
|
+
}),
|
|
436
|
+
mainnet: (opts = {}) => buildKnownPlugin({
|
|
437
|
+
network: "mainnet",
|
|
438
|
+
...opts
|
|
439
|
+
})
|
|
440
|
+
},
|
|
441
|
+
live: {
|
|
442
|
+
known: (opts) => buildKnownPlugin(opts),
|
|
443
|
+
testnet: (opts = {}) => buildKnownPlugin({
|
|
444
|
+
network: "testnet",
|
|
445
|
+
...opts
|
|
446
|
+
}),
|
|
447
|
+
mainnet: (opts = {}) => buildKnownPlugin({
|
|
448
|
+
network: "mainnet",
|
|
449
|
+
...opts
|
|
450
|
+
})
|
|
424
451
|
},
|
|
425
|
-
|
|
426
|
-
|
|
452
|
+
fork: {
|
|
453
|
+
known: (opts) => buildKnownPlugin(opts),
|
|
454
|
+
testnet: (opts = {}) => buildKnownPlugin({
|
|
455
|
+
network: "testnet",
|
|
456
|
+
...opts
|
|
457
|
+
}),
|
|
458
|
+
mainnet: (opts = {}) => buildKnownPlugin({
|
|
459
|
+
network: "mainnet",
|
|
460
|
+
...opts
|
|
461
|
+
})
|
|
462
|
+
}
|
|
427
463
|
});
|
|
428
464
|
//#endregion
|
|
429
465
|
export { deepbookCore, deepbookFor };
|