213 lines · 8.2 KB · TypeScript
RawDownload
import { existsSync } from "node:fs";import { mkdir } from "node:fs/promises";import { resolve } from "node:path";import { Context, Effect, Layer, Schema } from "effect";import { createConfig, type Config } from "./config/env";import { createLogger, resolveLogLevel, type Logger } from "./core/logger";import { primaryLogPathFor } from "./core/log-files";import { DownloadManager } from "./modules/engines/downloads/download-manager";import { DownloadStore } from "./modules/engines/downloads/download-store";import {  createLaunchFailureBudget,  type LaunchFailureBudget,} from "./modules/engines/launch-failure-budget";import { createComputeBridge, type ComputeBridge } from "./modules/compute/bridge";import { makeCompute, type Compute } from "./modules/compute/service";import { shutdownEngineJobs } from "./modules/engines/runtimes/engine-jobs";import { shutdownRuntimeInfo } from "./modules/engines/runtimes/runtime-info";import { RecipeStore } from "./modules/models/recipes/recipe-store";import { EventManager } from "./modules/system/event-manager";import { PeakMetricsStore, LifetimeMetricsStore } from "./modules/system/metrics-store";import { ControllerRequestStore } from "./stores/controller-request-store";import { ControllerSettingsStore } from "./stores/controller-settings-store";import { InferenceRequestStore } from "./stores/inference-request-store";import { RigStore } from "./stores/rig-store"; export interface AppContext {  config: Config;  logger: Logger;  eventManager: EventManager;  launchFailureBudget: LaunchFailureBudget;  downloadManager: DownloadManager;  compute: Compute;  bridge: ComputeBridge;  stores: {    recipeStore: RecipeStore;    downloadStore: DownloadStore;    peakMetricsStore: PeakMetricsStore;    lifetimeMetricsStore: LifetimeMetricsStore;    inferenceRequestStore: InferenceRequestStore;    controllerSettingsStore: ControllerSettingsStore;    controllerRequestStore: ControllerRequestStore;    rigStore: RigStore;  };} export class AppContextInitializationError extends Schema.TaggedErrorClass<AppContextInitializationError>()(  "AppContextInitializationError",  {    operation: Schema.String,    message: Schema.String,    source: Schema.Unknown,  },) {} export type ModelsDirectoryState = "exists" | "created" | "missing"; let modelsDirectoryState: ModelsDirectoryState = "missing"; export const getModelsDirectoryState = (): ModelsDirectoryState => modelsDirectoryState; const initializationError = (operation: string, source: unknown): AppContextInitializationError =>  new AppContextInitializationError({ operation, message: String(source), source }); const initialize = <A, E>(  operation: string,  effect: Effect.Effect<A, E>,): Effect.Effect<A, AppContextInitializationError> =>  effect.pipe(Effect.mapError((source) => initializationError(operation, source))); const initializeSync = <A>(  operation: string,  make: () => A,): Effect.Effect<A, AppContextInitializationError> =>  Effect.try({ try: make, catch: (source) => initializationError(operation, source) }); const releaseSafely = (  operation: string,  logger: Logger,  effect: Effect.Effect<void, unknown>,): Effect.Effect<void> =>  effect.pipe(    Effect.catch((error) =>      Effect.sync(() => logger.error(`${operation} failed`, { error: String(error) })),    ),  ); const ensureModelsDirectory = (modelsDirectory: string): Effect.Effect<ModelsDirectoryState> => {  if (existsSync(modelsDirectory)) return Effect.succeed("exists");  return Effect.tryPromise({    try: () => mkdir(modelsDirectory, { recursive: true }),    catch: () => undefined,  }).pipe(    Effect.as("created" as const),    Effect.catch(() => Effect.succeed("missing" as const)),  );}; export const makeAppContext = Effect.gen(function* () {  const config = yield* initializeSync("config.load", createConfig);  yield* initialize(    "data-directory.create",    Effect.tryPromise({      try: () => mkdir(config.data_dir, { recursive: true }),      catch: (source) => source,    }),  );  const dbPath = resolve(config.db_path);  const eventManager = new EventManager();  const logger = yield* Effect.acquireRelease(    initializeSync("logger.open", () =>      createLogger(resolveLogLevel("info"), {        filePath: primaryLogPathFor(config.data_dir, "controller"),        onLine: (line) => eventManager.publishLogLineUnsafe("controller", line),      }),    ),    (resource) => resource.shutdown(),  );  yield* Effect.acquireRelease(Effect.succeed(eventManager), (resource) =>    releaseSafely("event-manager.shutdown", logger, resource.shutdown()),  );   modelsDirectoryState = yield* ensureModelsDirectory(config.models_dir);  if (modelsDirectoryState === "missing") {    logger.warn(      `Models directory ${config.models_dir} does not exist and could not be created; set LOCAL_STUDIO_MODELS_DIR to a writable path`,    );  }   const recipeStore = yield* Effect.acquireRelease(    initialize("recipe-store.open", RecipeStore.open(dbPath)),    (resource) => releaseSafely("recipe-store.close", logger, resource.close()),  );  const downloadStore = yield* Effect.acquireRelease(    initialize("download-store.open", DownloadStore.make(dbPath)),    (resource) => releaseSafely("download-store.close", logger, resource.close()),  );  const peakMetricsStore = yield* Effect.acquireRelease(    initializeSync("peak-metrics-store.open", () => new PeakMetricsStore(dbPath)),    (resource) => releaseSafely("peak-metrics-store.close", logger, resource.close()),  );  const lifetimeMetricsStore = yield* Effect.acquireRelease(    initializeSync("lifetime-metrics-store.open", () => new LifetimeMetricsStore(dbPath)),    (resource) => releaseSafely("lifetime-metrics-store.close", logger, resource.close()),  );  const inferenceRequestStore = yield* Effect.acquireRelease(    initializeSync("inference-request-store.open", () => new InferenceRequestStore(dbPath)),    (resource) => releaseSafely("inference-request-store.close", logger, resource.close()),  );  const controllerSettingsStore = yield* Effect.acquireRelease(    initializeSync("controller-settings-store.open", () => new ControllerSettingsStore(dbPath)),    (resource) => releaseSafely("controller-settings-store.close", logger, resource.close()),  );  const controllerRequestStore = yield* Effect.acquireRelease(    initializeSync("controller-request-store.open", () => new ControllerRequestStore(dbPath)),    (resource) => releaseSafely("controller-request-store.close", logger, resource.close()),  );  const rigStore = yield* Effect.acquireRelease(    initializeSync("rig-store.open", () => new RigStore(dbPath)),    (resource) => releaseSafely("rig-store.close", logger, resource.close()),  );  yield* initialize(    "lifetime-metrics-store.initialize",    lifetimeMetricsStore.ensureFirstStartedEffect(),  );   const launchFailureBudget = createLaunchFailureBudget();  const compute = yield* initializeSync("compute.open", () => makeCompute(config, eventManager));  const bridge = createComputeBridge({    config,    compute: compute.service,    store: compute.store,    getRecipe: (recipeId) => recipeStore.get(recipeId),  });  const downloadManager = yield* initialize(    "download-manager.open",    DownloadManager.make(config, downloadStore, eventManager, logger),  );  yield* Effect.acquireRelease(Effect.void, () =>    releaseSafely("runtime-info.shutdown", logger, shutdownRuntimeInfo()),  );  yield* Effect.acquireRelease(Effect.void, () =>    releaseSafely("engine-jobs.shutdown", logger, shutdownEngineJobs()),  );  yield* Effect.acquireRelease(Effect.succeed(downloadManager), (resource) =>    releaseSafely("download-manager.shutdown", logger, resource.shutdown()),  );   return {    config,    logger,    eventManager,    launchFailureBudget,    downloadManager,    compute,    bridge,    stores: {      recipeStore,      downloadStore,      peakMetricsStore,      lifetimeMetricsStore,      inferenceRequestStore,      controllerSettingsStore,      controllerRequestStore,      rigStore,    },  } satisfies AppContext;}); export class AppContextService extends Context.Service<AppContextService, AppContext>()(  "local-studio/AppContext",) {} export const AppContextLive = Layer.effect(AppContextService, makeAppContext);