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
+46-17Lines changed: 46 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -46,35 +46,64 @@ Rules:
46
46
- Export `defaultLayer` only when wiring dependencies is useful
47
47
- Use the direct namespace form once the module is fully migrated
48
48
49
-
## Temporary mixed-mode pattern
49
+
## Service / Facade split
50
50
51
-
Prefer a single namespace whenever possible.
51
+
Migrated services are split into two files:
52
52
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.
53
+
-**Service module** (`service.ts`, `*-service.ts`, or `*-effect.ts`) — contains `Interface`, `Service`, `layer`, `defaultLayer`, schemas, types, errors, and pure helpers. Must **never** import `@/effect/runtime`.
54
+
-**Facade** (`index.ts`) — thin async wrapper that calls `runInstance()` or `run()` from `@/effect/run`. Contains **only** runtime-backed convenience functions. No re-exports of schemas, types, Service, layer, or anything else.
1.**No eager import of `@/effect/runtime`** — use `run()` / `runInstance()` from `@/effect/run` instead, which lazy-imports the runtime.
59
+
2.**No eager import of the service module** if the service is in the circular dependency SCC (auth, account, skill, truncate). Use the lazy `svc()` pattern:
3.**No value re-exports** — consumers that need schemas, types, `Service`, or `layer` import from the service module directly.
64
+
4.**Only async wrapper functions** — each function awaits `svc()` and passes an Effect to `run()` / `runInstance()`.
62
65
63
-
exportconst layer =Layer.effect(...)
64
-
}
65
-
```
66
+
### Why
67
+
68
+
Bun's bundler flattens all modules into a single file. When a circular dependency exists (`runtime → instances → services → config → auth → runtime`), the bundler picks an arbitrary evaluation order. If a facade eagerly imports `@/effect/runtime` or re-exports values from a service in the SCC, those values may be `undefined` when accessed at module load time — causing `undefined is not an object` crashes.
66
69
67
-
Then keep the old boundary thin:
70
+
The lazy `svc()` + `run()` pattern defers all access to call time, when all modules have finished initializing.
0 commit comments