import { join } from "node:path"; import { Effect } from "effect"; import { makeGitArtifactStore } from "../backing/git-artifact-store"; import { makeLibsqlScopeDb } from "../backing/libsql-scope-db"; import { makeQuickjsToolSandbox } from "../backing/quickjs-tool-sandbox"; import { makeSqliteWorkflowRunner } from "../backing/sqlite-workflow-runner"; import { makeQuickjsWorkflowDriver } from "../backing/quickjs-workflow-driver"; import { makeInProcessLiveChannel } from "../backing/in-process-live-channel"; import type { ScopeDb } from "../seams/scope-db"; import type { LiveChannel } from "../seams/live-channel"; import { makeAppsRuntime, type AppsRuntime } from "./runtime"; import type { AppsStore } from "./store"; import type { ClientResolver } from "./bindings"; // --------------------------------------------------------------------------- // Wire the five self-hosted seam backings into one AppsRuntime, rooted at a // data directory. This is what the self-host app calls at boot. The ScopeDb's // write events are wired into the LiveChannel here (invalidation delivery), and // the store is provided by the caller (over the host's pluginStorage + blobs). // --------------------------------------------------------------------------- export interface SelfHostAppsRuntimeOptions { /** Data dir root; `/artifacts`, `/scope-db`, `/workflows`. */ readonly dataDir: string; /** Store over the host's pluginStorage + blobs. */ readonly store: AppsStore; /** Routes bound integration calls to real APIs (policy/audit). */ readonly resolver: ClientResolver; /** In-memory backings for tests. */ readonly inMemory?: boolean; } export interface SelfHostAppsRuntime { readonly runtime: AppsRuntime; readonly scopeDb: ScopeDb; readonly liveChannel: LiveChannel; readonly close: () => Promise; } export const makeSelfHostAppsRuntime = ( options: SelfHostAppsRuntimeOptions, ): SelfHostAppsRuntime => { const inMem = options.inMemory === true; const artifactStore = makeGitArtifactStore({ root: inMem ? options.dataDir : join(options.dataDir, "artifacts"), }); const scopeDb = makeLibsqlScopeDb({ root: inMem ? ":memory:" : join(options.dataDir, "scope-db"), }); const sandbox = makeQuickjsToolSandbox(); // The workflow body runs inside the sandbox (Fix 3): the driver loads the // pinned bundle from the artifact store and drives one replay in QuickJS // behind the serializable step bridge. The runner services the bridge against // its journal. const workflowDriver = makeQuickjsWorkflowDriver({ artifactStore }); const workflows = makeSqliteWorkflowRunner({ path: inMem ? ":memory:" : join(options.dataDir, "workflows", "journal.db"), driver: workflowDriver, }); const liveChannel = makeInProcessLiveChannel(); // Wire scope-db writes -> live invalidations. const unwire = scopeDb.onWrite((event) => { for (const t of event.tables) { void Effect.runPromise( liveChannel.publish({ scope: event.scope, table: t.table, version: t.version }), ); } }); const runtime = makeAppsRuntime({ artifactStore, scopeDb, sandbox, workflows, liveChannel, store: options.store, resolver: options.resolver, }); return { runtime, scopeDb, liveChannel, close: async () => { unwire(); await Effect.runPromise(scopeDb.close().pipe(Effect.orElseSucceed(() => undefined))); await Effect.runPromise(workflows.close().pipe(Effect.orElseSucceed(() => undefined))); }, }; };