mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-24 06:45:22 +00:00
use Effect.cached for global config, remove resetGlobal
- Replace module-level _cachedGlobal with Effect.cached inside the layer — concurrent getGlobal() callers share the same fiber instead of racing - Remove resetGlobal() facade — tests use invalidate() instead - Document Effect.cached pattern in AGENTS.md and effect-migration.md - Mark Config as done in migration checklist
This commit is contained in:
@@ -47,6 +47,10 @@ See `specs/effect-migration.md` for the compact pattern reference and examples.
|
||||
- Prefer `Path.Path`, `Config`, `Clock`, and `DateTime` when those concerns are already inside Effect code.
|
||||
- For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition.
|
||||
|
||||
## Effect.cached for deduplication
|
||||
|
||||
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect-migration.md` for the full pattern.
|
||||
|
||||
## Instance.bind — ALS for native callbacks
|
||||
|
||||
`Instance.bind(fn)` captures the current Instance AsyncLocalStorage context and restores it synchronously when called.
|
||||
|
||||
@@ -109,6 +109,31 @@ const cache =
|
||||
|
||||
The key insight: don't split init into a separate method with a `started` flag. Put everything in the `InstanceState.make` closure and let `ScopedCache` handle the run-once semantics.
|
||||
|
||||
## Effect.cached for deduplication
|
||||
|
||||
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation. It memoizes the result and deduplicates concurrent fibers — second caller joins the first caller's fiber instead of starting a new one.
|
||||
|
||||
```ts
|
||||
// Inside the layer — yield* to initialize the memo
|
||||
let cached = yield* Effect.cached(loadExpensive())
|
||||
|
||||
const get = Effect.fn("Foo.get")(function* () {
|
||||
return yield* cached // concurrent callers share the same fiber
|
||||
})
|
||||
|
||||
// To invalidate: swap in a fresh memo
|
||||
const invalidate = Effect.fn("Foo.invalidate")(function* () {
|
||||
cached = yield* Effect.cached(loadExpensive())
|
||||
})
|
||||
```
|
||||
|
||||
Prefer `Effect.cached` over these patterns:
|
||||
- Storing a `Fiber.Fiber | undefined` with manual check-and-fork (e.g. `file/index.ts` `ensure`)
|
||||
- Storing a `Promise<void>` task for deduplication (e.g. `skill/index.ts` `ensure`)
|
||||
- `let cached: X | undefined` with check-and-load (races when two callers see `undefined` before either resolves)
|
||||
|
||||
`Effect.cached` handles the run-once + concurrent-join semantics automatically. For invalidatable caches, reassign with `yield* Effect.cached(...)` — the old memo is discarded.
|
||||
|
||||
## Scheduled Tasks
|
||||
|
||||
For loops or periodic work, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition.
|
||||
@@ -167,7 +192,7 @@ Still open and likely worth migrating:
|
||||
- [x] `Worktree`
|
||||
- [ ] `Bus`
|
||||
- [x] `Command`
|
||||
- [ ] `Config`
|
||||
- [x] `Config`
|
||||
- [ ] `Session`
|
||||
- [ ] `SessionProcessor`
|
||||
- [ ] `SessionPrompt`
|
||||
|
||||
@@ -1136,8 +1136,6 @@ export namespace Config {
|
||||
}),
|
||||
)
|
||||
|
||||
let _cachedGlobal: Info | undefined
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
@@ -1243,13 +1241,13 @@ export namespace Config {
|
||||
)
|
||||
}
|
||||
|
||||
_cachedGlobal = result
|
||||
return result
|
||||
})
|
||||
|
||||
let cachedGlobal = yield* Effect.cached(loadGlobal())
|
||||
|
||||
const getGlobal = Effect.fn("Config.getGlobal")(function* () {
|
||||
if (_cachedGlobal) return _cachedGlobal
|
||||
return yield* loadGlobal()
|
||||
return yield* cachedGlobal
|
||||
})
|
||||
|
||||
const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
|
||||
@@ -1453,7 +1451,7 @@ export namespace Config {
|
||||
})
|
||||
|
||||
const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) {
|
||||
_cachedGlobal = undefined
|
||||
cachedGlobal = yield* Effect.cached(loadGlobal())
|
||||
const task = Instance.disposeAll()
|
||||
.catch(() => undefined)
|
||||
.finally(() =>
|
||||
@@ -1513,10 +1511,6 @@ export namespace Config {
|
||||
return runPromise((svc) => svc.getGlobal())
|
||||
}
|
||||
|
||||
export function resetGlobal() {
|
||||
_cachedGlobal = undefined
|
||||
}
|
||||
|
||||
export async function update(config: Info) {
|
||||
return runPromise((svc) => svc.update(config))
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ async function check(map: (dir: string) => string) {
|
||||
await using tmp = await tmpdir({ git: true, config: { snapshot: true } })
|
||||
const prev = Global.Path.config
|
||||
;(Global.Path as { config: string }).config = globalTmp.path
|
||||
Config.resetGlobal()
|
||||
await Config.invalidate()
|
||||
try {
|
||||
await writeConfig(globalTmp.path, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
@@ -52,7 +52,7 @@ async function check(map: (dir: string) => string) {
|
||||
} finally {
|
||||
await Instance.disposeAll()
|
||||
;(Global.Path as { config: string }).config = prev
|
||||
Config.resetGlobal()
|
||||
await Config.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user