Skip to content

Commit 545ae2e

Browse files
Apply PR #17878: refactor(snapshot): effectify SnapshotService
2 parents 3eb1e22 + 50ab371 commit 545ae2e

File tree

7 files changed

+843
-684
lines changed

7 files changed

+843
-684
lines changed

packages/opencode/script/seed-e2e.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,46 +11,52 @@ const seed = async () => {
1111
const { Instance } = await import("../src/project/instance")
1212
const { InstanceBootstrap } = await import("../src/project/bootstrap")
1313
const { Config } = await import("../src/config/config")
14+
const { disposeRuntime } = await import("../src/effect/runtime")
1415
const { Session } = await import("../src/session")
1516
const { MessageID, PartID } = await import("../src/session/schema")
1617
const { Project } = await import("../src/project/project")
1718
const { ModelID, ProviderID } = await import("../src/provider/schema")
1819
const { ToolRegistry } = await import("../src/tool/registry")
1920

20-
await Instance.provide({
21-
directory: dir,
22-
init: InstanceBootstrap,
23-
fn: async () => {
24-
await Config.waitForDependencies()
25-
await ToolRegistry.ids()
21+
try {
22+
await Instance.provide({
23+
directory: dir,
24+
init: InstanceBootstrap,
25+
fn: async () => {
26+
await Config.waitForDependencies()
27+
await ToolRegistry.ids()
2628

27-
const session = await Session.create({ title })
28-
const messageID = MessageID.ascending()
29-
const partID = PartID.ascending()
30-
const message = {
31-
id: messageID,
32-
sessionID: session.id,
33-
role: "user" as const,
34-
time: { created: now },
35-
agent: "build",
36-
model: {
37-
providerID: ProviderID.make(providerID),
38-
modelID: ModelID.make(modelID),
39-
},
40-
}
41-
const part = {
42-
id: partID,
43-
sessionID: session.id,
44-
messageID,
45-
type: "text" as const,
46-
text,
47-
time: { start: now },
48-
}
49-
await Session.updateMessage(message)
50-
await Session.updatePart(part)
51-
await Project.update({ projectID: Instance.project.id, name: "E2E Project" })
52-
},
53-
})
29+
const session = await Session.create({ title })
30+
const messageID = MessageID.ascending()
31+
const partID = PartID.ascending()
32+
const message = {
33+
id: messageID,
34+
sessionID: session.id,
35+
role: "user" as const,
36+
time: { created: now },
37+
agent: "build",
38+
model: {
39+
providerID: ProviderID.make(providerID),
40+
modelID: ModelID.make(modelID),
41+
},
42+
}
43+
const part = {
44+
id: partID,
45+
sessionID: session.id,
46+
messageID,
47+
type: "text" as const,
48+
text,
49+
time: { start: now },
50+
}
51+
await Session.updateMessage(message)
52+
await Session.updatePart(part)
53+
await Project.update({ projectID: Instance.project.id, name: "E2E Project" })
54+
},
55+
})
56+
} finally {
57+
await Instance.disposeAll().catch(() => {})
58+
await disposeRuntime().catch(() => {})
59+
}
5460
}
5561

5662
await seed()
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { ServiceMap } from "effect"
2-
import type { Project } from "@/project/project"
1+
import { ServiceMap } from "effect";
2+
import type { Project } from "@/project/project";
33

44
export declare namespace InstanceContext {
5-
export interface Shape {
6-
readonly directory: string
7-
readonly project: Project.Info
8-
}
5+
export interface Shape {
6+
readonly directory: string;
7+
readonly worktree: string;
8+
readonly project: Project.Info;
9+
}
910
}
1011

11-
export class InstanceContext extends ServiceMap.Service<InstanceContext, InstanceContext.Shape>()(
12-
"opencode/InstanceContext",
13-
) {}
12+
export class InstanceContext extends ServiceMap.Service<
13+
InstanceContext,
14+
InstanceContext.Shape
15+
>()("opencode/InstanceContext") {}
Lines changed: 74 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,83 @@
1-
import { Effect, Layer, LayerMap, ServiceMap } from "effect"
2-
import { registerDisposer } from "./instance-registry"
3-
import { InstanceContext } from "./instance-context"
4-
import { ProviderAuthService } from "@/provider/auth-service"
5-
import { QuestionService } from "@/question/service"
6-
import { PermissionService } from "@/permission/service"
7-
import { FileWatcherService } from "@/file/watcher"
8-
import { VcsService } from "@/project/vcs"
9-
import { FileTimeService } from "@/file/time"
10-
import { FormatService } from "@/format"
11-
import { FileService } from "@/file"
12-
import { SkillService } from "@/skill/skill"
13-
import { Instance } from "@/project/instance"
1+
import { Effect, Layer, LayerMap, ServiceMap } from "effect";
2+
import { FileService } from "@/file";
3+
import { FileTimeService } from "@/file/time";
4+
import { FileWatcherService } from "@/file/watcher";
5+
import { FormatService } from "@/format";
6+
import { PermissionService } from "@/permission/service";
7+
import { Instance } from "@/project/instance";
8+
import { VcsService } from "@/project/vcs";
9+
import { ProviderAuthService } from "@/provider/auth-service";
10+
import { QuestionService } from "@/question/service";
11+
import { SkillService } from "@/skill/skill";
12+
import { SnapshotService } from "@/snapshot";
13+
import { InstanceContext } from "./instance-context";
14+
import { registerDisposer } from "./instance-registry";
1415

