everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
201 lines (199 loc) • 10.6 kB
JavaScript
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