fix(boot): commit a recomposition only once the root include accepts it
`ProfileRuntime.recompose({ reloadBundles: true })` replaced its profile
before the Loader had accepted the update, so a rejected recomposition left
`current`, `layers`, and `originOf` describing a stack that never mounted.
The user patch watchers took a second path through the launcher's
`composeLive`, which wrote the new conflicts into the failure registry
before the update and kept composing from the runtime's stale profile.
Conflict records shared the registry with row failures under a fabricated
entry id and a group id that meant three different things, so the plugin
inventory attributed a conflict to the layer that won the id, and a bundle
disabled after failing kept its import and apply records forever.
The runtime now holds one committed composition — profile, stack ownership,
conflicts — and publishes a candidate only after `entry.update` resolves;
`originOf` reads the committed owners instead of recomputing them. The
watchers call `recompose` through a `reapply` callback, so every live change
goes through the one entry point. Conflicts live on the runtime with their
messages and never enter `pluginFailures`, whose records name rows that
reached the Loader; the inventory lists them under the layer that lost. A
contained group clears its rows' records when it unmounts.
This commit is contained in:
1 parent
0914a152ff
commit
0caceb2ced
18 files changed
+249
-219
No files matched your search
@@ -29,7 +29,6 @@ import {
|
||||
loadProfile,
|
||||
PROFILE_PATCH_FILENAME,
|
||||
ProfileRuntime,
|
||||
recordRowConflicts,
|
||||
rootIncludeEntry,
|
||||
warnNestedFiberFailures,
|
||||
watchUserPatches,
|
||||
@@ -275,17 +274,17 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
const stack = composeProfileStack(NAME, profile.layers, [...userLayersOf(profile), ...composed.overlays])
|
||||
return { ...stack, patches: structuredClone(stack.patches) }
|
||||
}
|
||||
// Once the profile runtime is mounted its profile is the current one: a
|
||||
// bundle enabled since boot lives only there. The watcher path has no
|
||||
// runtime to record conflicts on beyond the registry the runtime shares, so
|
||||
// it records them itself once the tree accepted the update.
|
||||
const composeLive = (): PatchOptions[] => {
|
||||
const stack = composeFor(app.runtime?.current ?? composed.profile)
|
||||
if (app.current !== undefined) recordRowConflicts(app.current, stack.conflicts)
|
||||
return stack.patches
|
||||
// Every recomposition after boot — a watched user file, a bundle enabled
|
||||
// or installed — goes through the profile runtime, which publishes the
|
||||
// profile, the row ownership, and the conflicts only once the tree
|
||||
// accepted the update.
|
||||
const reapply = async (): Promise<void> => {
|
||||
const runtime = app.runtime
|
||||
if (runtime === undefined) throw new Error(`${NAME}: user patch reload needs the profile runtime`)
|
||||
await runtime.recompose()
|
||||
}
|
||||
for (const conflict of composed.stack.conflicts) process.stderr.write(`${NAME}: ${formatRowConflict(conflict)}\n`)
|
||||
// Cloned for the same insert-aliasing reason as composeLive: the boot
|
||||
// Cloned for the same insert-aliasing reason as composeFor: the boot
|
||||
// application must not mutate the objects later reloads recompose from.
|
||||
const ctx = await boot(NAME, rootConfig, structuredClone(composed.stack.patches), (hostCtx) => {
|
||||
app.current = hostCtx
|
||||
@@ -307,9 +306,9 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
uninstallRuntimeGuards = installRuntimeGuards(NAME, (line) => { process.stderr.write(`${line}\n`) })
|
||||
if (!signalShutdown.signal.aborted && ctx.fiber.state === FiberState.ACTIVE && ctx.get('loader') !== undefined) {
|
||||
warnNestedFiberFailures(ctx, NAME, (line) => { process.stderr.write(`${line}\n`) })
|
||||
recordRowConflicts(ctx, composed.stack.conflicts)
|
||||
await ctx.plugin(ProfileRuntime, {
|
||||
profile: composed.profile,
|
||||
stack: composed.stack,
|
||||
loadProfile: () => prepareProfile(options.profile),
|
||||
compose: composeFor,
|
||||
rootEntry: () => rootIncludeEntry(ctx),
|
||||
@@ -343,16 +342,8 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
}
|
||||
await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-hmr', config: { root: [] } })
|
||||
}
|
||||
await watchUserPatches(ctx, {
|
||||
binName: NAME,
|
||||
filename: composed.profile.patchPath,
|
||||
compose: composeLive,
|
||||
})
|
||||
await watchUserPatches(ctx, {
|
||||
binName: NAME,
|
||||
filename: homePatchPath(),
|
||||
compose: composeLive,
|
||||
})
|
||||
await watchUserPatches(ctx, { binName: NAME, filename: composed.profile.patchPath, reapply })
|
||||
await watchUserPatches(ctx, { binName: NAME, filename: homePatchPath(), reapply })
|
||||
} catch (error) {
|
||||
suppressShutdownError(ctx, signalShutdown.signal, error)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user