fix(core): preserve unchanged plugin prefix (#46857)

This commit is contained in:
Dax 2026-09-02 12:50:14 -04:00 committed by GitHub
parent 33dd4e3ba8
commit 36095decd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 27 deletions

View file

@ -98,17 +98,17 @@ const layer = Layer.effect(
() =>
lock.withPermit(
Effect.gen(function* () {
if (
active.size === definitions.length &&
Array.from(active.values()).every((entry, index) => {
const definition = definitions[index]
return entry.plugin.id === definition?.id && entry.plugin.revision === definition.revision
})
) {
for (const definition of definitions) {
const entry = active.get(definition.id)
if (entry) active.set(definition.id, { ...entry, plugin: definition })
}
const current = Array.from(active.values())
const changed = definitions.findIndex((definition, index) => {
const entry = current[index]
return entry?.plugin.id !== definition.id || entry.plugin.revision !== definition.revision
})
const prefix = changed === -1 ? definitions.length : changed
for (const definition of definitions.slice(0, prefix)) {
const entry = active.get(definition.id)
if (entry) active.set(definition.id, { ...entry, plugin: definition })
}
if (prefix === definitions.length && active.size === definitions.length) {
const nextInventory = [...definitions.map(activeInfo), ...failures]
if (JSON.stringify(inventory) === JSON.stringify(nextInventory)) return
inventory = nextInventory
@ -118,12 +118,19 @@ const layer = Layer.effect(
yield* State.batch(
Effect.gen(function* () {
const nextInventory: Plugin.Info[] = []
for (const definition of definitions) {
const previous = active.get(definition.id)
active.delete(definition.id)
if (previous) yield* Scope.close(previous.scope, Exit.void)
// Registrations are ordered by setup, so only the unchanged prefix can stay alive.
const previous = new Map(Array.from(active.entries()).slice(prefix))
yield* Effect.forEach(
Array.from(previous.entries()).toReversed(),
([id, entry]) =>
Effect.gen(function* () {
active.delete(id)
yield* Scope.close(entry.scope, Exit.void)
}),
{ discard: true },
)
const nextInventory = definitions.slice(0, prefix).map(activeInfo)
for (const definition of definitions.slice(prefix)) {
const loaded = yield* load(definition)
if (loaded.scope !== undefined) {
active.set(definition.id, { plugin: definition, scope: loaded.scope })
@ -137,10 +144,11 @@ const layer = Layer.effect(
features: { server: true, ...definition.features },
})
if (!previous) continue
const restored = yield* load(previous.plugin)
const fallback = previous.get(definition.id)
if (!fallback) continue
const restored = yield* load(fallback.plugin)
if (restored.scope !== undefined) {
active.set(definition.id, { plugin: previous.plugin, scope: restored.scope })
active.set(definition.id, { plugin: fallback.plugin, scope: restored.scope })
continue
}
yield* Effect.logError("failed to restore plugin; deactivating", {
@ -148,13 +156,6 @@ const layer = Layer.effect(
})
}
const removed = Array.from(active.entries())
.filter(([id]) => !ids.has(id))
.toReversed()
removed.forEach(([id]) => active.delete(id))
yield* Effect.forEach(removed, ([, entry]) => Scope.close(entry.scope, Exit.void), {
discard: true,
})
inventory = [...nextInventory, ...failures]
}),
)

View file

@ -13,6 +13,76 @@ import { PluginTestLayer } from "./plugin/fixture"
const it = testEffect(PluginTestLayer)
for (const scenario of [
{
name: "starts only the appended plugin",
before: ["a", "b"],
after: ["a", "b", "c"],
expected: ["start:c:1"],
},
{
name: "restarts the suffix after an insertion",
before: ["a", "b", "c"],
after: ["a", "x", "b", "c"],
expected: ["stop:c:1", "stop:b:1", "start:x:1", "start:b:1", "start:c:1"],
},
{
name: "restarts the suffix after a revision changes",
before: ["a", "b", "c"],
after: ["a", "b", "c"],
updated: "b",
expected: ["stop:c:1", "stop:b:1", "start:b:2", "start:c:1"],
},
{
name: "stops only the removed trailing plugin",
before: ["a", "b", "c"],
after: ["a", "b"],
expected: ["stop:c:1"],
},
]) {
it.effect(scenario.name, () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const events: string[] = []
const plugin = (id: string, revision = "1"): Plugin.Generation => ({
id,
revision,
effect: () =>
Effect.gen(function* () {
events.push(`start:${id}:${revision}`)
yield* Effect.addFinalizer(() => Effect.sync(() => events.push(`stop:${id}:${revision}`)))
}),
})
yield* plugins.activate(scenario.before.map((id) => plugin(id)))
events.length = 0
yield* plugins.activate(scenario.after.map((id) => plugin(id, id === scenario.updated ? "2" : "1")))
expect(events).toEqual(scenario.expected)
expect((yield* plugins.list()).map((plugin) => plugin.id)).toEqual(scenario.after.map((id) => Plugin.ID.make(id)))
}),
)
}
it.effect("updates inventory metadata without restarting an unchanged generation", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
let loads = 0
const plugin = {
id: "metadata",
revision: "1",
source: { type: "package" as const, target: "fixture" },
effect: () => Effect.sync(() => loads++),
}
yield* plugins.activate([plugin])
yield* plugins.activate([{ ...plugin, source: { ...plugin.source, outdated: true } }])
expect(loads).toBe(1)
expect((yield* plugins.list())[0]?.source).toEqual({ type: "package", target: "fixture", outdated: true })
}),
)
it.live("loads a local plugin with its configured options", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service