Second review round on the contained-group composition:
- Ownership scanned only `insert` rows, so an external bundle could set a
built-in row's id as a child through `{ id: own, config: [...] }` and the
Loader re-parented that row under the bundle while `originOf` still named
the built-in layer. The rows an id-targeted patch sets as a group's config
now count as the layer's own: an external bundle that sets an id another
layer owns is left out whole, a built-in layer that takes another layer's
id fails loud, and a layer restating its own child is not a repeat.
- `recompose` read the committed profile before awaiting the update, so a
watcher firing while a bundle was being enabled composed the tree before
the enable and applied it after; the plugin vanished while `layers` kept
listing it. Recompositions now queue, each reading what the previous one
committed, and a rejection stays its own caller's outcome.
- A patch that replaced a built-in target's config removed the wrapper the
bundle had put there, yet later inserts still targeted the detached
wrapper and vanished. The composition forgets the wrapper on such an
override and creates a new one under the same id.
- Generated wrapper ids bypassed the duplicate check, so a bundle declaring
a row spelled like a wrapper produced two nodes with one id. Declared and
generated ids now go through one registration.
- Failure records outlived their rows when a bundle update dropped a failed
row without unmounting the group, or when a pending row sat in a plain
nested group whose id the record named. A contained group now drops the
records of rows its configuration no longer names after each update, and
the audit records every row under the contained group that isolates it.
- `applyEntryPatches` assigned override values by reference, so applying a
patch list carried a later insert's rows back into the patch object; the
vendored include now clones object-valued overrides as it does inserts.
52 lines
2.1 KiB
TypeScript
52 lines
2.1 KiB
TypeScript
/**
|
|
* The one walk over the rows a patch list introduces. Composition, ownership,
|
|
* the package probe, and patch loading all read those rows and their group
|
|
* children; sharing the walk keeps them reading the same tree.
|
|
* @module @deepseek-ai/dsh-app-boot/patch-rows
|
|
*/
|
|
|
|
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
|
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
|
|
|
/** How a patch introduced a row: inserted it, or set it as a group's `config`. */
|
|
export type PatchRowSource = 'insert' | 'config'
|
|
|
|
/** Whether a config item is a row: a plain object naming a module. */
|
|
function isRowLike(value: unknown): value is EntryOptions {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && typeof (value as { name?: unknown }).name === 'string'
|
|
}
|
|
|
|
/**
|
|
* Visit one row and, for a group with a list config, each of its children,
|
|
* parents before children, in written order.
|
|
* @param row - the row.
|
|
* @param visit - called once per row.
|
|
*/
|
|
export function visitRowTree(row: EntryOptions, visit: (row: EntryOptions) => void): void {
|
|
visit(row)
|
|
if (row.group && Array.isArray(row.config)) {
|
|
for (const child of row.config as EntryOptions[]) visitRowTree(child, visit)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Visit every row a patch list introduces, at any depth, in written order:
|
|
* the rows a patch inserts, and the rows an id-targeted patch sets as a
|
|
* group's `config`, which mount as that group's children the same way an
|
|
* insert's do.
|
|
* @param patches - the patch list.
|
|
* @param visit - called once per row, with how the patch introduced it.
|
|
*/
|
|
export function visitPatchRows(patches: readonly PatchOptions[], visit: (row: EntryOptions, source: PatchRowSource) => void): void {
|
|
for (const patch of patches) {
|
|
if (patch.insert !== undefined) {
|
|
for (const row of patch.insert) visitRowTree(row, (entry) => { visit(entry, 'insert') })
|
|
continue
|
|
}
|
|
if (patch.id === undefined || !Array.isArray(patch.config)) continue
|
|
for (const item of patch.config as unknown[]) {
|
|
if (isRowLike(item)) visitRowTree(item, (entry) => { visit(entry, 'config') })
|
|
}
|
|
}
|
|
}
|