虽然Context是Cordis架构体系最核心的类型,但是通过 Context全面解析 (上篇 和 下篇)的介绍,我们知道Context仅仅定义了三个方法而已,它的很多方法都是RefectService以反射的方式赋予它的。ReflectService 是 Cordis 框架的核心依赖注入与生命周期管理引擎。它基于 ES6 Proxy 与 Reflect 反射机制,围绕轻量级线程(Fiber)建立服务体系。其核心能力包括 动态注册与解绑。跨异步上下文追踪、高阶代理扩展等。
1. 扩展的属性保存在哪里
ReflectService将扩展ReflectService,并以反射形式添加的属性定义在它的props属性中,这是一个值类型为Property的字典,其Key自然代表属性的名称。从Property类型的定义看出,这里的属性分为service和accessor两种,前者代表常规的服务实例对象或者函数,后者表示为ReflectService提供的计算属性。至于具体的服务服务实例,则存储在store属性对应的字典中,为了解决命名冲突确保唯一性,这个字典以Symbol作为Key,服务连同必要的元数据封装在Impl对象。
export class ReflectService {
public props: Dict<Property> = Object.create(null)
public store: Dict<Impl, symbol> = Object.create(null)
}
作为ReflectService属性的Property类型由Property.Service和Property.Accessor两者组成的可区分联合类型(Discriminated Unions),两者利用type属性来区分类型。由于服务被封装成Impl对象保存在store属性中,所以对应的Service类型只有唯一的type属性,而Accessor类型则利用get和set定义计算属性的读写逻辑,其中set是可以缺省的,返回的布尔值表示是否成功完成属性赋值。
export type Property = Property.Service | Property.Accessor
export namespace Property {
export interface Service {
type: 'service'
}
export interface Accessor {
type: 'accessor'
get: (this: Context, receiver: any, error: Error) => any
set?: (this: Context, value: any, receiver: any, error: Error) => boolean
}
}
用来封装服务实例的Impl对象除了表示服务实例的value属性,还包括表示服务注册名称的name、注册时所在ReflectService的Fiber。check方法用来确认服务是否处于激活状态。
export interface Impl {
name: string
fiber: Fiber
value?: any
check?: () => boolean
}
2. 服务的注册
我们通过调用ReflectService的rovide方法完成服务的注册,该方法除了提供注册服务对象和注册名称为,还可以提供一个可缺省的check参数来为上述的Impl对象提供用来验证服务激活状态的函数。服务的注册本质就是创建Property.Service和Impl对象,并将它们添加到props和store属性中的过程,不过涉及的细节远不止这些。
export class ReflectService {
provide(name: string, value?: any, check?: () => boolean) {
return this.ctx.fiber.effect(() => {
if (!this.props[name]) {
this.props[name] ??= { type: 'service' }
} else if (this.props[name].type !== 'service') {
throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
}
this.props[name] = { type: 'service' }
this.ctx.root[symbols.isolate][name] ??= Symbol(name)
const key = this.ctx[symbols.isolate][name]
const impl: Impl = { name, value, fiber: this.ctx.fiber, check }
if (this.store[key]) {
throw new Error(
`service "${name}" has been registered at <${this.store[key].fiber.name}>`)
}
this.store[key] = impl
this.ctx.fiber.store![name] = impl
if (this.ctx.fiber.state === FiberState.ACTIVE) {
this.notify([name])
}
return async () => {
delete this.store[key]
const fibers = this.notify([name])
await Promise.allSettled(fibers.map(fiber => fiber.await()))
// ensure self access before dependencies cleanup
delete this.ctx.fiber.store![name]
}
}, `ctx.provide(${JSON.stringify(name)})`)
}
}
由于整个服务注册是通过调用当前ReflectService的Fiber对象的effect方法完成的,这意味着整个服务注册的工作是可以通过指定参数返回的函数进行撤销的。换句话说,如果这个Fiber与某个插件关联,意味着服务注册是这个插件为系统带来的副作用,当这个插件不再需要而被卸载时,它为系统带来的副作用应该抹除,也就是需要解除服务注册。所以你会看到作为参数的函数最终返回另一个函数,后者再做焚尸灭迹的工作。
我们根据上述的代码总结一下整个服务注册的流程:
- 确认
props字典中是否存在一个同名的类型为service的属性,如果有则直接抛出异常拒绝重复注册; - 在
props中以指定的名称注册一个Property.Service对象; - 提取根
ReflectService的symbols.isolate字典,确保指定的名字在其中拥有一个映射的Symbol(如果没有,就根据指定的名称创建一个Symbol); - 然后从当前
ReflectService中提取symbols.isolate字典,由于该自上而下的ReflectService包含的symbols.isolate字典形成一个原型链,所以根据指定的名称总能得到一个Symbol(以为上一步已经保证了这一点); - 然后创建
Impl对象,然后以上面提取的Symbol作为Key存储在store字典中,在存储之前同样需要验证是否进行重复注册;从这个意义上讲,服务实例被存储在距当前最近的那个对当前服务进行隔离的那个ReflectService中,也就是最近的那个通过调用isolate(name)生成的子ReflectService中; Impl对象除了会存储在RefectService的sotre字典中,还会冗余存储于当前Fiber的store字典中;- 如果当前
Fiber的状态为激活状态,通过调用notifiy方法对外发送通知,注入此服务的插件就知道依赖服务之一上线了。如果注入的所有依赖服务都处于激活状态,当前插件才会真正执行。
针对上述的服务注册流程对整个系统造成的副作用,我们就能理解用于取消服务注册的执行流程了:
- 首先根据上面解析出来的唯一标识服务实例的
Symbol,以它为Key将封装服务的Impl对象从store字典中移除; - 同样以服务名称作为参数调用
notifiy方法对外发送通知,得到一组注入了该服务的所有插件对应的Fiber对象,通过调用它们的await方法直到它们下线; - 最后根据名称将
Impl对从当前Fiber的store字典中移除。
3. 服务状态改变的通知
服务是为插件服务的(在Cordis中服务其实也可以注册为插件),我们通过在插件中注入服务列表的方式来构建两者之间的依赖关系。Cordis要求注入到插件中的所有依赖服务全部上线才能执行,与之相对,一旦依赖服务下限,插件将不能保证正常工作,也将下线。所以在服务完成注册,以及服务注册的接触,都需要利用Cordis的事件总线来发送相应的通知,具体就体现在ReflectService的notify方法中。
要将notify方法的逻辑搞清楚,需要弄清楚该方法涉及的一个核心的接口Runtime,Runtime与插件有关。通过DeepSeek Harness插件内核-01:DeepSeek Harness三种插件形式 提供了三种插件定义形式,但最终都体现为一个函数。Cordis将这个函数成为插件形状(plugin shape)。当我们调用RegistryService第一次以插件形式对该函数进行注册时,它会创建一个Runtime对象,其name属性就是指定得插件名称,与插件绑定的Fiber会添加到filbers列表中,callback属性返回的就是这个插件函数,而可缺省的Config属性(这里首字母应该小写)为用来确定插件配置是否合法的验证器。如果将同一个函数作为插件多次注册,将会沿用以后的Runtime,并为新注册的插件创建用来管理生命周期的Fiber对象,该对象最终被添加到对应Runtime的fibers列表中。
export interface Runtime {
name?: string
fibers: DisposableList<Fiber>
callback: globalThis.Function
Config?: StandardSchemaV1
}
notify方法有两个参数,前者表示涉及状态改变的服务名称列表,后者提供一个过滤函数来选择真正受影响的插件,默认提供的规律函数表达的意思是:由于基于symbols.isolate字典的服务注册隔离的存在,导致同一个名称可能对应着不同服务实例,所以在确定某个插件是否真正依赖当前服务时,不能只关注服务名称,应该比较唯一标识服务实例的Symbol,该标识存储在Context的symbols.isolate字典中,相关机制可用参阅我的文章DeepSeek Harness插件内核-06:ReflectService全面解析
export class ReflectService {
notify(names: string[], filter = (ctx: Context, name: string)
=> ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
const fibers: Fiber[] = []
for (const runtime of this.ctx.registry.values()) {
for (const fiber of runtime.fibers) {
let hasUpdate = false
for (const name of names) {
if (!(name in fiber.inject)) continue
if (!filter(fiber.ctx, name)) continue
hasUpdate = true
fiber._checkImpl(name)
}
if (!hasUpdate) continue
fiber._refresh()
fibers.push(fiber)
}
}
for (const name of names) {
const self: Context = Object.create(this.ctx)
self[symbols.filter] = (target: Context) => filter(target, name)
this.ctx.events.emit(self, 'internal/service', name,
this._getImpl(name, false)?.value)
}
return fibers
}
}
我们来总结一下notify方法的执行流程:
- 它从
RegistryService中提取封装了所有插件的Runtime对象,从中找到依赖当前提供服务的所有插件的Fiber对象,具体流程如下:- 遍历所有
Runtime的fibers列表; - 对于每个
Fiber,利用filter函数验证其是否注入的当前提供的服务; - 如果有则收集起来,于此同时还会对
Fiber实施如下的操作:- 调用
Fiber的_checkImpl方法对依赖服务实施检验:如果对应的服务从RefjectService中获取不到,或者无法通过Impl的check方法的激活性检验,直接移除对应的Impl对象; - 针对所有依赖服务的检验完成之后,再次调用
_refresh进行刷新:遍历所有注入服务,如有有任何一个不存在(上一步被删除了),将插件自身的状态设置为非激活状态;
- 调用
- 遍历所有
- 在完成了受影响插件(
Fiber)收集工作之后,针对每个提供的服务:- 以当前
ReflectService为原型创建一个新的ReflectService; - 设置用于过滤
ReflectService的symbols.filter属性,设置的函数会调用指定的filter方法确定待检验的ReflectService是否有当前服务的影响; - 在新创建的这个
ReflectService上以emit形式发送internal/service事件,并指定服务名称和封装服务的Impl作为参数将服务该表的通知发送出去;
- 以当前
- 最后返回代表受影响插件绑定的
Fiber列表。
4. 服务的提取和重新设置
ReflectService提供了私有方法_getImpl利用指定的名称提取封装了指定服务的Impl对象,该方法的第二个可缺省参数表示是否需要验证提供此服务的Fiber对象是否处于激活状态,默认值为true。整个提取分两步:第一部从当前ReflectService的symbols.isolate中得到指定服务名称对应的Symbol,然后根据后者从store中提取对应的Impl对象,并作针对性的激活性检验。
export class ReflectService {
get(name: string, strict = true) {
return getTraceable(this.ctx, this._getImpl(name, strict)?.value)
}
set(name: string, value: any, error?: Error) {
const key = this.ctx[symbols.isolate][name]
const impl = this.store[key]
if (!impl) {
throw new Error(`cannot set property "${name}" without provide`)
}
if (impl.fiber !== this.ctx.fiber) {
throw new Error(`cannot set property "${name}" in multiple fibers`)
}
impl.value = value
return true
}
_getImpl(name: string, strict = true) {
const key = this.ctx[symbols.isolate][name]
const impl = key && this.store[key]
if (!impl) return
if (strict && impl.fiber.state !== FiberState.ACTIVE) return
return impl
}
}
ReflectService提供的get会调用_getImpl方法得到封装服务的Impl对象,并利用value属性提取服务对象,最后将给getTraceable方法转换成一个能够自动跟踪当前ReflectService的服务对象。个人认为getTraceable是整个Cordis体系最为核心的方法,它让同一个服务实例能够动态感知作为调用方法的插件,并在该插件指定的ReflectService中执行。如果对此感兴趣,可以参阅我的文章DeepSeek Harness插件内核-07:Context全面解析。proivie进行首次注册的服务可以通过set方法进行修改。它仅仅是修改现有Impl对象的value属性而已。
5. 为Context定义计算属性
ReflectService为ReflectService添加的属性只有上述的服务和计算属性两种形式,分别对应Property.Service和Property.Accessor接口。计算属性通过如下所示的accessor方法来定义,accessor函数的options参数的类型表明:我们指定的是一个剔除调type属性的Property.Accessor对象。指定的Property.Accessor对象会以指定的名称注册到props字典中。
export class ReflectService {
accessor(name: string, options: Omit<Property.Accessor, 'type'>) {
return this.ctx.fiber.effect(() => {
if (name in this.props) {
throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
}
this.props[name] = { type: 'accessor', ...options }
return () => delete this.props[name]
}, `ctx.accessor(${JSON.stringify(name)})`)
}
}
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
从定义了看出,accessor方法针对计算属性的注册依然实在当前ReflectService的Fiber对象的effect方法调用中执行的,如果执行返回的函数或者调用Fiber的dispose方法,注册的计算属性将会消失。如下就是一个典型的例子:
import { Context} from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis'{
interface Context {
firstName:string
lastName:string
fullName:string
}
}
let context = new Context();
context = context.extend({firstName: "Jayden", lastName: "Jiang"});
var dispose = context.reflect.accessor("fullName",{
get(this: Context, receiver: any, error: Error) {
return `${this.firstName} ${this.lastName}`;}
});
console.assert(context.fullName == "Jayden Jiang");
dispose();
console.assert(context.fullName === undefined);
6. 为指定的服务赋予跟踪当前Context的能力
如果我们希望将自动跟踪当前ReflectService的能力赋予指定的一个常规对象,我们可以直接调用ReflectService的trace方法,它会旨在内部调用我们在DeepSeek Harness插件内核-07:Context全面解析中花了整个篇幅介绍的getTraceable函数。
export class ReflectService {
trace<T>(value: T) {
return getTraceable(this.ctx, value)
}
}
在如下的演示程序中,AnyService对象的ctx属性原本就是undefined,但是经过context.reflect.trace()方法的包装后,它的ctx自动会与当前ReflectService绑定。
import { Context , getTraceable, symbols,Tracker} from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis'{
interface Context {
firstName:string
lastName:string
fullName:string
}
}
let context = new Context();
context = context.extend({firstName: "Jayden", lastName: "Jiang"});
context.reflect.accessor("fullName",{
get(this: Context, receiver: any, error: Error) {
return `${this.firstName} ${this.lastName}`;}
});
class AnyService{
[symbols.tracker]: Tracker = { property : "ctx"}
ctx:Context|undefined = undefined
}
const service = context.reflect.trace( new AnyService());
console.assert(service.ctx?.firstName == "Jayden");
console.assert(service.ctx?.lastName == "Jiang");
console.assert(service.ctx?.fullName == "Jayden Jiang");
7. 将函数的参数和执行上下文this绑定为当前Context
ReflectService提供了如下这个bind方法对指定的函数进行封装生成具有相同签名的代理函数,使代理函数函数在执行的时候也能自动跟踪当前的ReflectService。具体实现分如下两种情况:
- 如果作为常规函数被调用:代理处理器的
apply方法会将使用上面介绍的trace方法所有的参数和作为执行上下文进行代理化; - 如果作为构造函数被调用:代理处理器的
construct方法会使用trace方法对所有参数进行代理化。
export class ReflectService {
bind<T extends Function>(callback: T) {
return new Proxy(callback, {
apply: (target, thisArg, args) => {
return Reflect.apply(target, this.trace(thisArg), args.map(arg => this.trace(arg)))
},
construct: (target, args, newTarget) => {
return Reflect.construct(target, args.map(arg => this.trace(arg)), newTarget)
},
})
}
}
在如下这个演示程序中,如果直接调用getProfile函数,作为参数的FooService和BarService,以及作为执行上下文的thisArg,它们的ctx属性都是undefined。但是经过[symbols.invoke].reflect.bind方法包装了一下,我们就能利用它得到我们希望的结果。
import { Context , symbols,Tracker} from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis'{
interface Context {
firstName:string
lastName:string
gender:string
}
}
class FooService{
[symbols.tracker]: Tracker = { property : "ctx"}
ctx:Context|undefined = undefined
get firstName(){return this.ctx?.firstName;}
}
class BarService{
[symbols.tracker]: Tracker = { property : "ctx"}
ctx:Context|undefined = undefined
get lastName(){return this.ctx?.lastName;}
}
function getProfile(this:{ctx:Context}, foo:FooService, bar:BarService){
return `
FirstName: ${foo.firstName}
LastName: ${bar.lastName}
Gender: ${this.ctx.gender}`
}
const thisArg = {
[symbols.tracker]: { property : "ctx"},
ctx: undefined as any as Context
}
console.log( new Context()
.extend({firstName: "Jayden", lastName: "Jiang", gender: "Male"})
.reflect.bind(getProfile)
.call(thisArg, new FooService(), new BarService()));
输出:
FirstName: Jayden
LastName: Jiang
Gender: Male
8. 为Context混入新成员
ReflectService定义了如下这个 mixin 方法是 Cordis 框架中用于简化开发者体验的核心语法糖。它的核心作用是将深层服务(Service)的属性或方法,动态地平铺并混入到最外层的 ctx 上,同时利用 Generator(生成器)确保这些混入属性能够随着插件的生命周期自动加载与卸载。
export class ReflectService {
mixin(source: any, mixins: string[] | Dict<string>) {
const self = this
return this.ctx.fiber.effect(function* () {
const entries = Array.isArray(mixins)
? mixins.map(key => [key, key])
: Object.entries(mixins)
const getTarget = (ctx: Context, error: Error) => {
return ctx[source]
}
for (const [key, value] of entries) {
yield self.accessor(value, {
get(receiver, error) {
const service = getTarget(this, error)
if (isNullable(service)) return service
const mixin = receiver ? withProps(receiver, service) : service
const value = Reflect.get(service, key, mixin)
if (typeof value !== 'function') return value
return value.bind(mixin ?? service)
},
set(value, receiver, error) {
const service = getTarget(this, error)
const mixin = receiver ? withProps(receiver, service) : service
return Reflect.set(service, key, value, mixin)
},
})
}
}, `ctx.mixin(${JSON.stringify(source)})`)
}
}
如上面的代码所示,mixin 方法本质上是通过调用上面介绍的accessor方法将source对象指定的成员混入进当前ReflectService。这里使用了一个生成器函数(function*) 作为 effect 的回调。内部每次 yield self.accessor(...) 时,都会创建并登记一个动态访问器。Cordis 的 fiber.effect 会自动迭代这个生成器。当插件(Fiber)被卸载时,Cordis 会倒序自动执行所有 yield 返回的清理函数(即自动执行 delete props[name]),将混入的属性从 ctx 上彻底擦除,防止热重载时的内存泄漏与属性残留。
我们再来看看ReflectService构造函数的定义。由于它附加在symbols.tracker属性上的Tracker对象将property属性设置为ctx, 所以当前ReflectService会绑定到它的ctx属性上,noShadow属性被设置成true,表明构造时提供的ReflectService对它毫无意义,不需要利用影子上下文将它存储起来已被不时之需。在这之后,它通过调用mixin方法将四大核心服务的很多方法混入当前ReflectService,所以针对这些服务的很多方法的调用都可以作用于ReflectService对象之上。在后面的文章中,我们将直接使用混入的方法。
export class ReflectService {
constructor(public ctx: Context) {
defineProperty(this, symbols.tracker, {
property: 'ctx',
noShadow: true,
})
this.mixin('reflect', ['get', 'set', 'provide', 'accessor', 'mixin'])
this.mixin('fiber', ['runtime', 'effect'])
this.mixin('registry', ['inject', 'plugin'])
this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])
}
}

349

被折叠的 条评论
为什么被折叠?



