本系列的第8篇和第9篇主要介绍了Cordis服务的本质以及服务是如何利用ReflectService以反射的形式被注册的,接下来我们来介绍DeepSeek Harness作为基本部署单元和生命周期管理单元的插件、和作为插件调度和生命周期管理器的Fiber。
1. 在来认识一下插件在Cordis中的表现形式
我们在DeepSeek Harness插件内核-01:DeepSeek Harness三种插件形式介绍过插件的三种定义方式,现在来作一个简单的总结。定义插件的三种接口以及它们扩展的基础接口Base<T>都定义在命名空间Plugin中。Base<T>这个泛型接口的类型参数T表示为插件定义的配置类型,Base<T>的所有成员都是可以缺省的,包括表示名称的name、具有配置验证能力的config、表示注入服务的inject、提供服务的provide和针对服务配置消费声明的intercept。其中inject属性有表示服务名称列表和服务名称与配置的映射。
export namespace Plugin {
export interface Base<T = any> {
name?: string
Config?: StandardSchemaV1<any, T>
inject?: Inject
provide?: string | string[]
intercept?: Dict<boolean>
}
export interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any
}
export interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any
}
export interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any
}
}
export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
函数、类和对象形式的插件分别对应Function<T>、Constructor<T>和Object<T>三个接口,类型参数T为插件配置的类型。三种形式的插件定义中都有一个具有一致签名的函数/方法,t它们包含的两个参数context和config,前者表示当前插件被注册的Context,后者表示为插件提供的配置对象。
由于插件注册和执行被认为是对整个上下文体系具有副作用的操作,按照Cordis的涉及哲学:对这样的一组操作构建一个作为执行上下文的Conetxt,以此来控制这组操作的影响范围,对于操作对整个体系带来的副作用,我们可以定义对应了用来消除副作用的操作并保存在这个Context中,最终我们对通过的Context的终结来执行这组清理操作,来抹除留下的痕迹。Cordis用来干这种事情的就是Context的Fiber对象,虽然不能保证每个Context都有一个Fiber,但是由于Context的继承结构,每个Context总是会找到一个从事这方面工作的Fiber对象。具体来说Fiber提供了effect方法来执行提供的回调函数,并用来收集回调返回的清理函数,当Fiber的dispose方法执行的时候,收集的清理函数将会被执行。
对于插件来说,包括插件本身,有它创建的待释放资源(比如IO句柄)、注册的全局事件,针对它们的释放和接触都会以函数的形式保存在作为生命周期管理器的Fiber对象上,并随着Fiber的dispose而被调用,其主要的目的就是在卸载插件之后能够尽可能抹除它存续期间带来的影响。所以三种插件形式体现的核心函数具有一个any类型的返回值,我们一般提供一个在插件卸载时执行清理工作的函数。如果对象定义成实现Constructor<T>接口的类,除了需要定义服务标准的构造函数之外,还可以利用symbols.initHooks属性定义一个初始化过程中自动被调用的钩子回调函数,利用symbols.init返回上述额清理函数。
虽然形式各异,但万法归宗,三种形式的插件最终都被如下这个resolve函数解析为一个函数。对于函数形式的插件,其提供的函数就是最终形式;对于类,则是构造函数原型的apply方法;至于对象形式,那就是apply方法。
resolve(plugin: Plugin): Function | undefined {
try {
if (typeof plugin === 'function') return plugin
if (isApplicable(plugin)) return plugin.apply
} catch {}
}
function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
}
2. 几个辅助类型和接口
站在Cordis的角度来说,插件可视为一个以Context和插件配置作为输入,返回的清理函数作为输出的函数。当然,你也可以额外给插件指定一些元数据,比如名称,依赖和提供的服务。至于函数如何被加载、调度执行、卸载甚至热重载,则是Fiber需要解决的问题。不过在正式介绍Fiber之前,我们先来介绍一些辅助的类型和接口。
2.1 DisposableList<T>
这个 DisposableList<T> 是一个非常有设计感的、具备双向映射能力且防止内存泄漏的可销毁列表(容器)。
export class DisposableList<T extends WeakKey> {
private sn = 0
private map = new Map<number, T>()
private weak = new WeakMap<T, number>()
get length() {
return this.map.size
}
push(value: T) {
const sn = ++this.sn
this.map.set(sn, value)
this.weak.set(value, sn)
return () => this.map.delete(sn)
}
delete(value: T) {
const sn = this.weak.get(value)
if (!sn) return false
return this.map.delete(sn)
}
clear() {
const values = […this.map.values()]
this.map.clear()
return values.reverse()
}
[Symbol.iterator]() {
return this.map.values()
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return […this]
}
}
interface WeakKeyTypes {
object: object;
}
type WeakKey = WeakKeyTypes[keyof WeakKeyTypes];
DisposableList<T>的泛型参数T是对WeakKey的扩展,从定义可以看出WeakKey的类型其实就是object(本质上就是WeakKeyTypes["object"]),因此这个容器只用来存放对象类型的数据。类内部定义了三个私有属性,借助于这三个数据成员,DisposableList<T>定义了成员添加、删除和迭代,以及清除列表的方法。值得一提的是它的push方法,它返回一个函数用来将添加的对象从列表中移除。
- sn:一个自增的序列号,作为容器内每个对象的唯一标识;
- map:正向映射(序列号 -> 对象)。用来保证对象的顺序、计算数量以及支持迭代遍历;
- weak:反向映射(对象 -> 序列号)。用来实现 O(1) 复杂度的快速查找与删除,同时避免内存泄漏。如果外部没有引用这个对象,WeakMap 允许该对象被垃圾回收(GC)。
2.2 EffectRunner<T>
顾名思义,EffectRunner<T>是执行插件函数的Runner,由于插件的执行会为整个上下文体系带来副作用(effect),所以需要收集相应的清理函数来恢复带来的副作用。这个一个泛型接口,类型参数T会作为epoch属性的类型,所谓的epoch不尽快可以确定当前Runner的状态,还可以确定插件依赖服务的组合版本。
interface EffectRunner<T> {
epoch: T
execute: () => any
collect: (dispose: Disposable) => void
getOuterStack: () => string[]
}
export type Disposable<T = any> = () => T
其它三个成员说明如下:
- execute:用来执行插件;
- collect:用来收集恢复插件副作用的函数,函数类型被定义成Disposable<T>类型;
- getOuterStack: 用来截取原始的代码堆栈。
Fiber内部使用的Runner是一个EffectRunner<string|false>对象,其epoch属性会有三种表现形式:
- __INACTIVE__:表示插件因必要的依赖服务尚未就绪而处于非激活状态;
- false: 表示插件正在被强制驱逐或销毁,将不再参与任何后续的依赖刷新调度;
- 就绪依赖服务指纹:表示所有依赖服务均就绪,满足插件的所有可执行条件。由于具体的属性值是拼接所有依赖服务所在Fiber的uid而成,所以我们将它称为就绪依赖服务指纹。如果没有注入依赖服务,该属性将会是一个空字符串。
在Fiber的存续期间,Runner的epoch可能频繁地在__INACTIVE__和就绪依赖服务指纹之间切换:如果插件注册的时候没有注入依赖服务或者依赖服务全部就绪,epoch将是就绪依赖服务指纹形式。提供依赖服务的任何一个Fiber加载、卸载和承载,当前Fiber都会接收到通知,并会重写确定所有依赖服务是否处于就绪状态,如果是就为epoch生成就绪依赖服务指纹,否则就将其设置为__INACTIVE__。
2.3 Runtime
在DeepSeek Harness插件内核-08:利用RefectService以反射方式扩展Context中介绍在服务状态发生改变对外发送通知的时候,我们已经介绍过如下这个Runtime接口。
export interface Runtime {
name?: string
fibers: DisposableList<Fiber>
callback: globalThis.Function
Config?: StandardSchemaV1
}
当我们调用RegistryService第一次以插件形式对该函数进行注册时,它会创建一个Runtime对象,其name属性就是指定的插件名称,与插件绑定的Fiber会添加到filbers列表中,callback属性返回的就是这个插件函数,而可缺省的Config属性(这里首字母应该小写)为用来确定插件配置是否合法的验证器。如果将同一个函数作为插件多次注册,将会沿用当前这个Runtime,并为新注册的插件创建用来管理生命周期的Fiber对象,该对象最终被添加到对应Runtime的fibers列表中。换句话说,Runtime是与插件定义绑定的,不是与插件实例绑定,fibers列表中的每个Fiber对象才与每个插件实例绑定。值得一提的是,fibers列表的类型正式上面介绍的DisposableList<Fiber>。
3. Fiber的字段成员
Fiber只会在如下两种场景下被创建:
- 创建根Context****: 这里仅仅创建一个Fiber对象与根Context绑定在一起,因为很多地方需要调用Fiber的effect方法。此时由于尚无插件注册,所以它的Runtime为undefined;
- 注册插件:,为注册的插件实例(同一个插件函数可以多次注册为不同的插件实例)创建对应的Fiber对象作为调度和生命周期管理器,此时会提供插件函数对应的Runtime,当前创建的Fiber将会作为其fibers列表中的一员。
按照Context的继承规则,针对某个属性的访问会从当前Context逐级进行追溯,直到找到为止。所以当我们调用某个Context的isolate方法创建一个子Context时,虽然它具有了独立的symbols.isolate字典,但是它的fiber依然指向继承自父Context的那个。这将导致一个非常严重的问题,直接破坏isolate方法试图构建的隔离栅栏,在我的文章[DeepSeek Harness插件内核-11:这算Cordis一个大BUG吗?对这个问题进行单独介绍。
Fiber复杂管理插件的生命周期,其自身也包含了相对复杂的状态流转规则,简单起见,我们先来了解它定义的一些基本字段成员。为了方便后续的叙述,我们将前者成为根Fiber,后者成为非根Fiber,或者基于插件的Fiber。
export class Fiber {
public uid: number | null
public readonly ctx: Context
public config: any
public _config: any
public state = FiberState.PENDING
public readonly dispose: () => Promise<void>
public inertia: Promise<void> | undefined
public readonly _hooks: Dict<DisposableList<Function>> = Object.create(null)
public readonly _disposables = new DisposableList<Disposable>()
protected context: Context
private _error: any
private _runner: EffectRunner<string>
private _store: Dict<Impl> = Object.create(null)
}
PENDINGexport const enum FiberState {
PENDING,
LOADING,
ACTIVE,
FAILED,
DISPOSED,
UNLOADING,
}
各成员说明如下:
- uid:Fiber的唯一标识。根Fiber的 uid 始终为 0,非根Fiber利用RegistryService对象上维护的插件计数器作为uid。当该插件被销毁卸载时,它会被强制设为null;
- ctx和context:根Fiber的ctx属性引用的自然就是根Context。当我们将某个插件注册到某个Context上时,会创建一个子Context,并与插件的Fiber的ctx属性绑定。context可以视为ctx的别名,其底层指向与 ctx 完全相同,但为 Fiber 的子类或派生类内部提供了更具体、更强类型的 Context 类型推导支持;
- _config 和 config:前者表示注册插件时提供的原始配置,后者表示原始的插件配置在经过检验、修剪和注入默认值后的最终生效的配置,它们对于根Fiber来说始终时undefined;
- _disposables:用来收集Fiber存续期间收集的Disposable对象;
- dispose:根Fiber用此方法重启整个应用,非根Fiber则用它来销毁插件,_disposables集合中收集的Disposable对象将在此方法中执行;
- _store和store:前者用来存储一组Impl对象,表示注入到插件中的就绪依赖服务,后者仅仅前者的一个快照;
- inertia:以安全的方式执行插件的重载和卸载,在底层起到防抖防重入的作用,确保状态切换请求能安全排队执行;
- _hooks:内部私有的钩子事件注册表。每一个键代表一个生命周期事件,其值利用 DisposableList列表存放着一堆事件回调函数;
- _error:内部私有的错误暂存器;
- _runner:一个EffectRunner<string>对象,非根Fiber利用它来调度执行插件,该属性对于根Fiber来没有什么实质性作用。
- state:当前Fiber所处的生命周期状态,枚举FiberState定义了所有的状态,这个状态一般会综合uid、_error和_runner综合计算出来;
4. Fiber的创建
鉴于Fiber上述的两种创建场景,我们接下来分别介绍根Fiber和非根Fiber的创建流程。
4.1 随着根Context的实例化被创建
Fiber的构造函数定义如下,当构造函数开始执行的时候,它会将提供的config参数作为原始配置赋值给_config属性,并且定义collect函数来收集最终释放是需要指定的Disposable对象(将该对象推入_disposables属性表示的DisposableList列表)。
export class Fiber {
constructor(
public parent: Context,
config: any,
public inject: Dict<any>,
public runtime: Plugin.Runtime | null,
getOuterStack: () => string[],
) {
this._config = config
const collect = (dispose: Disposable) => {
this._disposables.push(dispose)
}
if (runtime) {
…
} else {
this.uid = 0
this.ctx = this.context = parent
this.state = FiberState.ACTIVE
this.store = Object.create(null)
this._runner = {
epoch: '',
getOuterStack,
execute: () => {},
collect,
}
this.dispose = () => this.restart()
}
}
}
鉴于上述的两种构建方式,如果Fiber是随着根Context的实例化被创建的化,传入的参数列表中与插件相关的参数都是undefined,包括:
- config:插件的配置;
- inject:注入服务和对应配置的映射表;
- runtime:插件函数绑定的Runtime对象。
此时只有作为根Context的parent参数,并且会进入else分支:
- 将uid设置为0,将作为parent传入的Context也作为自己的Context(绑定到ctx属性上),三位一体,更符合其创世Fiber的超然地位;
- 由于是与根Context绑定,所以它一出世就是激活状态;
- 在作为依赖服务实例快照的store属性设置为一个以null为原型的空对象;
- 初始化_runner,其execute方法什么都没有做,上面定义的collect方法被作为它的同名方法,所以清理函数可以正常收集;
- 初始化dispose方法,它将调用如下的restart方法进行重启。
export class Fiber {
async restart() {
this.assertActive()
this._setEpoch(INACTIVE)
this._refresh()
await this.await()
}
async await() {
while (this.inertia) {
await this.inertia
}
if (this._error) throw this._error
return this
}
}
由于restart方法调用了_setEpoch方法(这是驱动Fiber状态其流转的核心方法,我们将在后续予以介绍)并将epoch设置为__INACTIVE__,对于根Fiber对象只能将应用卸载,而不能真正实现重启。但是对于根Fiber来说,由于后续执行的_refresh方法进行状态刷新,所在卸载后如果所有依赖服务全部就绪,就会真正重启插件。
4.2 以插件注册的形式被创建
如果Fiber是因注册插件被创建,执行逻辑就要复杂多了,具体走的就是runtime存在的那个分支。它首先从RegistryService对象获取插件注册计数器作为自己标识的uid,然后扩展当前Context创建一个子Context,并将它与自己的ctx属性绑定在一起,同时子Context的fiber属性将成为针对自己的引用。由于这个子Context是当前Fiber绑定的上下文,下面所说的当前Context指的就是它。如果利用inject提供的注入服务的配置,将会以父Context的Context.intercept字典作为原型创建一个新的字典,并作为当前Context的Context.intercept属性,并将注入的服务配置添加到这个字典中,此后就可以从当前Context中得到依赖服务对应的配置了。
export class Fiber {
constructor(
public parent: Context,
config: any,
public inject: Dict<any>,
public runtime: Plugin.Runtime | null,
getOuterStack: () => string[],
) {
this._config = config
const collect = (dispose: Disposable) => {
this._disposables.push(dispose)
}
if (runtime) {
this.uid = parent.registry.counter
this.ctx = this.context = parent.extend({ fiber: this })
const injectEntries = Object.entries(this.inject)
if (injectEntries.length) {
this.ctx[Context.intercept] = Object.create(parent[Context.intercept])
for (const [name, config] of injectEntries) {
if (isNullable(config)) continue
this.ctx[Context.intercept][name] = config
}
}
this._runner = {
epoch: INACTIVE,
getOuterStack,
execute: function () {
if (isConstructor(runtime.callback)) {
const instance = new runtime.callback(this.ctx, this.config)
for (const hook of instance?.[symbols.initHooks] ?? []) {
hook()
}
return instance?.[symbols.init]?.()
} else {
return runtime.callback(this.ctx, this.config)
}
},
collect,
}
this.dispose = parent.fiber.effect(() => {
const remove = runtime.fibers.push(this)
return async () => {
this.uid = null
emitPluginDisposed(this.context, this)
if (this.ctx.registry.has(runtime.callback)) {
remove()
if (!runtime.fibers.length) {
this.ctx.registry.delete(runtime.callback)
}
}
this._setEpoch(INACTIVE)
if (!this.inertia) {
this._updateState(() => {
this.inertia = this._unload()
return FiberState.UNLOADING
})
}
while (this.inertia) {
await this.inertia
}
}
}, 'ctx.plugin()')
try {
this.context.emit('internal/plugin', this)
} catch (error) {
void Promise.resolve(this.dispose()).catch(reason => this.ctx.logger.error(reason))
throw error
}
if (this.uid !== null && parent.fiber.state !== FiberState.UNLOADING) {
for (const name of Object.keys(this.inject)) {
this._checkImpl(name)
}
this._refresh()
}
} else {
…
}
}
}
4.2.1 EffectRunner的初始化
在完成了服务注册的设置后,构造函数需要创建一个EffectRunner<string>对象作为运行插件的_runner属性,具体会对四个属性进行如下设置:
- epoch: 设置成__INACTIVE__;
- getOuterStack:构造函数的同名参数;
- collect:之前创建的用来向_disposables列表中添加Disposable对象的函数;
- execute: 作为执行插件的方法,具体执行逻辑取决于注册的插件定义形式:
- 如果类型为Plugin.Constructor<T>,会以当前Context和通过config属性表示的配置作为参数调用构成函数,然后从生成对象的symbols.initHooks属性中提取设置的初始化钩子函数予以执行,如果对象的symbols.init属性被设置了一个初始化函数,将返回此函数执行的结果,否则返回undefined;
- 对于其它插件形式,将当前Context和通过config属性表示的配置作为参数调用对应的插件函数就可以了。
4.2.1 dispose方法的初始化
完成了针对_runner属性的初始化之后,当前Fiber对象会被添加到runtime的fibers列表中,然后初始化dispose方法。dispose方法本身是父Fiber的effect方法的返回结果,意味着即使当前Fiber对象的dispose方法没有被调用,当父Fiber的dispose方法调用的时候,清理工作也会执行。我们看看dipose方法都做了些什么:
- 将作为标识的uid设置为null,这是一个重要的表示Fiber被销毁的标识;
- 调用emitPluginDisposed函数利用事件总线对外发送插件卸载的通知;
- 将当前Fiber从Runtime的fibers列表中移除(执行的是调用push方法返回的函数),如果fibers列表为空,意味着Runtime绑定的插件函数再无对应的注册实例,此时调用RegistryService的delete方法完全接解除对应插件的注册;
- 将epoch设置为__INACTIVE__调用调用_setEpoch方法,驱动Fiber状态机进行状态流转;
- 如果inertia未作设置,通过调用_updateState方法指定的返回将当前状态设置为UNLOADING,并将_unload方法返回的用来卸载插件的Promise赋值给inertia,并在一个循环中调用inertia完成插件的卸载工作。
4.2.3 依赖服务可用性检验
在完成了针对_runner和dispose两个成员的初始化后,如果当前uid不会null(确保Fiber没有因disose方法的调用被销毁)并且父Fiber的状态不是UNLOADING(并未处于卸载过程中),构造函数还会调用_checkImpl方法检验注入到当前插件的每个服务的状态。如下面的代码所示,只要注入的服务不存在或者利用提供的check方法确定此时不处于激活状态,描述依赖服务的Impl对象将会立即从_store字典中移除。也就是说,一旦针对所有注入的依赖服务执行了一轮_checkImpl方法,_store字典中存储的是处于可用状态的依赖服务。
export class Fiber {
_checkImpl(name: string) {
const impl = this.ctx.reflect._getImpl(name, true)
if (!impl) return delete this._store[name]
try {
if (impl.check && !impl.check.call(getTraceable(this.ctx, impl.value))) {
return delete this._store[name]
}
} catch (error) {
impl.fiber.ctx.logger.error(error)
return delete this._store[name]
}
this._store[name] = impl
}
}
构造函数的最后一个步骤就是调用如下整个_refresh方法通过判断依赖服务的状态来刷新自己的状态。具体做法很简单:从inject属性中提取所有依赖服务的名称,只要有一个不存在于_store字典中(意味着该服务尚未就绪),就意味着依赖服务缺失,当前插件不可用,此时就会调用状态机流转方法_setEpoch将epoch设置为__INACTIVE__。反之生成依赖服务Fiber指纹作为epoch,并作为参数调用_setEpoch方法,此时插件将以可用状态流转下去,随后插件将会借助于_runner的execute方法予以执行。
export class Fiber {
_refresh() {
let epoch: string | boolean = false
epoch = ''
for (const name of Object.keys(this.inject)) {
const impl = this._store[name]
if (!impl) {
epoch = INACTIVE
break
}
epoch += ':' + impl.fiber.uid
}
this._setEpoch(epoch)
}
}
5. Fiber状态机
Fiber在Cordis被设计成一个状态机,并且是一个具备依赖感知并且带有异步并发锁的复杂状态机,它是整个生命周期管理和热重载的核心底座。通过上面的介绍我们知道一个Fiber对象具有两个于状态相关的属性成员,一个自然是通过state表示的状态,具体有通过FiberState枚举表示的6种状态。另一个则是_runner的epoch属性,它本质也是对插件状态的一种表征,它只有三种形式。
5.1 状态流转在_setEpoch方法中的实现
实际上驱动Fiber状态流转利用的是后者,具体来说是如下这个_setEpoch方法。
export class Fiber {
private _setEpoch(epoch: string) {
const oldEpoch = this._runner.epoch
if (epoch === oldEpoch) return
this._runner.epoch = epoch
if (this.inertia) return
this._updateState(() => {
if (epoch !== INACTIVE && oldEpoch === INACTIVE) {
this.inertia = this._reload()
return FiberState.LOADING
} else {
this.inertia = this._unload()
return FiberState.UNLOADING
}
})
}
}
如上面的代码片段所示,如果指定的epoch于_runner当前的状态不一致,则会使用_runner的epoch属性进行设置。在通过inertia确定当前并没有进行重载和卸载的情况下,它会调用如下所示的_updateState方法对state属性表示的状态进行更新。
export class Fiber {
private _updateState(callback: () => void | FiberState) {
const oldState = this.state
this.state = callback() ?? this._getState()
if (oldState === this.state) return
this.context.emit('internal/status', this, oldState)
if (oldState !== FiberState.ACTIVE && this.state !== FiberState.ACTIVE) return
for (const key of Reflect.ownKeys(this.ctx.reflect.store)) {
const impl = this.ctx.reflect.store[key as symbol]
if (impl.fiber !== this) continue
this.ctx.reflect.notify([impl.name])
}
}
}
_updateState方法会执行指定的回调函数,并将返回值作为当前计算出来的状态。如果回调函数没有返回具体的状态,则调用如下这个_getState来确定当前应有的状态。在完成新状态更新并确定状态真正发生改变之后,_updateState会调用当前Context的emit方法对外发送一个名为internal/status的通知,通知的参数携带了当前Fiber对象和之前的状态。如果服务注册到Fiber所在的Context上,在从Active转向其它状态,或者从其它状态转向Active的情况下,需要针对每个服务调用ReflectService的notifiy方法通知依赖此服务的插件。我们在DeepSeek Harness插件内核-08:利用RefectService以反射方式扩展Context介绍过,依赖这些服务的Fiber会调用上述的_checkImpl方法再次检验服务的状态,并在确认依赖服务的状态确实发生变化情况下调用上述的_refresh进行刷新,最终将决定目标插件应该上线还是被卸载。
如下这个_getState方法体现了Fiber提供的默认状态解析规则。由于Fiber的dispose方法总是会将uid率先设置为null,所以该属性是否为null就是Fiber的状态是否为DISPOSED的标志。由于只有出错的情况下才会设置_error,所以该属性就是是否是FAILED状态的标志。由于_runner的epoch属性只有三种形式,前面已经排除了DISPOSED状态的可能,所以该属性的值不可是false,所以只要它的值不是__INACTIVE__,当前的状态就应该是ACTIVE。如果不满足这三个条件,就属于初始状态PENDING。
export class Fiber {
private _getState() {
if (this.uid === null) return FiberState.DISPOSED
if (this._error) return FiberState.FAILED
if (this._runner.epoch !== INACTIVE) return FiberState.ACTIVE
return FiberState.PENDING
}
}
在回过来看看_setEpoch在调用_updateState方法时,指定的回调函数都做了些什么:
- 如果_runner的oldEpoch从__INACTIVE__转换成依赖服务Fiber指纹,意味着依赖服务全部就绪,此时会调用_reload方法实施重载,并将返回的Promise赋值给inertia,其它并发操作将会就此选择取消或者等待,以避免并发操作导致的一致性问题。回调函数最终返回LOADING状态表明正在实施重载工作;
- 否则存在如两种情况,这两个情况下都应该采用上面类似的方式调用_reload方法将卸载当前插件,并返回UNLOADING状态表明正在实施卸载工作:
- 原来的值为依赖服务Fiber指纹(oldEpoch !== INACTIVE), 新的值可能是__INACTIVE__,意图明确,应该卸载;也可能是与之前不一样的依赖服务Fiber指纹,符合热启动的场景,也需要先卸载在刷新;
- 当前的值为__INACTIVE__,不论原来的值什么,意图已经很明确了,应该卸载。
export class Fiber {
private _setEpoch(epoch: string) {
…
this._updateState(() => {
if (epoch !== INACTIVE && oldEpoch === INACTIVE) {
this.inertia = this._reload()
return FiberState.LOADING
} else {
this.inertia = this._unload()
return FiberState.UNLOADING
}
})
}
}
从如下所示的_reload方法的定义可看出, Fiber在重载的时候不仅会重新生成就绪依赖服务快照的store属性,还会调用_resolveConfig方法重新加工处理原始的配置,并通过调用_execute方法执行_runner将执行插件。如果执行_runner发生异常,对_error赋值(它将影响_getState方法返回的状态),并将_runner的epoch设置为__INACTIVE__。在这之后根据当前状态继续调用_updateState方法让状态机继续流转下去,指定的回调函数会执行如下操作:
- 如果_runner的状态依旧未曾改变,意味着整个重载是成功的,此时只需要利用_getState方法确定最新状态即可;
- 否则意味着重载失败,直接调用_unload方法卸载插件,并返回UNLOADING表明正在进行卸载工作。
export class Fiber {
private async _reload() {
this.store = { …this._store }
const oldEpoch = this._runner.epoch
try {
await Promise.resolve()
if (this._runner.epoch === oldEpoch) {
this.config = this._resolveConfig(this._config)
await this._execute(this._runner)
this._error = undefined
}
} catch (reason) {
this.ctx.logger.error(reason)
this._error = reason
this._runner.epoch = INACTIVE
}
this._updateState(() => {
if (this._runner.epoch === oldEpoch) {
this.inertia = undefined
} else {
this.inertia = this._unload()
return FiberState.UNLOADING
}
})
}
}
如下所示的_unload方法则会执行_disposables中的所有Disposable对象来完成所有的清理工作,最终将_disposables列表清空,并将store设置为undefined。在这之后根据当前状态继续调用_updateState方法让状态机继续流转下去,指定的回调函数会执行如下操作:
- 如果_runner的epoch是__INACTIVE__,意味着卸载是成功的,此时只需要利用_getState方法确定最新状态即可;
- 否则意味着依赖服务发生了改变,但依然是就绪状态,很明显这属于热重载场景,所以需要调用_reload方法实施重载,并返回LOADING状态表明重载工作正在进行。
export class Fiber {
private async _unload() {
await Promise.all(this._disposables.clear().map(async (dispose) => {
try {
await composeError(async (info) => {
await Promise.resolve()
info.error = new Error()
await runDisposable(dispose)
}, this._runner.getOuterStack)
} catch (reason) {
this.ctx.logger.error(reason)
}
}))
this.store = undefined
this._updateState(() => {
if (this._runner.epoch === INACTIVE) {
this.inertia = undefined
} else {
this.inertia = this._reload()
return FiberState.LOADING
}
})
}
}
5.2 状态流转总结
当目前我们已经上已经知道了Fiber状态机总体上是如何流转下去的,具体的状态流转规则如下所示。
总结一下具体的流转逻辑:
-
初始化 → PENDING
- Fiber 创建时,先挂载到父 Context。
- 如果依赖未满足,保持在 PENDING。
-
依赖满足 → LOADING
- _refresh() 检查依赖实现;
- _setEpoch() 将 epoch 从 INACTIVE 切换到有效值时,触发 _reload();
- 状态更新为 LOADING。
-
加载成功 → ACTIVE
- _reload() 执行插件回调,收集 effect;
- 若无错误,进入 ACTIVE;
- 此时 Fiber 提供服务,ctx.reflect.notify() 会广播实现可用。
-
加载失败 → FAILED
- 如果 _reload() 抛出异常,记录 _error;
- 状态进入 FAILED;
- Fiber 保持挂载,但不可用。
-
卸载 → UNLOADING
- _setEpoch(INACTIVE) 或父 Context 卸载时触发;
- _unload() 清理所有 disposables;
- 状态进入 UNLOADING。
-
卸载完成 → PENDING 或 DISPOSED
- 如果 Fiber 仍可重启(uid 未清空),则回到 PENDING;
- 如果 dispose() 被调用并彻底清理,uid 设为 null,进入 DISPOSED。
在此基础上加上 ReflectService 在进行服务注册和更新服务配置会调用 notify 方法通知注入此方法的插件,对应的Fiber会从在调用_checkImpl重新检查依赖服务的状态并更新_store字典,然后调用_refresh方法进行刷新,该方法会根据当前的状态生成对应的epoch,并作为参数调用_setEpoch针对最新的状态流转下去。
6. 公共方法/属性
上面介绍Fiber状态机流转涉及的方法都是一些私有方法,在我们来来看看Fiber定义了哪些供我们调用的公共方法。其中最常用的公共方法莫过于随处可见的effect方法。在 Cordis 的语境下,一个 Effect 可以是一个插件的执行、一个事件监听的注册、或者一个服务的绑定等。这个方法的唯一核心目标是确保所有异步操作产生的副作用,在 Fiber 发生重启、卸载或报错时,能够以严格的顺序、无泄漏地被清理干净。
export class Fiber {
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
get name() ;
assertActive() ;
getEffects() ;
async await();
async restart();
update(config: any, noSave = false);
}
export interface EffectMeta {
label: string
children: EffectMeta[]
}
其它的公共方法还包括如下这些:
- name:如果runtime的name存在,则将它作为返回值,否则会沿着Context这棵树向上追溯,最终以root兜底;
- assertActive:通过判单uid是否为null确定当前Fiber是否处于活动状态,如果为null直接抛出异常;
- getEffects:以EffectMeta对象的形式返回_disposables集合中的Effect标签;
- await:在一个循环中执行inertia属性设置的Promise,等待重载或者卸载操作的正常结束;
- restart:强制让当前 Fiber 先破后立进行异步重启。它先通过 _setEpoch(INACTIVE) 实施卸载,随后调用 _refresh() 刷新状态并根据当前状态决定后续的动作;
- update:动态更新当前插件的配置并触发热重载。它会将新配置注入上下文中,并智能对比配置是否发生改变,若发生改变则在底层自动触发 restart() 流程,从而实现不重启进程的插件配置热插拔。




