fix(core): batch plugin shutdown

This commit is contained in:
Dax Raad 2026-06-22 09:04:29 -04:00
parent 79b55d4db8
commit cf31029350
2 changed files with 45 additions and 3 deletions

View file

@ -103,8 +103,16 @@ export const layer = Layer.effect(
}
}[keyof Hooks][] = []
const events = yield* EventV2.Service
const scope = yield* Scope.Scope
const locks = KeyedMutex.makeUnsafe<ID>()
const scope = yield* Scope.make()
// One registry-owned scope lets shutdown remove every plugin transform in one batch.
yield* Effect.addFinalizer((exit) =>
Effect.gen(function* () {
hooks = []
yield* State.batch(Scope.close(scope, exit))
}),
)
const svc = Service.of({
add: Effect.fn("Plugin.add")(function* (input) {
@ -112,7 +120,7 @@ export const layer = Layer.effect(
yield* locks.withLock(id)(
Effect.gen(function* () {
const existing = hooks.find((item) => item.id === id)
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
const childScope = yield* Scope.fork(scope)
const result = yield* input.effect.pipe(
Scope.provide(childScope),
@ -181,7 +189,7 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const existing = hooks.find((item) => item.id === id)
hooks = hooks.filter((item) => item.id !== id)
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
}),
)
}),

View file

@ -46,6 +46,40 @@ describe("PluginV2", () => {
}),
)
it.effect("batches plugin state rebuilds when the registry layer finalizes", () =>
Effect.gen(function* () {
let finalized = 0
const values = State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
finalize: () => Effect.sync(() => finalized++),
})
const layerScope = yield* Scope.fork(yield* Scope.Scope)
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
yield* State.batch(
Effect.forEach(
["first", "second"],
(id) =>
plugin.add({
id: PluginV2.ID.make(id),
effect: values
.transform((editor) => {
editor.add(id)
})
.pipe(Effect.asVoid),
}),
{ discard: true },
),
)
finalized = 0
yield* Scope.close(layerScope, Exit.void)
expect(values.get().values).toEqual([])
expect(finalized).toBe(1)
}),
)
it.effect("serializes same-ID additions and leaves one removable attachment", () =>
Effect.gen(function* () {
const values = state()