15-
export { InstanceContext } from "./instance-context"
16+
export { InstanceContext } from "./instance-context";
1617

1718
export type InstanceServices =
18-
| QuestionService
19-
| PermissionService
20-
| ProviderAuthService
21-
| FileWatcherService
22-
| VcsService
23-
| FileTimeService
24-
| FormatService
25-
| FileService
26-
| SkillService
19+
| QuestionService
20+
| PermissionService
21+
| ProviderAuthService
22+
| FileWatcherService
23+
| VcsService
24+
| FileTimeService
25+
| FormatService
26+
| FileService
27+
| SkillService
28+
| SnapshotService;
2729

28-
function lookup(directory: string) {
29-
const project = Instance.project
30-
const ctx = Layer.sync(InstanceContext, () => InstanceContext.of({ directory, project }))
31-
return Layer.mergeAll(
32-
Layer.fresh(QuestionService.layer),
33-
Layer.fresh(PermissionService.layer),
34-
Layer.fresh(ProviderAuthService.layer),
35-
Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie),
36-
Layer.fresh(VcsService.layer),
37-
Layer.fresh(FileTimeService.layer).pipe(Layer.orDie),
38-
Layer.fresh(FormatService.layer),
39-
Layer.fresh(FileService.layer),
40-
Layer.fresh(SkillService.layer),
41-
).pipe(Layer.provide(ctx))
30+
// NOTE: LayerMap only passes the key (directory string) to lookup, but we need
31+
// the full instance context (directory, worktree, project). We read from the
32+
// legacy Instance ALS here, which is safe because lookup is only triggered via
33+
// runPromiseInstance -> Instances.get, which always runs inside Instance.provide.
34+
// This should go away once the old Instance type is removed and lookup can load
35+
// the full context directly.
36+
function lookup(_key: string) {
37+
const ctx = Layer.sync(InstanceContext, () =>
38+
InstanceContext.of(Instance.current),
39+
);
40+
return Layer.mergeAll(
41+
Layer.fresh(QuestionService.layer),
42+
Layer.fresh(PermissionService.layer),
43+
Layer.fresh(ProviderAuthService.layer),
44+
Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie),
45+
Layer.fresh(VcsService.layer),
46+
Layer.fresh(FileTimeService.layer).pipe(Layer.orDie),
47+
Layer.fresh(FormatService.layer),
48+
Layer.fresh(FileService.layer),
49+
Layer.fresh(SkillService.layer),
50+
Layer.fresh(SnapshotService.layer),
51+
).pipe(Layer.provide(ctx));
4252
}
4353

44-
export class Instances extends ServiceMap.Service<Instances, LayerMap.LayerMap<string, InstanceServices>>()(
45-
"opencode/Instances",
46-
) {
47-
static readonly layer = Layer.effect(
48-
Instances,
49-
Effect.gen(function* () {
50-
const layerMap = yield* LayerMap.make(lookup, { idleTimeToLive: Infinity })
51-
const unregister = registerDisposer((directory) => Effect.runPromise(layerMap.invalidate(directory)))
52-
yield* Effect.addFinalizer(() => Effect.sync(unregister))
53-
return Instances.of(layerMap)
54-
}),
55-
)
54+
export class Instances extends ServiceMap.Service<
55+
Instances,
56+
LayerMap.LayerMap<string, InstanceServices>
57+
>()("opencode/Instances") {
58+
static readonly layer = Layer.effect(
59+
Instances,
60+
Effect.gen(function* () {
61+
const layerMap = yield* LayerMap.make(lookup, {
62+
idleTimeToLive: Infinity,
63+
});
64+
const unregister = registerDisposer((directory) =>
65+
Effect.runPromise(layerMap.invalidate(directory)),
66+
);
67+
yield* Effect.addFinalizer(() => Effect.sync(unregister));
68+
return Instances.of(layerMap);
69+
}),
70+
);
5671

57-
static get(directory: string): Layer.Layer<InstanceServices, never, Instances> {
58-
return Layer.unwrap(Instances.use((map) => Effect.succeed(map.get(directory))))
59-
}
72+
static get(
73+
directory: string,
74+
): Layer.Layer<InstanceServices, never, Instances> {
75+
return Layer.unwrap(
76+
Instances.use((map) => Effect.succeed(map.get(directory))),
77+
);
78+
}
6079

61-
static invalidate(directory: string): Effect.Effect<void, never, Instances> {
62-
return Instances.use((map) => map.invalidate(directory))
63-
}
80+
static invalidate(directory: string): Effect.Effect<void, never, Instances> {
81+
return Instances.use((map) => map.invalidate(directory));
82+
}
6483
}

packages/opencode/src/effect/runtime.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ export const runtime = ManagedRuntime.make(
1212
export function runPromiseInstance<A, E>(effect: Effect.Effect<A, E, InstanceServices>) {
1313
return runtime.runPromise(effect.pipe(Effect.provide(Instances.get(Instance.directory))))
1414
}
15+
16+
export function disposeRuntime() {
17+
return runtime.dispose()
18+
}

0 commit comments

Comments
 (0)