import { Effect, Schema, type Scope } from 'effect' import type { HttpServerStopResult, StoppableHttpServer } from './shutdown' export class HostingStartupError extends Schema.TaggedError()('HostingStartupError', { component: Schema.Literals([ 'observability', 'private sites database', 'database', 'revalidation', 'storage', 'cache invalidation', 'http', ]), }) {} export interface HostingLifecycleDependencies { initializeExporters(): Promise shutdownExporters(): Promise closeDatabase(): Promise closePrivateDatabase(): Promise closeRevalidation(): Promise acquireStorage?: Effect.Effect startCleanup(): void stopCleanup(): void startInvalidation(): Promise stopInvalidation(): Promise bootstrap(): Promise createServer(): StoppableHttpServer stopServer(server: StoppableHttpServer): Promise logFailure(component: string): void logHttpStop(result: HttpServerStopResult): void } /** Acquire in dependency order; unwind partial startup and drain HTTP before storage users. */ export const acquireHosting = Effect.fn('hosting.acquireHosting')(function* ( dependencies: HostingLifecycleDependencies, ): Effect.fn.Return { const startup = (component: HostingStartupError['component'], run: () => Promise) => Effect.tryPromise({ try: run, catch: () => new HostingStartupError({ component }), }) const cleanup = (component: HostingStartupError['component'], run: () => Promise) => startup(component, run).pipe( Effect.catchTag('HostingStartupError', () => Effect.sync(() => dependencies.logFailure(component))), Effect.asVoid, ) yield* Effect.acquireRelease(startup('observability', dependencies.initializeExporters), () => cleanup('observability', dependencies.shutdownExporters), ) yield* Effect.addFinalizer(() => Effect.gen(function* () { yield* cleanup('private sites database', dependencies.closePrivateDatabase) yield* cleanup('database', dependencies.closeDatabase) }), ) yield* Effect.addFinalizer(() => cleanup('revalidation', dependencies.closeRevalidation)) if (dependencies.acquireStorage) { yield* dependencies.acquireStorage.pipe(Effect.mapError(() => new HostingStartupError({ component: 'storage' }))) } yield* Effect.acquireRelease(Effect.sync(dependencies.startCleanup), () => Effect.sync(dependencies.stopCleanup)) yield* Effect.acquireRelease(startup('cache invalidation', dependencies.startInvalidation), () => cleanup('cache invalidation', dependencies.stopInvalidation), ) // Retain one actual bootstrap promise, not an interruptible wrapper around disk writes. const bootstrap = dependencies.bootstrap().catch(() => dependencies.logFailure('hot cache bootstrap')) yield* Effect.addFinalizer(() => Effect.promise(() => bootstrap)) return yield* Effect.acquireRelease( Effect.try({ try: dependencies.createServer, catch: () => new HostingStartupError({ component: 'http' }), }), (server) => cleanup('http', async () => dependencies.logHttpStop(await dependencies.stopServer(server))), ) })