Просмотр исходного кода

fix(core): batch plugin shutdown

Dax Raad 2 месяцев назад
Родитель
Сommit
cf31029350
2 измененных файлов с 45 добавлено и 3 удалено
  1. 11 3
      packages/core/src/plugin.ts
  2. 34 0
      packages/core/test/plugin.test.ts

+ 11 - 3
packages/core/src/plugin.ts

@@ -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)
           }),
         )
       }),

+ 34 - 0
packages/core/test/plugin.test.ts

@@ -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()