fix(cordis): make config reload transactional
This commit is contained in:
192
vendor/loader/src/config/entry.ts
vendored
192
vendor/loader/src/config/entry.ts
vendored
@@ -21,6 +21,11 @@ export interface EntryOptions {
|
||||
inject?: Inject | null
|
||||
}
|
||||
|
||||
function updateError(stage: 'import' | 'dispose' | 'apply' | 'rollback', options: EntryOptions, cause: unknown) {
|
||||
const detail = cause instanceof Error ? cause.message : String(cause)
|
||||
return new Error(`failed to ${stage} loader entry ${options.id} (${options.name}): ${detail}`, { cause })
|
||||
}
|
||||
|
||||
function takeEntries(object: {}, keys: string[]) {
|
||||
const result: [string, any][] = []
|
||||
for (const key of keys) {
|
||||
@@ -38,6 +43,11 @@ function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['
|
||||
return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
|
||||
}
|
||||
|
||||
function replaceKeys<T extends {}>(target: T, source: T): T {
|
||||
for (const key of Object.keys(target)) Reflect.deleteProperty(target, key)
|
||||
return Object.assign(target, source)
|
||||
}
|
||||
|
||||
/** One configured plugin node inside an `EntryTree`. */
|
||||
export class Entry {
|
||||
static readonly key = Symbol.for('cordis.entry')
|
||||
@@ -51,6 +61,7 @@ export class Entry {
|
||||
public subtree?: EntryTree
|
||||
|
||||
_initTask?: Promise<void>
|
||||
_disposing = 0
|
||||
|
||||
constructor(public loader: Loader) {
|
||||
this.ctx = loader.ctx.extend({ [Entry.key]: this })
|
||||
@@ -71,13 +82,18 @@ export class Entry {
|
||||
|
||||
/** True when this entry or any owning parent entry is disabled. */
|
||||
get disabled() {
|
||||
return this._disabled(this.options)
|
||||
}
|
||||
|
||||
private _disabled(options: EntryOptions) {
|
||||
// group is always enabled
|
||||
if (this.options.group) return false
|
||||
let entry: Entry | undefined = this
|
||||
do {
|
||||
if (options.group) return false
|
||||
if (options.disabled) return true
|
||||
let entry = this.parent.ctx.fiber.entry
|
||||
while (entry) {
|
||||
if (entry.options.disabled) return true
|
||||
entry = entry.parent.ctx.fiber.entry
|
||||
} while (entry)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -90,12 +106,12 @@ export class Entry {
|
||||
return interpolate(this.ctx, this.options.config)
|
||||
}
|
||||
|
||||
private _patchContext(diff: string[]) {
|
||||
this.context.waterfall('loader/patch-context', this, () => {
|
||||
private async _patchContext(diff: string[]) {
|
||||
await this.context.waterfall('loader/patch-context', this, async () => {
|
||||
Object.setPrototypeOf(this.ctx, this.parent.ctx)
|
||||
|
||||
if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
|
||||
this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
|
||||
await this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -106,41 +122,122 @@ export class Entry {
|
||||
await this.init()
|
||||
}
|
||||
|
||||
async _dispose(fiber = this.fiber) {
|
||||
if (!fiber) return
|
||||
if (this.fiber === fiber) this.fiber = undefined
|
||||
this._disposing += 1
|
||||
try {
|
||||
await fiber.dispose()
|
||||
} finally {
|
||||
this._disposing -= 1
|
||||
}
|
||||
}
|
||||
|
||||
/** Merge new options, restart as needed, and persist through the parent tree. */
|
||||
async update(options: Partial<EntryOptions>, create = false, force = false) {
|
||||
const legacy = { ...this.options }
|
||||
|
||||
// step 1: update options
|
||||
if (create) {
|
||||
this.options = options as EntryOptions
|
||||
} else {
|
||||
const previousOptions = this.options
|
||||
const legacy = { ...previousOptions }
|
||||
const candidate = create ? options as EntryOptions : { ...previousOptions }
|
||||
if (!create) {
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (isNullable(value)) {
|
||||
delete this.options[key]
|
||||
delete candidate[key as keyof EntryOptions]
|
||||
} else {
|
||||
this.options[key] = value
|
||||
candidate[key as keyof EntryOptions] = value as never
|
||||
}
|
||||
}
|
||||
}
|
||||
sortKeys(this.options)
|
||||
sortKeys(candidate)
|
||||
|
||||
// step 2: execute
|
||||
if (this.disabled) {
|
||||
this.fiber?.dispose()
|
||||
const diff = Object
|
||||
.keys({ ...candidate, ...legacy })
|
||||
.filter(key => !deepEqual(candidate[key as keyof EntryOptions], legacy[key as keyof EntryOptions]))
|
||||
if (!diff.length && !force) return
|
||||
|
||||
const commit = () => {
|
||||
if (create) return
|
||||
this.options = replaceKeys(previousOptions, candidate)
|
||||
}
|
||||
|
||||
const previous = this.fiber
|
||||
if (!previous?.uid) {
|
||||
this.fiber = undefined
|
||||
this.options = candidate
|
||||
try {
|
||||
if (!this._disabled(candidate)) await this.init()
|
||||
} catch (error) {
|
||||
this.options = previousOptions
|
||||
throw error
|
||||
}
|
||||
commit()
|
||||
return
|
||||
}
|
||||
|
||||
// step 3: check if options are changed
|
||||
if (this.fiber?.uid) {
|
||||
const diff = Object
|
||||
.keys({ ...this.options, ...legacy })
|
||||
.filter(key => !deepEqual(this.options[key], legacy[key]))
|
||||
if (!diff.length && !force) return
|
||||
if (this._disabled(candidate)) {
|
||||
this.options = candidate
|
||||
try {
|
||||
await this._dispose(previous)
|
||||
} catch (error) {
|
||||
this.options = previousOptions
|
||||
throw updateError('dispose', candidate, error)
|
||||
}
|
||||
commit()
|
||||
this.context.emit('loader/partial-dispose', this, legacy, true)
|
||||
this._patchContext(diff)
|
||||
} else {
|
||||
await this.init()
|
||||
return
|
||||
}
|
||||
|
||||
const replace = diff.some(key => key === 'name' || key === 'inject' || key === 'group')
|
||||
if (!replace) {
|
||||
this.options = candidate
|
||||
try {
|
||||
await this._patchContext(diff)
|
||||
} catch (error) {
|
||||
this.options = previousOptions
|
||||
try {
|
||||
await this._patchContext(diff)
|
||||
} catch (rollbackError) {
|
||||
throw updateError('rollback', legacy, new AggregateError([error, rollbackError]))
|
||||
}
|
||||
this.context.emit('loader/partial-dispose', this, candidate, true)
|
||||
throw updateError('apply', candidate, error)
|
||||
}
|
||||
commit()
|
||||
this.context.emit('loader/partial-dispose', this, legacy, true)
|
||||
return
|
||||
}
|
||||
|
||||
let plugin: any
|
||||
try {
|
||||
plugin = diff.includes('name')
|
||||
? this.loader.unwrapExports(await this.parent.tree.import(candidate.name, this.getOuterStack))
|
||||
: previous.runtime!.callback
|
||||
} catch (error) {
|
||||
throw updateError('import', candidate, error)
|
||||
}
|
||||
|
||||
const previousPlugin = previous.runtime!.callback
|
||||
this.options = candidate
|
||||
try {
|
||||
await this._dispose(previous)
|
||||
} catch (error) {
|
||||
this.options = previousOptions
|
||||
throw updateError('dispose', candidate, error)
|
||||
}
|
||||
|
||||
try {
|
||||
await this._start(plugin)
|
||||
} catch (error) {
|
||||
this.options = previousOptions
|
||||
try {
|
||||
await this._start(previousPlugin)
|
||||
} catch (rollbackError) {
|
||||
throw updateError('rollback', legacy, new AggregateError([error, rollbackError]))
|
||||
}
|
||||
this.context.emit('loader/partial-dispose', this, candidate, true)
|
||||
throw updateError('apply', candidate, error)
|
||||
}
|
||||
commit()
|
||||
this.context.emit('loader/partial-dispose', this, legacy, true)
|
||||
}
|
||||
|
||||
getOuterStack = () => {
|
||||
@@ -159,26 +256,39 @@ export class Entry {
|
||||
await (this._initTask ??= this._init())
|
||||
} finally {
|
||||
this._initTask = undefined
|
||||
if (!this.loader.getTasks().length) this.ctx.reflect.notify(['loader'])
|
||||
}
|
||||
this.fiber?.await().finally(() => {
|
||||
if (this.loader.getTasks().length) return
|
||||
this.ctx.reflect.notify(['loader'])
|
||||
})
|
||||
await this.fiber?.await()
|
||||
}
|
||||
|
||||
private async _init() {
|
||||
let exports: any
|
||||
let plugin: any
|
||||
try {
|
||||
exports = await this.parent.tree.import(this.options.name, this.getOuterStack)
|
||||
plugin = this.loader.unwrapExports(await this.parent.tree.import(this.options.name, this.getOuterStack))
|
||||
} catch (error) {
|
||||
this.ctx.logger.error(error)
|
||||
return
|
||||
} finally {
|
||||
this._initTask = undefined
|
||||
throw updateError('import', this.options, error)
|
||||
}
|
||||
const plugin = this.loader.unwrapExports(exports)
|
||||
this._patchContext([])
|
||||
try {
|
||||
await this._start(plugin)
|
||||
} catch (error) {
|
||||
throw updateError('apply', this.options, error)
|
||||
}
|
||||
}
|
||||
|
||||
private async _start(plugin: any) {
|
||||
let fiber: Fiber | undefined
|
||||
try {
|
||||
fiber = await this._create(plugin)
|
||||
await fiber.await()
|
||||
} catch (error) {
|
||||
await this._dispose(fiber)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async _create(plugin: any): Promise<Fiber> {
|
||||
await this._patchContext([])
|
||||
this.loader.showLog(this, 'apply')
|
||||
this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
|
||||
return this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user