summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-05-02 11:21:40 -0400
committerGitHub <[email protected]>2026-05-02 11:21:40 -0400
commitb09b7d28b801a4c4f3f1c691345a35a0c62aafd6 (patch)
tree7fe7311f597f2360e702d93e790be67438b97007 /packages
parent31ed4602e14d44d840a579bca871be955ff49391 (diff)
downloadopencode-b09b7d28b801a4c4f3f1c691345a35a0c62aafd6.tar.gz
opencode-b09b7d28b801a4c4f3f1c691345a35a0c62aafd6.zip
refactor(instance-store): consolidate dispose helpers (#25424)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/cli/bootstrap.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/worker.ts2
-rw-r--r--packages/opencode/src/config/config.ts11
-rw-r--r--packages/opencode/src/project/instance-store.ts13
-rw-r--r--packages/opencode/src/project/instance.ts11
-rw-r--r--packages/opencode/src/server/routes/global.ts2
-rw-r--r--packages/opencode/src/server/routes/instance/index.ts2
-rw-r--r--packages/opencode/src/server/routes/instance/project.ts6
-rw-r--r--packages/opencode/test/fixture/fixture.ts10
9 files changed, 37 insertions, 22 deletions
diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts
index aa6aef6a2..a0dd9fe2a 100644
--- a/packages/opencode/src/cli/bootstrap.ts
+++ b/packages/opencode/src/cli/bootstrap.ts
@@ -9,7 +9,7 @@ export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
const result = await cb()
return result
} finally {
- await InstanceStore.runtime.runPromise((s) => s.dispose(Instance.current))
+ await InstanceStore.disposeInstance(Instance.current)
}
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index 41ca99a71..0f0fd693d 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -88,7 +88,7 @@ export const rpc = {
async shutdown() {
Log.Default.info("worker shutting down")
- await InstanceStore.runtime.runPromise((s) => s.disposeAll())
+ await InstanceStore.disposeAllInstances()
if (server) await server.stop(true)
},
}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 4dcab3e8d..46a31cf1c 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -13,7 +13,6 @@ import { Env } from "../env"
import { applyEdits, modify } from "jsonc-parser"
import { type InstanceContext } from "../project/instance"
import { InstanceStore } from "../project/instance-store"
-import { InstanceRef } from "@/effect/instance-ref"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { existsSync } from "fs"
import { GlobalBus } from "@/bus/global"
@@ -739,15 +738,17 @@ export const layer = Layer.effect(
.writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2))
.pipe(Effect.orDie)
if (options?.dispose !== false) {
- const ctx = yield* InstanceRef
- if (ctx) yield* Effect.promise(() => InstanceStore.runtime.runPromise((s) => s.dispose(ctx)))
+ // Fail loudly if no instance is bound — silently skipping would
+ // mask "config update without an active instance" bugs. The throw
+ // comes from `Instance.current` inside `InstanceState.context`.
+ const ctx = yield* InstanceState.context
+ yield* Effect.promise(() => InstanceStore.disposeInstance(ctx))
}
})
const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) {
yield* invalidateGlobal
- const task = InstanceStore.runtime
- .runPromise((s) => s.disposeAll())
+ const task = InstanceStore.disposeAllInstances()
.catch(() => undefined)
.finally(() =>
GlobalBus.emit("event", {
diff --git a/packages/opencode/src/project/instance-store.ts b/packages/opencode/src/project/instance-store.ts
index e96c421a7..00075be64 100644
--- a/packages/opencode/src/project/instance-store.ts
+++ b/packages/opencode/src/project/instance-store.ts
@@ -1,7 +1,7 @@
import { GlobalBus } from "@/bus/global"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { InstanceRef } from "@/effect/instance-ref"
-import { disposeInstance } from "@/effect/instance-registry"
+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"
@@ -94,7 +94,7 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
const disposeContext = Effect.fn("InstanceStore.disposeContext")(function* (ctx: InstanceContext) {
yield* Effect.logInfo("disposing instance", { directory: ctx.directory })
- yield* Effect.promise(() => disposeInstance(ctx.directory))
+ yield* Effect.promise(() => runDisposers(ctx.directory))
yield* emitDisposed({ directory: ctx.directory, project: ctx.project.id })
})
@@ -135,7 +135,7 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
yield* Effect.logInfo("reloading instance", { directory })
if (previous) {
yield* Deferred.await(previous.deferred).pipe(Effect.ignore)
- yield* Effect.promise(() => disposeInstance(directory))
+ yield* Effect.promise(() => runDisposers(directory))
yield* emitDisposed({ directory, project: input.project?.id })
}
yield* completeLoad(directory, input, entry)
@@ -197,4 +197,11 @@ 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"
diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts
index 44ba39763..22c2779ce 100644
--- a/packages/opencode/src/project/instance.ts
+++ b/packages/opencode/src/project/instance.ts
@@ -42,10 +42,17 @@ export const Instance = {
restore<R>(ctx: InstanceContext, fn: () => R): R {
return context.provide(ctx, fn)
},
+ // followup: `reload` survives because `test/server/project-init-git.test.ts`
+ // spies on this exact method. Once that test asserts on `InstanceStore.reloadInstance`
+ // (or moves to an Effect runtime), this wrapper can drop.
async reload(input: InstanceStore.LoadInput) {
- return InstanceStore.runtime.runPromise((store) => store.reload(input))
+ return InstanceStore.reloadInstance(input)
},
+ // followup: `dispose` survives for legacy fixtures that read `Instance.current`
+ // out of ALS (e.g. `test/fixture/fixture.ts` `provideTmpdirInstance`,
+ // `test/question/question.test.ts` cancellation tests). Convert those to call
+ // `InstanceStore.disposeInstance(ctx)` directly once `Instance.provide` is gone.
async dispose() {
- return InstanceStore.runtime.runPromise((store) => store.dispose(Instance.current))
+ return InstanceStore.disposeInstance(Instance.current)
},
}
diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts
index 97fee3bfc..f40a58453 100644
--- a/packages/opencode/src/server/routes/global.ts
+++ b/packages/opencode/src/server/routes/global.ts
@@ -200,7 +200,7 @@ export const GlobalRoutes = lazy(() =>
},
}),
async (c) => {
- await InstanceStore.runtime.runPromise((s) => s.disposeAll())
+ await InstanceStore.disposeAllInstances()
GlobalBus.emit("event", {
directory: "global",
payload: {
diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts
index 6ee9b4fad..530c02345 100644
--- a/packages/opencode/src/server/routes/instance/index.ts
+++ b/packages/opencode/src/server/routes/instance/index.ts
@@ -63,7 +63,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
},
}),
async (c) => {
- await InstanceStore.runtime.runPromise((s) => s.dispose(Instance.current))
+ await InstanceStore.disposeInstance(Instance.current)
return c.json(true)
},
)
diff --git a/packages/opencode/src/server/routes/instance/project.ts b/packages/opencode/src/server/routes/instance/project.ts
index 7db2bbdda..14c8c87b0 100644
--- a/packages/opencode/src/server/routes/instance/project.ts
+++ b/packages/opencode/src/server/routes/instance/project.ts
@@ -81,7 +81,11 @@ export const ProjectRoutes = lazy(() =>
Project.Service.use((svc) => svc.initGit({ directory: dir, project: prev })),
)
if (next.id === prev.id && next.vcs === prev.vcs && next.worktree === prev.worktree) return c.json(next)
- await Instance.reload({ directory: dir, worktree: dir, project: next })
+ await Instance.reload({
+ directory: dir,
+ worktree: dir,
+ project: next,
+ })
return c.json(next)
},
)
diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts
index a861285a1..23dd61d88 100644
--- a/packages/opencode/test/fixture/fixture.ts
+++ b/packages/opencode/test/fixture/fixture.ts
@@ -9,15 +9,11 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "@/config/config"
import { InstanceRef } from "../../src/effect/instance-ref"
import { Instance } from "../../src/project/instance"
-import { InstanceStore } from "../../src/project/instance-store"
import { TestLLMServer } from "../lib/llm-server"
-// Test helper for tearing down all loaded instances. Used in afterEach hooks.
-// Replaces direct Instance.disposeAll() calls so the legacy promise method can be removed.
-// IMPORTANT: must use InstanceStore.runtime, not AppRuntime or a test-layer Service —
-// Instance.provide loads instances into InstanceStore.runtime's Service cache, and that
-// Service is built per-runtime (not shared via memoMap across Effect.runPromise boundaries).
-export const disposeAllInstances = () => InstanceStore.runtime.runPromise((s) => s.disposeAll())
+// Re-export for test ergonomics. The implementation lives next to the runtime
+// it consumes; see `InstanceStore.disposeAllInstances` for the rationale.
+export { disposeAllInstances } from "../../src/project/instance-store"
// Strip null bytes from paths (defensive fix for CI environment issues)
function sanitizePath(p: string): string {