fix(instance): run bootstrap from instance store (#25475)
This commit is contained in:
9
packages/opencode/src/project/bootstrap-service.ts
Normal file
9
packages/opencode/src/project/bootstrap-service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Context, Effect } from "effect"
|
||||
|
||||
export interface Interface {
|
||||
readonly run: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceBootstrap") {}
|
||||
|
||||
export * as InstanceBootstrap from "./bootstrap-service"
|
||||
@@ -10,21 +10,19 @@ import { Command } from "../command"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Config } from "@/config/config"
|
||||
import { Service } from "./bootstrap-service"
|
||||
|
||||
export interface Interface {
|
||||
readonly run: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceBootstrap") {}
|
||||
export { Service } from "./bootstrap-service"
|
||||
export type { Interface } from "./bootstrap-service"
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
// Yield each bootstrap dep at layer init so `run` itself has R = never.
|
||||
// This breaks the circular declaration loop through Config → Instance → InstanceStore
|
||||
// (instance-store.ts only yields this Service tag, never the impl-side services).
|
||||
// InstanceStore imports only the lightweight tag from bootstrap-service.ts,
|
||||
// so it can depend on bootstrap without importing this implementation graph.
|
||||
const bus = yield* Bus.Service
|
||||
const config = yield* Config.Service
|
||||
const file = yield* File.Service
|
||||
|
||||
27
packages/opencode/src/project/instance-runtime.ts
Normal file
27
packages/opencode/src/project/instance-runtime.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { type InstanceContext } from "./instance-context"
|
||||
import { InstanceStore, type LoadInput } from "./instance-store"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
// Production InstanceStore wiring plus a bridge for Promise/ALS callers that
|
||||
// cannot yet yield InstanceStore.Service. This keeps InstanceStore itself
|
||||
// low-level while still giving legacy Hono and CLI paths the production
|
||||
// bootstrap implementation. Delete the Promise helpers once those callers are
|
||||
// migrated to Effect boundaries that provide InstanceStore directly.
|
||||
// Keep the bootstrap implementation import lazy: Instance is imported broadly,
|
||||
// and importing the app bootstrap graph at module load can trigger ESM cycles.
|
||||
export const layer = Layer.unwrap(
|
||||
Effect.promise(async () => {
|
||||
const { InstanceBootstrap } = await import("./bootstrap")
|
||||
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
|
||||
}),
|
||||
)
|
||||
|
||||
const runtime = makeRuntime(InstanceStore.Service, layer)
|
||||
|
||||
export const load = (input: LoadInput) => runtime.runPromise((store) => store.load(input))
|
||||
export const disposeInstance = (ctx: InstanceContext) => runtime.runPromise((store) => store.dispose(ctx))
|
||||
export const disposeAllInstances = () => runtime.runPromise((store) => store.disposeAll())
|
||||
export const reloadInstance = (input: LoadInput) => runtime.runPromise((store) => store.reload(input))
|
||||
|
||||
export * as InstanceRuntime from "./instance-runtime"
|
||||
@@ -2,10 +2,10 @@ import { GlobalBus } from "@/bus/global"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { disposeInstance as runDisposers } from "@/effect/instance-registry"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
|
||||
import { type InstanceContext } from "./instance-context"
|
||||
import { InstanceBootstrap } from "./bootstrap-service"
|
||||
import * as Project from "./project"
|
||||
|
||||
export interface LoadInput<R = never> {
|
||||
@@ -36,10 +36,11 @@ interface Entry {
|
||||
readonly deferred: Deferred.Deferred<InstanceContext>
|
||||
}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootstrap.Service> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const project = yield* Project.Service
|
||||
const bootstrap = yield* InstanceBootstrap.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const cache = new Map<string, Entry>()
|
||||
|
||||
@@ -59,6 +60,7 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
project: result.project,
|
||||
})),
|
||||
)
|
||||
yield* bootstrap.run.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
if (input.init) yield* input.init.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
return ctx
|
||||
}).pipe(Effect.withSpan("InstanceStore.boot"))
|
||||
@@ -195,13 +197,4 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
|
||||
|
||||
export const runtime = makeRuntime(Service, defaultLayer)
|
||||
|
||||
// Promise-returning helpers for callers without an Effect runtime in scope.
|
||||
// They route through `runtime` (not a yielded Service from a fresh runtime)
|
||||
// so they share the cache that `Instance.provide` populates.
|
||||
export const disposeInstance = (ctx: InstanceContext) => runtime.runPromise((store) => store.dispose(ctx))
|
||||
export const disposeAllInstances = () => runtime.runPromise((store) => store.disposeAll())
|
||||
export const reloadInstance = (input: LoadInput) => runtime.runPromise((store) => store.reload(input))
|
||||
|
||||
export * as InstanceStore from "./instance-store"
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import { context, type InstanceContext } from "./instance-context"
|
||||
import { InstanceStore } from "./instance-store"
|
||||
import { InstanceRuntime } from "./instance-runtime"
|
||||
|
||||
export type { InstanceContext } from "./instance-context"
|
||||
export type { LoadInput } from "./instance-store"
|
||||
|
||||
export const Instance = {
|
||||
async provide<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }): Promise<R> {
|
||||
const ctx = await InstanceStore.runtime.runPromise((store) =>
|
||||
store.load({ directory: input.directory, init: input.init }),
|
||||
)
|
||||
const ctx = await InstanceRuntime.load({ directory: input.directory, init: input.init })
|
||||
return context.provide(ctx, async () => input.fn())
|
||||
},
|
||||
get current() {
|
||||
|
||||
Reference in New Issue
Block a user