You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: packages/opencode/specs/effect-migration.md
+53-54Lines changed: 53 additions & 54 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,18 +4,18 @@ Practical reference for new and migrated Effect code in `packages/opencode`.
4
4
5
5
## Choose scope
6
6
7
-
Use the shared runtime for process-wide services with one lifecycle for the whole app.
7
+
Use `InstanceState` (from `src/effect/instance-state.ts`) for services that need per-directory state, per-instance cleanup, or project-bound background work. InstanceState uses a `ScopedCache` keyed by directory, so each open project gets its own copy of the state that is automatically cleaned up on disposal.
8
8
9
-
Use `src/effect/instances.ts` for services that are created per directory or need `InstanceContext`, per-project state, or per-instance cleanup.
9
+
Use `makeRunPromise` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`.
10
10
11
-
-Shared runtime: config readers, stateless helpers, global clients
12
-
- Instance-scoped: watchers, per-project caches, session state, project-bound background work
11
+
-Global services (no per-directory state): Account, Auth, Installation, Truncate
12
+
- Instance-scoped (per-directory state via InstanceState): File, FileTime, FileWatcher, Format, Permission, Question, Skill, Snapshot, Vcs, ProviderAuth
13
13
14
-
Rule of thumb: if two open directories should not share one copy of the service, it belongs in `Instances`.
14
+
Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`.
15
15
16
16
## Service shape
17
17
18
-
For a fully migrated module, use the public namespace directly:
18
+
Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions:
- Keep `Interface`, `Service`, `layer`, and `defaultLayer` on the owning namespace
46
-
-Export `defaultLayer` only when wiring dependencies is useful
47
-
-Use the direct namespace form once the module is fully migrated
48
-
49
-
## Temporary mixed-mode pattern
60
+
- Keep everything in one namespace, one file — no separate `service.ts` / `index.ts` split
61
+
-`runPromise` goes inside the namespace (not exported unless tests need it)
62
+
-Facade functions are plain `async function` — no `fn()` wrappers
63
+
- Use `Effect.fn("Namespace.method")` for all Effect functions (for tracing)
64
+
- No `Layer.fresh` — InstanceState handles per-directory isolation
50
65
51
-
Prefer a single namespace whenever possible.
66
+
## Schema → Zod interop
52
67
53
-
Use a `*Effect` namespace only when there is a real mixed-mode split, usually because a legacy boundary facade still exists or because merging everything immediately would create awkward cycles.
68
+
When a service uses Effect Schema internally but needs Zod schemas for the HTTP layer, derive Zod from Schema using the `zod()` helper from `@/util/effect-zod`:
0 commit comments