DeepSeek Harness深度拆解-08:如何实现在自定义的逻辑范围内的隔离通过实例演示的方式体现了@deepseek-ai/dsh-scope提供的基于Scope的隔离机制。通过提供的createScope函数,我们可用构建一个逻辑层面的Scope树,并将节点动态绑定到Context树的某个节点上,再利用scopeTarget函数提供的对象作为触发事件的主体,该对象会利用提供的过滤函数让基于某个Scope发出的事件只能在限定的Scope内传播。结合使用ScopedLayers,我们可用实现基于Scope的可继承注册表。接下来我们就来看看这一切都是如何实现的。
1. ScopeKey和Scope树的构建
Scope树的节点通过如下这个Scope接口表示,每个Scope通过一个ScopeKey对象作为唯一标识。ScopeKey本质上就是object类型的别名。
export type ScopeKey = object
Scope树通过如下这个类型为WeakMap<ScopeKey, ScopeKey>的全局弱引用字段来维护,每个KV表示多一个具有父子关系的ScopeKey对,父子是一对多的关系,所以这里Key和Value分别是Child和Parent。
const scopeParents = new WeakMap<ScopeKey, ScopeKey>()
如下所示的linkScopeParent函数会根据指定的两个ScopeKey,构建一个KV并添加在scopeParents全局字典中。在添加之前会进行环路检验。我们可调用的bindScopeParent这个导出的函数,它除了调用linkScopeParent方法完成指定的这对父子关系的构建外,还利用返回对象提供的rebind方法实现针对另一个Parent的重新绑定。bindScopeParent函数返回类型被声明为ScopeParentBinding接口。
export function bindScopeParent(key: ScopeKey, parent: ScopeKey): ScopeParentBinding {
if (scopeParents.has(key)) {
throw new Error('dsh-scope: scope key is already bound to a parent; re-linking requires the binding returned by the original bind')
}
linkScopeParent(key, parent)
return {
rebind(next: ScopeKey): void {
linkScopeParent(key, next)
},
}
}
function linkScopeParent(key: ScopeKey, parent: ScopeKey): void {
for (let cursor: ScopeKey | undefined = parent;
cursor !== undefined;
cursor = scopeParents.get(cursor)) {
if (cursor === key) throw new Error('dsh-scope: scope parent link would form a cycle')
}
scopeParents.set(key, parent)
}
export interface ScopeParentBinding {
rebind(parent: ScopeKey): void
}
2. Scope的创建
Scope树的节点通过如下这个Scope接口表示,它本质上是对一个Context的封装。Scope同时是一个Disposable对象,可用调用其dispose方法将其终结释放。
export interface Scope {
ctx: Context
rawDispose: () => Promise<void> | void
dispose(): Promise<void>
}
定义在Scope接口的三个成员说明如下:
- ctx:该Scope绑定的Context;
- rawDispose:原始释放函数。它是真正执行清理的逻辑,可能是同步或异步。以raw作为命名前缀是因为它通常不包含框架的包装逻辑(幂等、并发控制、事件触发、子Scope清理等),一般不直接对外暴露或调用;
- dispose:对外释放方法,统一返回Promise<void>。
Scope对象最终通过如下这个createScope函数根据指定Conext、ScopeKey和可选的CreateScopeOptions创建而成。CreateScopeOptions利用parent提供作为Parent标识的ScopeKey,这是一个可选的参数。如果利用这个配置选项提供了Parent节点,会调用上述的bindScopeParent函数建立父子关系。
export function createScope(ctx: Context, key: ScopeKey,
options?: CreateScopeOptions): Scope {
if (options?.parent !== undefined) bindScopeParent(key, options.parent)
const fiber = ctx.plugin(scope)
const scoped: Context = fiber.ctx.extend({ [kScope]: key })
let disposing: Promise<void> | undefined
return {
ctx: scoped,
rawDispose: fiber.dispose,
dispose: () => (disposing ??= quiesceFiber(fiber)),
}
}
function scope(): void {}
const kScope = Symbol('dsh.scope')
由于Scope同时是一个Disposable对象,需要一个生命周期管理器。在Cordis的世界里,说到管理生命周期,自然会想到Fiber。从createScope函数的定义可用看出,Scope对象也采用Fiber来管理自己的生命周期。由于Fiber随着插件的注册而创建,所以虽然不需要一个具体的插件,这里不得不调用指定Context的plugin方法注册了一个什么都没做的插件函数,就是为了得到这个Fiber对象。我们知道插件是与创建的子Context绑定在一起的,这个字Context通过调用extend方法作进一步扩展,其目的是将当前Scope的Key利用kScope常量标识的Symbol附加到Context上,Socpe的ctx字段最终返回的就是这个Context。
至于创建的Scope对象的其他两个方法,rawDispose直接对应Fiber对象的dispose方法,dispose方法返回的Promise<void>是如下这个quiesceFiber函数的返回值,该方法在调用Fiber对象的dispose方法之余,还会利用一个循环内持续调用Fiber的inertia方法,其目的在于确保整个释放过程完全结束。
async function quiesceFiber(fiber: Fiber): Promise<void> {
await Promise.resolve(fiber.dispose())
while (fiber.inertia !== undefined) await fiber.inertia
}
3. 根据Scope过滤事件处理器
DeepSeek Harness插件内核-15:再谈Cordis的事件总线对Cordis事件总线的事件分发机制进行了详细介绍,我们知道除了internal/update这种特殊的事件会将作为事件函数至于Fiber自身的_hooks字段外,其余的事件处理函数都会被封装成Hook被注册到EventsService这个单例服务对象的_hooks字典中。所以在不进行任何过滤的前提下,只要指定了正确的事件名称,不论当前身在何处,都能监听到来自当前Context树任何一个地方触发的事件。
但是如果在分发事件的时候指定了一个主体对象,该对象除了会作为事件处理函数的执行上下文(this)之外,定义其中的通过symbols.filter这个Symbol标识的方法还会用来过滤获选的Hook对象。这个过滤方法的签名(Context)=>boolean,实施过滤时将会使用Hook的ctx字段作为参数。scopeTarget函数正是利用了这一原理创建了一个对象,并利用当前Scope定义了这个过滤函数实现了事件向上传递的特性。
如下面的代码所示,scopeTarget函数提供了两个参数。base参数提供的对象可以利用自身的symbols.filter方法实施前置过滤,key参数则提供一个ScopeKey表示触发事件所在的Scope。scopeTarget函数最终返回一个只包含symbols.filter方法的对象。
export function scopeTarget<T extends object>(base: T, key: ScopeKey | undefined): Scoped<T> {
const baseFilter = (base as { [CordisContext.filter]?: (ctx: Context) => boolean })[CordisContext.filter]
const carrier = {
[CordisContext.filter](ctx: Context): boolean {
if (baseFilter !== undefined && !baseFilter.call(base, ctx)) return false
const tag = scopeOf(ctx)
if (tag === undefined) return true
for (let cursor = key; cursor !== undefined; cursor = scopeParents.get(cursor)) {
if (cursor === tag) return true
}
return false
},
}
carrierKeys.set(carrier, key)
return carrier as unknown as Scoped<T>
}
export function scopeOf(ctx: Context): ScopeKey | undefined {
return (ctx as Context & { [kScope]?: ScopeKey })[kScope]
}
const carrierKeys = new WeakMap<object, ScopeKey | undefined>()
返回对象的过滤方法具有这样的逻辑:
- 如果base参数指定的对象定义了过滤方法,并且返回false,直接返回false;
- 调用scopeOf函数提取Context携带的Scope标识,如果不能存在,意味Context并没有被限定为某个Scope,直接返回true;
- 根据参数key指定的Scope作为起点在Scope树(通过scopeParents表示)中不断向上追溯,只要遇到一个Scope与当前Context携带的Scope一致,这意味着Hook对应的Context在触发事件源Scope的向上继承链路上,符合事件向上传递的法则,返回true;
- 不满足以上条件,即表明Hook并不在监听的目标范围内,返回false阻止它的执行。
scopeTarget函数返回的对象被视为一个携带过滤方法的对象会被添加到carrierKeys这个全局字典中,用来存储这样的对象与对应ScopeKey的映射。scopeTarget函数的返回类型被声明为Scoped<T>,定义如下。
export type Scoped<T extends object> = object & { readonly [ScopedBrand]: T }
declare const ScopedBrand: unique symbol
4. 基于ScopedLayers的注册表
Scope树具有两种消费形式,一种就是利用scopeTarget函数构建一个主体对象来过滤事件监听器,另一种就是利用ScopedLayers<L>构建一个具有层级继承关系的注册表。我们可以将ScopedLayers<L>视为多层注册表的集合,而作为泛型参数的L表示具体某个单层的注册表。L派生于接口ScopeLayer,后者仅仅定义了唯一的isEmpty方法来确定注册表是否为空。
export class ScopedLayers<L extends ScopeLayer> {
readonly global: L
private readonly scoped = new Map<ScopeKey, L>()
constructor(
private readonly createLayer: (scope: ScopeKey | undefined) => L,
private readonly onChange: () => void) {
this.global = createLayer(undefined)
}
}
export interface ScopeLayer {
isEmpty(): boolean
}
如ScopedLayers<L>的定义可知,它利用global字段返回的ScopeLayer来表示全局共享的注册表,而scoped字段返回的字典则用来表示基于某个Scope的注册表。构造函数提供如下两个参数(同时绑定为同名的字段):
- createLayer: 用来创建ScopeLayer的工厂函数;
- onChange: 注册表发生更新后的回调函数。
当ScopedLayers被构造的时候,由参数createLayer指定的工厂函数将来一个ScopeLayer对象,并将其作为全局注册表赋值给global字段。如下定义的effect方法是ScopedLayers最为核心的方法,因为我们会调用此方法并利用作为参数的action函数来完成具体的注册操作。除了action参数,该方法提供的ctx参数体现了注册操作针对的Scope,如果没有绑定具体的Scope,意味着直接注册到全局注册表中。该方法最终返回Fiber同名方法的调用,options参数的label最终会作为调用该方法的同名参数,notify字段表示注册成功或者解除注册时是否调用onChange函数来发送通知。
export class ScopedLayers<L extends ScopeLayer> {
effect(
ctx: Context,
action: (layer: L) => () => void,
options: { label: string; notify?: boolean },
): () => void {
const scope = scopeOf(ctx)
const notify = options.notify ?? true
const dispose = ctx.effect(function* (this: ScopedLayers<L>) {
let layer: L
let created = false
if (scope === undefined) {
layer = this.global
} else {
const existing = this.scoped.get(scope)
if (existing === undefined) {
layer = this.createLayer(scope)
this.scoped.set(scope, layer)
created = true
} else {
layer = existing
}
}
let undo: () => void
try {
undo = action(layer)
} catch (error) {
if (scope !== undefined && created && layer.isEmpty()) this.scoped.delete(scope)
throw error
}
yield () => {
undo()
if (scope !== undefined && layer.isEmpty()) this.scoped.delete(scope)
if (notify) this.onChange()
}
if (notify) this.onChange()
}.bind(this), options.label)
return dispose
}
}
如上述的代码所示,在将指定Context绑定的Scope提取出来后,方法调用Context对象effect方法(其实就是Fiber的effect方法),并利用设置的生成器函数执行如下的操作:
- 确定操作的ScopeLayer:
- 如果当前Context没有绑定一个具体的Scope,直接使用global字段返回的作为全局注册表的ScopeLayer对象;
- 否则从scoped字典提出对应的ScopeLayer对象,如果不存在就调用createLayer函数创建一个新的ScopeLayer对象并添加到scoped字典中。
- 将上面解析出来的ScopeLayer作为参数调用action函数实施注册操作,并得到返回的清理函数(比如解除注册操作就可以实现在这里);
- 如果开启通知,则调用onChange函数;
- yield语句会保证执行返回的函数会:
- 执行action函数返回的清理函数;
- 如果ScopeLayer为空,且不是全局注册表,则将其从scoped字典中移除。
ScopedLayers还定义了如下三个方法:
- peek:从scope字典中提取指定Scope的ScopeLayer;
- chainLayers:提取指定Scope及其上级的所有ScopeLayer(不包含global),按照Scope从高到低排序;
- merge<V>: 将指定Scope及其上级的所有ScopeLayer(包含global)注册的条目合并成字典(Map<string, V>)输出。
export class ScopedLayers<L extends ScopeLayer> {
peek(scope: ScopeKey | undefined): L | undefined {
if (scope === undefined) return undefined
return this.scoped.get(scope)
}
chainLayers(scope: ScopeKey | undefined): L[] {
const layers: L[] = []
for (const key of scopeChainOf(scope).reverse()) {
const layer = this.scoped.get(key)
if (layer !== undefined) layers.push(layer)
}
return layers
}
merge<V>(
scope: ScopeKey | undefined,
pick: (layer: L) => NamedEntries<V>): Map<string, V> {
const merged = new Map(pick(this.global).entries())
for (const layer of this.chainLayers(scope)) {
for (const [name, value] of pick(layer).entries()) merged.set(name, value)
}
return merged
}
}
export function scopeChainOf(key: ScopeKey | undefined): ScopeKey[] {
const chain: ScopeKey[] = []
for (let cursor = key; cursor !== undefined; cursor = scopeParents.get(cursor)) chain.push(cursor)
return chain
}
merge<V>方法除了传入标识Scope的ScopeKey之外,还需要提供一个函数将ScopeLayer转换成NamedEntries<V>。我们自定义的ScopeLayer一般使用NamedEntries<V>来存储注册的需要命名的条目。与之对应的AnonymousEntries<V>,它们都派生自EntryValues<V>接口。
interface EntryValues<V> {
values(): IterableIterator<V>
isEmpty(): boolean
}
export class NamedEntries<V> implements EntryValues<V> {
private data = new Map<string, V>()
constructor(private readonly duplicateError: (name: string) => Error)
insert(name: string, value: V): () => void
get(name: string): V | undefined
has(name: string): boolean
keys(): IterableIterator<string>
entries(): IterableIterator<[string, V]>
values(): IterableIterator<V>
isEmpty(): boolean
}
export class AnonymousEntries<V> implements EntryValues<V> {
private data = new Map<symbol, V>()
append(value: V): () => void
values(): IterableIterator<V>
isEmpty(): boolean
}
