effectify Plugin service (#18570)

This commit is contained in:
Kit Langton
2026-03-21 20:50:22 -04:00
committed by GitHub
parent 0e0e7a4a4b
commit 3236f228fb
2 changed files with 197 additions and 108 deletions

View File

@@ -75,6 +75,31 @@ export const ZodInfo = zod(Info) // derives z.ZodType from Schema.Union
See `Auth.ZodInfo` for the canonical example.
## InstanceState init patterns
The `InstanceState.make` init callback receives a `Scope`, so you can use `Effect.acquireRelease`, `Effect.addFinalizer`, and `Effect.forkScoped` inside it. Resources acquired this way are automatically cleaned up when the instance is disposed or invalidated by `ScopedCache`. This makes it the right place for:
- **Subscriptions**: Use `Effect.acquireRelease` to subscribe and auto-unsubscribe:
```ts
const cache = yield* InstanceState.make<State>(
Effect.fn("Foo.state")(function* (ctx) {
// ... load state ...
yield* Effect.acquireRelease(
Effect.sync(() => Bus.subscribeAll((event) => { /* handle */ })),
(unsub) => Effect.sync(unsub),
)
return { /* state */ }
}),
)
```
- **Background fibers**: Use `Effect.forkScoped` — the fiber is interrupted on disposal.
- **Side effects at init**: Config notification, event wiring, etc. all belong in the init closure. Callers just do `InstanceState.get(cache)` to trigger everything, and `ScopedCache` deduplicates automatically.
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.
## Scheduled Tasks
For loops or periodic work, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition.
@@ -127,7 +152,7 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
Still open and likely worth migrating:
- [ ] `Plugin`
- [x] `Plugin`
- [ ] `ToolRegistry`
- [ ] `Pty`
- [ ] `Worktree`