refactor(loader): resolve config after injected services

This commit is contained in:
Turtle
2026-08-07 17:27:38 +08:00
parent b692f38506
commit 7e3a82eacc
38 changed files with 404 additions and 306 deletions

View File

@@ -3,7 +3,13 @@ import { deepEqual, isNullable } from '@deepseek-ai/cosmokit'
import { Loader } from '../index.ts'
import { EntryGroup } from './group.ts'
import { EntryTree } from './tree.ts'
import { evaluate, interpolate } from './utils.ts'
import { evaluate } from './utils.ts'
/** Static plugin hook for resolving a container config while preserving nested entry configs. */
export const EntryConfigResolver = Symbol.for('cordis.loader.entry-config-resolver')
/** Resolver installed at {@link EntryConfigResolver}. */
export type EntryConfigResolver = (ctx: Context, config: any) => any
/** Serialized plugin entry options stored in loader config files. */
export interface EntryOptions {
@@ -101,17 +107,12 @@ export class Entry {
return evaluate(this.ctx, expr)
}
_resolveConfig(plugin: any): [any, any?] {
if (plugin[EntryGroup.key]) return this.options.config
return interpolate(this.ctx, this.options.config)
}
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)) {
await this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
await this.fiber.update(this.options.config, true)
}
})
}
@@ -258,7 +259,15 @@ export class Entry {
this._initTask = undefined
if (!this.loader.getTasks().length) this.ctx.reflect.notify(['loader'])
}
await this.fiber?.await()
await this._await()
}
async _await() {
try {
await this.fiber?.await()
} catch (error) {
throw updateError('apply', this.options, error)
}
}
private async _init() {
@@ -278,17 +287,13 @@ export class Entry {
private async _start(plugin: any) {
let fiber: Fiber | undefined
try {
fiber = await this._create(plugin)
await this._patchContext([])
this.loader.showLog(this, 'apply')
fiber = this.fiber = this.ctx.registry.plugin(plugin, this.options.config, this.getOuterStack)
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')
return this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
}
}

View File

@@ -69,6 +69,10 @@ export class EntryGroup {
try {
const outcomes = await Promise.allSettled(config.map(options => this.create(options)))
// Disposal owns termination: sibling starts can still be settling after
// the containing tree has gone away, but their failures no longer
// describe a live update to roll back.
if (this.ctx.fiber.uid === null) return
const failures = outcomes
.filter((outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected')
.map(outcome => outcome.reason)

View File

@@ -51,7 +51,7 @@ export abstract class EntryTree {
continue
}
const outcomes = await Promise.allSettled(
[...this.entries()].map(entry => entry.fiber?.await()),
[...this.entries()].map(entry => entry._await()),
)
const failures = outcomes
.filter((outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected')

View File

@@ -1,9 +1,16 @@
import { Context, Inject, Service } from '@deepseek-ai/cordis'
import { Context, FiberState, Inject, Service, type Fiber } from '@deepseek-ai/cordis'
import { defineProperty, isNullable, type Dict } from '@deepseek-ai/cosmokit'
import { ModuleLoader } from './internal.ts'
import { Entry, type EntryOptions } from './config/entry.ts'
import {
Entry,
EntryConfigResolver,
type EntryConfigResolver as ConfigResolver,
type EntryOptions,
} from './config/entry.ts'
import { EntryGroup } from './config/group.ts'
import isolate from './config/isolate.ts'
import { EntryTree } from './config/tree.ts'
import { interpolate } from './config/utils.ts'
/** Re-export entry node APIs. */
export * from './config/entry.ts'
@@ -87,6 +94,15 @@ export class Loader extends EntryTree {
ctx.reflect.provide('loader', this, this[Service.check])
ctx.on('internal/config', function (this: Fiber, _config, next) {
const config = next()
if (!this.entry || this.parent.fiber?.entry === this.entry) return config
const plugin = this.runtime?.callback as Record<PropertyKey, unknown> | undefined
if (plugin?.[EntryGroup.key]) return config
const resolve = plugin?.[EntryConfigResolver] as ConfigResolver | undefined
return resolve ? resolve(this.ctx, config) : interpolate(this.ctx, config)
}, { global: true })
ctx.on('internal/update', async function (config, noSave, next) {
if (!this.entry || noSave || this.parent.fiber?.entry === this.entry) return next()
await next()
@@ -127,7 +143,8 @@ export class Loader extends EntryTree {
if (!ctx.registry.has(fiber.runtime!.callback)) return
// case 5: the entry's tree is being disposed
if (!fiber.entry.parent.tree.ctx.fiber.uid) return
const treeOwner = fiber.entry.parent.tree.ctx.fiber
if (!treeOwner.uid || treeOwner.state === FiberState.UNLOADING) return
// case 6: Loader is replacing or removing this exact fiber
if (fiber.entry._disposing) return