UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

1 lines • 18.6 kB
{"version":3,"file":"docker.cjs","names":["Layer","AlchemyContext","AlchemyContextLive","Effect","Context","PlatformServices","FetchHttpClient","Interaction","ArtifactStore","createArtifactStore","AdoptPolicy","AuthProviders","State","Docker","Stack","Stage","HostingAdapter","HttpClient","Duration","Clock","HttpClientRequest","HealthTimeout","Alchemy","Deploy","DeployFailed","HostingOperationFailed","InstanceNotFound"],"sources":["../../../src/services/hosting/docker.ts"],"sourcesContent":["/**\n * Docker dev-substrate `HostingAdapter` (ADR 0035).\n *\n * Alchemy is the control plane, embedded programmatically: each instance is\n * one Alchemy stack holding a `Docker.Container` resource, evaluated\n * in-process through `alchemy/Deploy`'s `deploy()` — the same plan/apply and\n * state-ledger semantics as `alchemy deploy` without a CLI subprocess. The\n * choice was deliberate: `Deploy.deploy` is a public programmatic API and\n * alchemy's own test harness (`lib/Test/Core.js`) drives stacks exactly this\n * way, so idempotency comes from Alchemy's plan diffing (an unchanged\n * descriptor yields a no-op apply and never recreates the container) and the\n * ledger is Alchemy's local file state store under `stateDir`.\n * Stop/start/inspect go through the same provider's `Docker` service, so\n * every mutation flows through the one control plane — the adapter never\n * shells `docker` itself. Docker is the development/CI substrate only;\n * Railway and Fly adapters implement the same seam.\n *\n * The host port is deliberately ephemeral (`external: 0`): Docker binds a\n * free host port at container creation, the provider reports the resolved\n * mapping in the resource output, and the instance URL derives from it. The\n * binding is stable for the container's lifetime (stop/start preserves it);\n * only a re-create — i.e. a descriptor change — rebinds it.\n */\n\nimport * as Alchemy from \"alchemy\";\nimport { AlchemyContext, AlchemyContextLive, AuthProviders, Stack, Stage } from \"alchemy\";\nimport { AdoptPolicy } from \"alchemy/AdoptPolicy\";\nimport { ArtifactStore, createArtifactStore } from \"alchemy/Artifacts\";\nimport * as Deploy from \"alchemy/Deploy\";\nimport * as Docker from \"alchemy/Docker\";\nimport * as Interaction from \"alchemy/Interaction\";\nimport * as State from \"alchemy/State\";\nimport { PlatformServices } from \"alchemy/Util/PlatformServices\";\nimport { Clock, Context, Duration, Effect, Layer } from \"effect\";\nimport * as FetchHttpClient from \"effect/unstable/http/FetchHttpClient\";\nimport * as HttpClient from \"effect/unstable/http/HttpClient\";\nimport * as HttpClientRequest from \"effect/unstable/http/HttpClientRequest\";\nimport {\n type DeployedInstance,\n DeployFailed,\n HealthTimeout,\n HostingAdapter,\n HostingOperationFailed,\n InstanceNotFound,\n type InstanceSnapshot,\n type RuntimeDescriptor,\n} from \"./adapter\";\n\nexport interface DockerHostingOptions {\n /** Container port the runtime serves HTTP on. */\n readonly containerPort?: number | undefined;\n /** Path polled for readiness after deploy/restart. */\n readonly healthPath?: string | undefined;\n /** Bounded window for the readiness poll. */\n readonly healthTimeout?: Duration.Input | undefined;\n /** Interval between readiness polls. */\n readonly healthInterval?: Duration.Input | undefined;\n /** Alchemy state ledger directory (defaults to `<cwd>/.alchemy`). */\n readonly stateDir?: string | undefined;\n /** Adopt pre-existing containers instead of failing on conflicts. */\n readonly adopt?: boolean | undefined;\n /** Alchemy stage scoping the state ledger. */\n readonly stage?: string | undefined;\n}\n\nconst DEFAULT_CONTAINER_PORT = 3000;\nconst DEFAULT_HEALTH_PATH = \"/health\";\nconst DEFAULT_HEALTH_TIMEOUT: Duration.Input = \"90 seconds\";\nconst DEFAULT_HEALTH_INTERVAL: Duration.Input = \"500 millis\";\nconst DEFAULT_STAGE = \"local\";\n\ninterface ContainerInspect {\n readonly State: { readonly Status: string };\n readonly Config: { readonly Image: string };\n readonly NetworkSettings: {\n readonly Ports?:\n | Record<string, ReadonlyArray<{ readonly HostIp: string; readonly HostPort: string }> | null>\n | null\n | undefined;\n };\n}\n\nconst containerName = (instance: string): string =>\n `bos-hosting-${instance.replaceAll(/[^a-zA-Z0-9_.-]/g, \"-\").toLowerCase()}`;\n\nconst stackName = (instance: string): string => `BosHosting-${containerName(instance)}`;\n\nconst describeHostError = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nconst isNotFound = (error: unknown): boolean =>\n error instanceof Error &&\n \"reason\" in error &&\n (error as { reason?: { _tag?: string } }).reason?._tag === \"NotFound\";\n\nconst alchemyContextLayer = (options: DockerHostingOptions) =>\n Layer.effect(\n AlchemyContext,\n Layer.build(AlchemyContextLive).pipe(\n Effect.map((context) => Context.get(context, AlchemyContext)),\n Effect.orDie,\n Effect.map((context) =>\n AlchemyContext.of({\n ...context,\n dev: false,\n adopt: options.adopt ?? false,\n dotAlchemy: options.stateDir ?? context.dotAlchemy ?? \".alchemy\",\n }),\n ),\n ),\n );\n\nconst platformLayer = Layer.mergeAll(PlatformServices, FetchHttpClient.layer);\n\nconst runtimeLayer = (options: DockerHostingOptions) =>\n Layer.mergeAll(\n alchemyContextLayer(options),\n Interaction.layerNonInteractive(),\n Layer.succeed(ArtifactStore, createArtifactStore()),\n Layer.succeed(AdoptPolicy, options.adopt ?? false),\n Layer.succeed(AuthProviders, {}),\n State.localState(),\n ).pipe(Layer.provideMerge(platformLayer));\n\nconst dockerServiceLayer = (options: DockerHostingOptions) =>\n Docker.providers().pipe(\n Layer.provide(\n Layer.succeed(Stack, {\n name: stackName(\"shared\"),\n stage: options.stage ?? DEFAULT_STAGE,\n resources: {},\n bindings: {},\n actions: {},\n }),\n ),\n Layer.provide(Layer.succeed(Stage, options.stage ?? DEFAULT_STAGE)),\n );\n\nexport const dockerHostingAdapterLayer = (options: DockerHostingOptions = {}) =>\n // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- alchemy's Docker.providers() requirements resolve as any under tsgo; the seam's R is closed by the provides below\n Layer.effect(\n HostingAdapter,\n Effect.gen(function* () {\n const http = yield* HttpClient.HttpClient;\n const docker = yield* Docker.Docker;\n const containerPort = options.containerPort ?? DEFAULT_CONTAINER_PORT;\n const healthPath = options.healthPath ?? DEFAULT_HEALTH_PATH;\n const stage = options.stage ?? DEFAULT_STAGE;\n const runtime = runtimeLayer(options);\n\n const hostPortOf = (info: ContainerInspect): number | undefined => {\n const bindings = info.NetworkSettings.Ports?.[`${containerPort}/tcp`];\n const hostPort = bindings?.[0]?.HostPort;\n return hostPort === undefined || hostPort === \"\" ? undefined : Number(hostPort);\n };\n\n const hostUrl = (hostPort: number): string => `http://127.0.0.1:${hostPort}`;\n\n const inspectInstance = (instance: string) =>\n docker.container.inspect(containerName(instance));\n\n const waitForHealth = Effect.fn(\"DockerHostingAdapter.waitForHealth\")(function* (\n instance: string,\n url: string,\n ) {\n const timeoutMs = Duration.toMillis(\n Duration.fromInputUnsafe(options.healthTimeout ?? DEFAULT_HEALTH_TIMEOUT),\n );\n const interval = Duration.fromInputUnsafe(\n options.healthInterval ?? DEFAULT_HEALTH_INTERVAL,\n );\n const deadline = (yield* Clock.currentTimeMillis) + timeoutMs;\n let attempts = 0;\n while (true) {\n attempts += 1;\n const healthy = yield* http.execute(HttpClientRequest.get(`${url}${healthPath}`)).pipe(\n Effect.map((response) => response.status >= 200 && response.status < 300),\n Effect.orElseSucceed(() => false),\n );\n if (healthy) return url;\n if ((yield* Clock.currentTimeMillis) >= deadline) {\n return yield* new HealthTimeout({ instance, url, attempts });\n }\n yield* Effect.sleep(interval);\n }\n });\n\n const snapshotOf = (instance: string, info: ContainerInspect): InstanceSnapshot => {\n const running = info.State.Status === \"running\";\n const port = running ? hostPortOf(info) : undefined;\n return {\n instance,\n running,\n url: port === undefined ? undefined : hostUrl(port),\n image: info.Config.Image,\n };\n };\n\n const deployDescriptor = (descriptor: RuntimeDescriptor) =>\n Effect.gen(function* () {\n const name = containerName(descriptor.instance);\n const stack = Alchemy.Stack(\n stackName(descriptor.instance),\n {\n providers: Docker.providers(),\n state: State.localState(),\n },\n Effect.gen(function* () {\n const container = yield* Docker.Container(\"runtime\", {\n image: descriptor.image,\n name,\n environment: { ...descriptor.identityEnv },\n labels: { \"bos.hosting/instance\": descriptor.instance },\n ports: [{ external: 0, internal: containerPort }],\n start: true,\n restart: \"no\",\n });\n return { container };\n }),\n );\n yield* Deploy.deploy({ stack, stage, dev: false }).pipe(\n Effect.provide(runtime),\n Effect.mapError(\n (error) =>\n new DeployFailed({\n instance: descriptor.instance,\n reason: describeHostError(error),\n }),\n ),\n );\n const info = yield* inspectInstance(descriptor.instance).pipe(\n Effect.mapError(\n (error) =>\n new DeployFailed({\n instance: descriptor.instance,\n reason: describeHostError(error),\n }),\n ),\n );\n const port = hostPortOf(info);\n if (port === undefined) {\n return yield* new DeployFailed({\n instance: descriptor.instance,\n reason: `container published no host port for ${containerPort}/tcp`,\n });\n }\n const url = hostUrl(port);\n yield* waitForHealth(descriptor.instance, url);\n return { instance: descriptor.instance, url };\n });\n\n const current = (instance: string) =>\n inspectInstance(instance).pipe(\n Effect.map((info) => snapshotOf(instance, info)),\n Effect.catchTag(\n \"PlatformError\",\n (error): Effect.Effect<InstanceSnapshot, HostingOperationFailed> =>\n isNotFound(error)\n ? Effect.succeed({ instance, running: false, url: undefined, image: undefined })\n : Effect.fail(\n new HostingOperationFailed({\n instance,\n operation: \"current\",\n reason: describeHostError(error),\n }),\n ),\n ),\n );\n\n const restart = (\n instance: string,\n ): Effect.Effect<\n DeployedInstance,\n InstanceNotFound | HealthTimeout | HostingOperationFailed\n > =>\n Effect.gen(function* () {\n const info = yield* inspectInstance(instance);\n if (info.State.Status === \"running\") {\n yield* docker.container.stop(containerName(instance));\n }\n yield* docker.container.start(containerName(instance));\n const after = yield* inspectInstance(instance);\n const port = hostPortOf(after);\n if (port === undefined) {\n return yield* new HostingOperationFailed({\n instance,\n operation: \"restart\",\n reason: `container published no host port for ${containerPort}/tcp`,\n });\n }\n const url = hostUrl(port);\n yield* waitForHealth(instance, url);\n return { instance, url };\n }).pipe(\n Effect.catchTag(\n \"PlatformError\",\n (error): Effect.Effect<DeployedInstance, InstanceNotFound | HostingOperationFailed> =>\n isNotFound(error)\n ? Effect.fail(new InstanceNotFound({ instance }))\n : Effect.fail(\n new HostingOperationFailed({\n instance,\n operation: \"restart\",\n reason: describeHostError(error),\n }),\n ),\n ),\n );\n\n const stop = (instance: string) =>\n docker.container.stop(containerName(instance)).pipe(\n Effect.asVoid,\n Effect.catchTag(\n \"PlatformError\",\n (error): Effect.Effect<void, InstanceNotFound | HostingOperationFailed> =>\n isNotFound(error)\n ? Effect.fail(new InstanceNotFound({ instance }))\n : Effect.fail(\n new HostingOperationFailed({\n instance,\n operation: \"stop\",\n reason: describeHostError(error),\n }),\n ),\n ),\n );\n\n return HostingAdapter.of({ deploy: deployDescriptor, current, restart, stop });\n }),\n ).pipe(\n Layer.provide(dockerServiceLayer(options)),\n Layer.provide(runtimeLayer(options)),\n ) as Layer.Layer<HostingAdapter>;\n"],"mappings":"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"}