Skip to content

Commit cf31029

Browse files
committed
fix(core): batch plugin shutdown
1 parent 79b55d4 commit cf31029

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

packages/core/src/plugin.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,24 @@ export const layer = Layer.effect(
103103
}
104104
}[keyof Hooks][] = []
105105
const events = yield* EventV2.Service
106-
const scope = yield* Scope.Scope
107106
const locks = KeyedMutex.makeUnsafe<ID>()
107+
const scope = yield* Scope.make()
108+
109+
// One registry-owned scope lets shutdown remove every plugin transform in one batch.
110+
yield* Effect.addFinalizer((exit) =>
111+
Effect.gen(function* () {
112+
hooks = []
113+
yield* State.batch(Scope.close(scope, exit))
114+
}),
115+
)
108116

109117
const svc = Service.of({
110118
add: Effect.fn("Plugin.add")(function* (input) {
111119
const id = ID.make(input.id)
112120
yield* locks.withLock(id)(
113121
Effect.gen(function* () {
114122
const existing = hooks.find((item) => item.id === id)
115-
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
123+
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
116124
const childScope = yield* Scope.fork(scope)
117125
const result = yield* input.effect.pipe(
118126
Scope.provide(childScope),
@@ -181,7 +189,7 @@ export const layer = Layer.effect(
181189
Effect.gen(function* () {
182190
const existing = hooks.find((item) => item.id === id)
183191
hooks = hooks.filter((item) => item.id !== id)
184-
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
192+
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
185193
}),
186194
)
187195
}),

packages/core/test/plugin.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,40 @@ describe("PluginV2", () => {
4646
}),
4747
)
4848

49+
it.effect("batches plugin state rebuilds when the registry layer finalizes", () =>
50+
Effect.gen(function* () {
51+
let finalized = 0
52+
const values = State.create({
53+
initial: () => ({ values: [] as string[] }),
54+
draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
55+
finalize: () => Effect.sync(() => finalized++),
56+
})
57+
const layerScope = yield* Scope.fork(yield* Scope.Scope)
58+
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
59+
60+
yield* State.batch(
61+
Effect.forEach(
62+
["first", "second"],
63+
(id) =>
64+
plugin.add({
65+
id: PluginV2.ID.make(id),
66+
effect: values
67+
.transform((editor) => {
68+
editor.add(id)
69+
})
70+
.pipe(Effect.asVoid),
71+
}),
72+
{ discard: true },
73+
),
74+
)
75+
finalized = 0
76+
77+
yield* Scope.close(layerScope, Exit.void)
78+
expect(values.get().values).toEqual([])
79+
expect(finalized).toBe(1)
80+
}),
81+
)
82+
4983
it.effect("serializes same-ID additions and leaves one removable attachment", () =>
5084
Effect.gen(function* () {
5185
const values = state()

0 commit comments

Comments
 (0)