|
1 | | -import { GlobalBus } from "@/bus/global" |
2 | | -import { disposeInstance } from "@/effect/instance-registry" |
3 | | -import { makeRuntime } from "@/effect/run-service" |
4 | 1 | import { AppFileSystem } from "@opencode-ai/core/filesystem" |
5 | | -import { iife } from "@/util/iife" |
6 | | -import * as Log from "@opencode-ai/core/util/log" |
7 | | -import { LocalContext } from "@/util/local-context" |
8 | 2 | import * as Project from "./project" |
9 | | -import { WorkspaceContext } from "@/control-plane/workspace-context" |
10 | | -import { Context, Effect, Layer } from "effect" |
| 3 | +import { context, type InstanceContext } from "./instance-context" |
| 4 | +import { InstanceStore } from "./instance-store" |
11 | 5 |
|
12 | | -export interface InstanceContext { |
13 | | - directory: string |
14 | | - worktree: string |
15 | | - project: Project.Info |
16 | | -} |
17 | | - |
18 | | -const context = LocalContext.create<InstanceContext>("instance") |
19 | | - |
20 | | -export interface LoadInput { |
21 | | - directory: string |
22 | | - init?: () => Promise<unknown> |
23 | | - worktree?: string |
24 | | - project?: Project.Info |
25 | | -} |
26 | | - |
27 | | -export interface Interface { |
28 | | - readonly load: (input: LoadInput) => Effect.Effect<InstanceContext> |
29 | | - readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext> |
30 | | - readonly dispose: (ctx: InstanceContext) => Effect.Effect<void> |
31 | | - readonly disposeAll: () => Effect.Effect<void> |
32 | | -} |
33 | | - |
34 | | -export class InstanceStore extends Context.Service<InstanceStore, Interface>()("@opencode/InstanceStore") {} |
35 | | - |
36 | | -export const instanceStoreLayer: Layer.Layer<InstanceStore, never, Project.Service> = Layer.effect( |
37 | | - InstanceStore, |
38 | | - Effect.gen(function* () { |
39 | | - const project = yield* Project.Service |
40 | | - const cache = new Map<string, Promise<InstanceContext>>() |
41 | | - const disposal = { |
42 | | - all: undefined as Promise<void> | undefined, |
43 | | - } |
44 | | - |
45 | | - const boot = Effect.fn("InstanceStore.boot")(function* (input: LoadInput & { directory: string }) { |
46 | | - const ctx = |
47 | | - input.project && input.worktree |
48 | | - ? { |
49 | | - directory: input.directory, |
50 | | - worktree: input.worktree, |
51 | | - project: input.project, |
52 | | - } |
53 | | - : yield* project.fromDirectory(input.directory).pipe( |
54 | | - Effect.map((result) => ({ |
55 | | - directory: input.directory, |
56 | | - worktree: result.sandbox, |
57 | | - project: result.project, |
58 | | - })), |
59 | | - ) |
60 | | - const init = input.init |
61 | | - if (init) yield* Effect.promise(() => context.provide(ctx, init)) |
62 | | - return ctx |
63 | | - }) |
64 | | - |
65 | | - function track(directory: string, next: Promise<InstanceContext>) { |
66 | | - const task = next.catch((error) => { |
67 | | - if (cache.get(directory) === task) cache.delete(directory) |
68 | | - throw error |
69 | | - }) |
70 | | - cache.set(directory, task) |
71 | | - return task |
72 | | - } |
73 | | - |
74 | | - const load = Effect.fn("InstanceStore.load")(function* (input: LoadInput) { |
75 | | - const directory = AppFileSystem.resolve(input.directory) |
76 | | - const existing = cache.get(directory) |
77 | | - if (existing) return yield* Effect.promise(() => existing) |
78 | | - |
79 | | - Log.Default.info("creating instance", { directory }) |
80 | | - return yield* Effect.promise(() => track(directory, Effect.runPromise(boot({ ...input, directory })))) |
81 | | - }) |
82 | | - |
83 | | - const reload = Effect.fn("InstanceStore.reload")(function* (input: LoadInput) { |
84 | | - const directory = AppFileSystem.resolve(input.directory) |
85 | | - Log.Default.info("reloading instance", { directory }) |
86 | | - yield* Effect.promise(() => disposeInstance(directory)) |
87 | | - cache.delete(directory) |
88 | | - const next = track(directory, Effect.runPromise(boot({ ...input, directory }))) |
89 | | - |
90 | | - GlobalBus.emit("event", { |
91 | | - directory, |
92 | | - project: input.project?.id, |
93 | | - workspace: WorkspaceContext.workspaceID, |
94 | | - payload: { |
95 | | - type: "server.instance.disposed", |
96 | | - properties: { |
97 | | - directory, |
98 | | - }, |
99 | | - }, |
100 | | - }) |
101 | | - |
102 | | - return yield* Effect.promise(() => next) |
103 | | - }) |
104 | | - |
105 | | - const dispose = Effect.fn("InstanceStore.dispose")(function* (ctx: InstanceContext) { |
106 | | - Log.Default.info("disposing instance", { directory: ctx.directory }) |
107 | | - yield* Effect.promise(() => disposeInstance(ctx.directory)) |
108 | | - cache.delete(ctx.directory) |
109 | | - |
110 | | - GlobalBus.emit("event", { |
111 | | - directory: ctx.directory, |
112 | | - project: ctx.project.id, |
113 | | - workspace: WorkspaceContext.workspaceID, |
114 | | - payload: { |
115 | | - type: "server.instance.disposed", |
116 | | - properties: { |
117 | | - directory: ctx.directory, |
118 | | - }, |
119 | | - }, |
120 | | - }) |
121 | | - }) |
122 | | - |
123 | | - const disposeAll = Effect.fn("InstanceStore.disposeAll")(function* () { |
124 | | - if (disposal.all) return yield* Effect.promise(() => disposal.all!) |
125 | | - |
126 | | - disposal.all = iife(async () => { |
127 | | - Log.Default.info("disposing all instances") |
128 | | - const entries = [...cache.entries()] |
129 | | - for (const [key, value] of entries) { |
130 | | - if (cache.get(key) !== value) continue |
131 | | - |
132 | | - const ctx = await value.catch((error) => { |
133 | | - Log.Default.warn("instance dispose failed", { key, error }) |
134 | | - return undefined |
135 | | - }) |
136 | | - |
137 | | - if (!ctx) { |
138 | | - if (cache.get(key) === value) cache.delete(key) |
139 | | - continue |
140 | | - } |
141 | | - |
142 | | - if (cache.get(key) !== value) continue |
143 | | - await Effect.runPromise(dispose(ctx)) |
144 | | - } |
145 | | - }).finally(() => { |
146 | | - disposal.all = undefined |
147 | | - }) |
148 | | - |
149 | | - return yield* Effect.promise(() => disposal.all!) |
150 | | - }) |
151 | | - |
152 | | - yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore)) |
153 | | - |
154 | | - return InstanceStore.of({ |
155 | | - load, |
156 | | - reload, |
157 | | - dispose, |
158 | | - disposeAll, |
159 | | - }) |
160 | | - }), |
161 | | -) |
162 | | - |
163 | | -export const instanceStoreDefaultLayer = instanceStoreLayer.pipe(Layer.provide(Project.defaultLayer)) |
164 | | - |
165 | | -const instanceStoreRuntime = makeRuntime(InstanceStore, instanceStoreDefaultLayer) |
| 6 | +export type { InstanceContext } from "./instance-context" |
| 7 | +export type { LoadInput } from "./instance-store" |
166 | 8 |
|
167 | 9 | export const Instance = { |
168 | | - load(input: LoadInput): Promise<InstanceContext> { |
169 | | - return instanceStoreRuntime.runPromise((store) => store.load(input)) |
| 10 | + load(input: InstanceStore.LoadInput): Promise<InstanceContext> { |
| 11 | + return InstanceStore.runtime.runPromise((store) => store.load(input)) |
170 | 12 | }, |
171 | 13 | async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> { |
172 | 14 | return context.provide(await Instance.load(input), async () => input.fn()) |
@@ -215,12 +57,12 @@ export const Instance = { |
215 | 57 | return context.provide(ctx, fn) |
216 | 58 | }, |
217 | 59 | async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) { |
218 | | - return instanceStoreRuntime.runPromise((store) => store.reload(input)) |
| 60 | + return InstanceStore.runtime.runPromise((store) => store.reload(input)) |
219 | 61 | }, |
220 | 62 | async dispose() { |
221 | | - return instanceStoreRuntime.runPromise((store) => store.dispose(Instance.current)) |
| 63 | + return InstanceStore.runtime.runPromise((store) => store.dispose(Instance.current)) |
222 | 64 | }, |
223 | 65 | async disposeAll() { |
224 | | - return instanceStoreRuntime.runPromise((store) => store.disposeAll()) |
| 66 | + return InstanceStore.runtime.runPromise((store) => store.disposeAll()) |
225 | 67 | }, |
226 | 68 | } |
0 commit comments