UNPKG

everything-dev

Version:

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

201 lines (199 loc) • 10.6 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_runtime = require('../../_virtual/_rolldown/runtime.cjs'); const require_services_hosting_adapter = require('./adapter.cjs'); let effect = require("effect"); let alchemy = require("alchemy"); alchemy = require_runtime.__toESM(alchemy, 1); let alchemy_AdoptPolicy = require("alchemy/AdoptPolicy"); let alchemy_Artifacts = require("alchemy/Artifacts"); let alchemy_Deploy = require("alchemy/Deploy"); alchemy_Deploy = require_runtime.__toESM(alchemy_Deploy, 1); let alchemy_Docker = require("alchemy/Docker"); alchemy_Docker = require_runtime.__toESM(alchemy_Docker, 1); let alchemy_Interaction = require("alchemy/Interaction"); alchemy_Interaction = require_runtime.__toESM(alchemy_Interaction, 1); let alchemy_State = require("alchemy/State"); alchemy_State = require_runtime.__toESM(alchemy_State, 1); let alchemy_Util_PlatformServices = require("alchemy/Util/PlatformServices"); let effect_unstable_http_FetchHttpClient = require("effect/unstable/http/FetchHttpClient"); effect_unstable_http_FetchHttpClient = require_runtime.__toESM(effect_unstable_http_FetchHttpClient, 1); let effect_unstable_http_HttpClient = require("effect/unstable/http/HttpClient"); effect_unstable_http_HttpClient = require_runtime.__toESM(effect_unstable_http_HttpClient, 1); let effect_unstable_http_HttpClientRequest = require("effect/unstable/http/HttpClientRequest"); effect_unstable_http_HttpClientRequest = require_runtime.__toESM(effect_unstable_http_HttpClientRequest, 1); //#region src/services/hosting/docker.ts /** * Docker dev-substrate `HostingAdapter` (ADR 0035). * * Alchemy is the control plane, embedded programmatically: each instance is * one Alchemy stack holding a `Docker.Container` resource, evaluated * in-process through `alchemy/Deploy`'s `deploy()` — the same plan/apply and * state-ledger semantics as `alchemy deploy` without a CLI subprocess. The * choice was deliberate: `Deploy.deploy` is a public programmatic API and * alchemy's own test harness (`lib/Test/Core.js`) drives stacks exactly this * way, so idempotency comes from Alchemy's plan diffing (an unchanged * descriptor yields a no-op apply and never recreates the container) and the * ledger is Alchemy's local file state store under `stateDir`. * Stop/start/inspect go through the same provider's `Docker` service, so * every mutation flows through the one control plane — the adapter never * shells `docker` itself. Docker is the development/CI substrate only; * Railway and Fly adapters implement the same seam. * * The host port is deliberately ephemeral (`external: 0`): Docker binds a * free host port at container creation, the provider reports the resolved * mapping in the resource output, and the instance URL derives from it. The * binding is stable for the container's lifetime (stop/start preserves it); * only a re-create — i.e. a descriptor change — rebinds it. */ const DEFAULT_CONTAINER_PORT = 3e3; const DEFAULT_HEALTH_PATH = "/health"; const DEFAULT_HEALTH_TIMEOUT = "90 seconds"; const DEFAULT_HEALTH_INTERVAL = "500 millis"; const DEFAULT_STAGE = "local"; const containerName = (instance) => `bos-hosting-${instance.replaceAll(/[^a-zA-Z0-9_.-]/g, "-").toLowerCase()}`; const stackName = (instance) => `BosHosting-${containerName(instance)}`; const describeHostError = (error) => error instanceof Error ? error.message : String(error); const isNotFound = (error) => error instanceof Error && "reason" in error && error.reason?._tag === "NotFound"; const alchemyContextLayer = (options) => effect.Layer.effect(alchemy.AlchemyContext, effect.Layer.build(alchemy.AlchemyContextLive).pipe(effect.Effect.map((context) => effect.Context.get(context, alchemy.AlchemyContext)), effect.Effect.orDie, effect.Effect.map((context) => alchemy.AlchemyContext.of({ ...context, dev: false, adopt: options.adopt ?? false, dotAlchemy: options.stateDir ?? context.dotAlchemy ?? ".alchemy" })))); const platformLayer = effect.Layer.mergeAll(alchemy_Util_PlatformServices.PlatformServices, effect_unstable_http_FetchHttpClient.layer); const runtimeLayer = (options) => effect.Layer.mergeAll(alchemyContextLayer(options), alchemy_Interaction.layerNonInteractive(), effect.Layer.succeed(alchemy_Artifacts.ArtifactStore, (0, alchemy_Artifacts.createArtifactStore)()), effect.Layer.succeed(alchemy_AdoptPolicy.AdoptPolicy, options.adopt ?? false), effect.Layer.succeed(alchemy.AuthProviders, {}), alchemy_State.localState()).pipe(effect.Layer.provideMerge(platformLayer)); const dockerServiceLayer = (options) => alchemy_Docker.providers().pipe(effect.Layer.provide(effect.Layer.succeed(alchemy.Stack, { name: stackName("shared"), stage: options.stage ?? DEFAULT_STAGE, resources: {}, bindings: {}, actions: {} })), effect.Layer.provide(effect.Layer.succeed(alchemy.Stage, options.stage ?? DEFAULT_STAGE))); const dockerHostingAdapterLayer = (options = {}) => effect.Layer.effect(require_services_hosting_adapter.HostingAdapter, effect.Effect.gen(function* () { const http = yield* effect_unstable_http_HttpClient.HttpClient; const docker = yield* alchemy_Docker.Docker; const containerPort = options.containerPort ?? DEFAULT_CONTAINER_PORT; const healthPath = options.healthPath ?? DEFAULT_HEALTH_PATH; const stage = options.stage ?? DEFAULT_STAGE; const runtime = runtimeLayer(options); const hostPortOf = (info) => { const hostPort = (info.NetworkSettings.Ports?.[`${containerPort}/tcp`])?.[0]?.HostPort; return hostPort === void 0 || hostPort === "" ? void 0 : Number(hostPort); }; const hostUrl = (hostPort) => `http://127.0.0.1:${hostPort}`; const inspectInstance = (instance) => docker.container.inspect(containerName(instance)); const waitForHealth = effect.Effect.fn("DockerHostingAdapter.waitForHealth")(function* (instance, url) { const timeoutMs = effect.Duration.toMillis(effect.Duration.fromInputUnsafe(options.healthTimeout ?? DEFAULT_HEALTH_TIMEOUT)); const interval = effect.Duration.fromInputUnsafe(options.healthInterval ?? DEFAULT_HEALTH_INTERVAL); const deadline = (yield* effect.Clock.currentTimeMillis) + timeoutMs; let attempts = 0; while (true) { attempts += 1; if (yield* http.execute(effect_unstable_http_HttpClientRequest.get(`${url}${healthPath}`)).pipe(effect.Effect.map((response) => response.status >= 200 && response.status < 300), effect.Effect.orElseSucceed(() => false))) return url; if ((yield* effect.Clock.currentTimeMillis) >= deadline) return yield* new require_services_hosting_adapter.HealthTimeout({ instance, url, attempts }); yield* effect.Effect.sleep(interval); } }); const snapshotOf = (instance, info) => { const running = info.State.Status === "running"; const port = running ? hostPortOf(info) : void 0; return { instance, running, url: port === void 0 ? void 0 : hostUrl(port), image: info.Config.Image }; }; const deployDescriptor = (descriptor) => effect.Effect.gen(function* () { const name = containerName(descriptor.instance); const stack = alchemy.Stack(stackName(descriptor.instance), { providers: alchemy_Docker.providers(), state: alchemy_State.localState() }, effect.Effect.gen(function* () { return { container: yield* alchemy_Docker.Container("runtime", { image: descriptor.image, name, environment: { ...descriptor.identityEnv }, labels: { "bos.hosting/instance": descriptor.instance }, ports: [{ external: 0, internal: containerPort }], start: true, restart: "no" }) }; })); yield* alchemy_Deploy.deploy({ stack, stage, dev: false }).pipe(effect.Effect.provide(runtime), effect.Effect.mapError((error) => new require_services_hosting_adapter.DeployFailed({ instance: descriptor.instance, reason: describeHostError(error) }))); const info = yield* inspectInstance(descriptor.instance).pipe(effect.Effect.mapError((error) => new require_services_hosting_adapter.DeployFailed({ instance: descriptor.instance, reason: describeHostError(error) }))); const port = hostPortOf(info); if (port === void 0) return yield* new require_services_hosting_adapter.DeployFailed({ instance: descriptor.instance, reason: `container published no host port for ${containerPort}/tcp` }); const url = hostUrl(port); yield* waitForHealth(descriptor.instance, url); return { instance: descriptor.instance, url }; }); const current = (instance) => inspectInstance(instance).pipe(effect.Effect.map((info) => snapshotOf(instance, info)), effect.Effect.catchTag("PlatformError", (error) => isNotFound(error) ? effect.Effect.succeed({ instance, running: false, url: void 0, image: void 0 }) : effect.Effect.fail(new require_services_hosting_adapter.HostingOperationFailed({ instance, operation: "current", reason: describeHostError(error) })))); const restart = (instance) => effect.Effect.gen(function* () { if ((yield* inspectInstance(instance)).State.Status === "running") yield* docker.container.stop(containerName(instance)); yield* docker.container.start(containerName(instance)); const after = yield* inspectInstance(instance); const port = hostPortOf(after); if (port === void 0) return yield* new require_services_hosting_adapter.HostingOperationFailed({ instance, operation: "restart", reason: `container published no host port for ${containerPort}/tcp` }); const url = hostUrl(port); yield* waitForHealth(instance, url); return { instance, url }; }).pipe(effect.Effect.catchTag("PlatformError", (error) => isNotFound(error) ? effect.Effect.fail(new require_services_hosting_adapter.InstanceNotFound({ instance })) : effect.Effect.fail(new require_services_hosting_adapter.HostingOperationFailed({ instance, operation: "restart", reason: describeHostError(error) })))); const stop = (instance) => docker.container.stop(containerName(instance)).pipe(effect.Effect.asVoid, effect.Effect.catchTag("PlatformError", (error) => isNotFound(error) ? effect.Effect.fail(new require_services_hosting_adapter.InstanceNotFound({ instance })) : effect.Effect.fail(new require_services_hosting_adapter.HostingOperationFailed({ instance, operation: "stop", reason: describeHostError(error) })))); return require_services_hosting_adapter.HostingAdapter.of({ deploy: deployDescriptor, current, restart, stop }); })).pipe(effect.Layer.provide(dockerServiceLayer(options)), effect.Layer.provide(runtimeLayer(options))); //#endregion exports.dockerHostingAdapterLayer = dockerHostingAdapterLayer; //# sourceMappingURL=docker.cjs.map