Cordis插件针对服务的消费具有一个基本的原则:只有在显式注入的前提下才能消费目标服务。但是一旦完成了服务注入,Cordis就会认为当前插件没有依赖服务的参与就不能正常工作,所以它会将插件的生命周期和服务生命周期绑定在一起。对于插件来说,只有它所有注入的依赖服务全员上线,它才能上线;反之,如果在运行过程中有任何一个依赖服务下线,自己也改不了下线的命运,所以插件与服务之间就是这种硬性依赖的关系。但是很多时候插件需要保持始终处于激活的状态,消费的服务也不是必需的,就需要通过一些变通将针对服务的依赖变得软性一点。
1. 插件只能消费显式注入的服务
先注入,后消费是Cordis进行服务调用的一条铁律。以如下的演示程序为例,我们定义了FooService和BarService这两个实现了FoobarService接口的服务,并在它们的invoke方法中输出一点指示性文字确定当前调用的服务类型。我们调用plugin方法将这两个服务注册到创建的Context中,并在另一个通过调用inject方法注册的插件中试着消费这两个服务。
import{Context,Service}from'@deepseek-ai/cordis'interfaceFoobarService{invoke():void}declaremodule'@deepseek-ai/cordis'{interfaceContext{foo:FoobarService bar:FoobarService}}classFooServiceextendsService{constructor(ctx:Context){super(ctx,"foo")}invoke(){console.log("FooService.invoke() is invoked!")}}classBarServiceextendsService{constructor(ctx:Context){super(ctx,"bar")}invoke(){console.log("BarService.invoke() is invoked!")}}constctx=newContext()ctx.plugin(FooService)ctx.plugin(BarService)ctx.inject(["foo"],ctx=>{for(constserviceof[()=>ctx.foo,()=>ctx.bar]){try{service().invoke();}catch(err){console.log(err)}}})输出:
FooService.invoke()is invoked!Error: cannot get property"bar"without inject at<anonymous>(E:\dsh\deepseek-harness\explor\src\downgrade.ts:36:49)at Object.ctx(E:\dsh\deepseek-harness\explor\src\downgrade.ts:38:13)at Fiber.execute(E:\dsh\deepseek-harness\vendor\cordis\src\fiber.ts:259:28)at<anonymous>(E:\dsh\deepseek-harness\vendor\cordis\src\fiber.ts:366:45)at composeError(E:\dsh\deepseek-harness\vendor\cordis\src\utils.ts:272:25)at Fiber._execute(E:\dsh\deepseek-harness\vendor\cordis\src\fiber.ts:358:12)at Fiber._reload(E:\dsh\deepseek-harness\vendor\cordis\src\fiber.ts:656:20)at process.processTicksAndRejections(node:internal/process/task_queues:104:5)由于inject方法只指定了foo服务,所以插件中只能调用注册的FooService。调用BarService会直接抛出异常,并提示cannot get property "bar" without inject。
2. 插件生命周期受控于依赖的服务
Cordis内部会维护一个服务与插件依赖于它的插件所在Fiber的映射关系。当某个服务下线时,Cordis会利用这个映射找到对用的Fiber对象,并使用它让插件也下线。反之,当服务重写上线后,也会便利所有的映射的Fiber,如果对应插件注入的服务全部处于激活的状态,对应的插件会被重新拉起来。下面就是一个典型的例子:
import{Context,Service}from'@deepseek-ai/cordis'...functiondelay(ms:number):Promise<void>{returnnewPromise(resolve=>setTimeout(resolve,ms))}constctx=newContext()constfoo=ctx.plugin(FooService)constbar=ctx.plugin(BarService)ctx.inject(["foo","bar"],ctx=>{console.log("plugin starts...")consttimer=setInterval(()=>console.log("Plugin is alive."),1000)ctx.effect(()=>()=>{console.log("Plugin is disposed...")clearInterval(timer)})})ctx.plugin(asyncctx=>{awaitdelay(3000)foo.restart();awaitdelay(3000)bar.dispose();delay(10000)})如代码所示,我们调用inject方法同时注入了foo和bar服务注册了一个插件,该插件会在启动的时候输出plugin starts...,然后以1秒为间隔输出Plugin is alive.表明自己还活着。我们通过调用Context的effect方法输出Plugin is disposed...,以为着这段文件会在插件下线的时候被输出。在另一个注册的插件中,会在启动后3秒时调用注册FooService返回的Fiber的restart方法实施重启,然后在6秒后调用FooService对应Fiber的dispose方法。整个程序运行后会生成如下的输出:
plugin starts... Plugin is alive. Plugin is alive. Plugin is disposed... plugin starts... Plugin is alive. Plugin is alive. Plugin is disposed...从输出结果可以看出,注册插件的生命周期完全收到依赖服务的控制。任一依赖服务所在Fiber的重启都将导致插件的重启(前提时所有依赖服务全员在线),如果任何一个依赖服务下线后没有再起来,插件将永远来起不来。
3. 既能消费服务,又保持插件独立的生命周期
如果消费的服务并非插件必需,比如我们在它上面应用了降级策略,当检测到某个服务不可能时,立即使用另一个服务来替换,这是保持可用性的常用策略。但是如何将这套策略应用到某个插件上呢?现在面临的困境在于:消费服务必需注入,注入就导致两者之间生命周期的依赖。其实问题很好解,那就是将服务注入到之间的子插件中,并利用后者来提供消费的服务和检测服务的状态。
以如下的演示程序为例,我们直接调用plugin方法注册了我们的插件,并定义了两个FoobarService|undefined类型的primary和secondary变量,分别表示主服务和后背的降级服务。插件会以1秒的间隔持续调用服务,如果primary不可用,就使用secondary。
import{Context,Service}from'@deepseek-ai/cordis'...constctx=newContext()constfoo=ctx.plugin(FooService)ctx.plugin(BarService)ctx.plugin(ctx=>{letprimary:FoobarService|undefinedletsecondary:FoobarService|undefinedctx.inject(["foo"],ctx=>{primary=ctx.fooreturn()=>primary=undefined})ctx.inject(["bar"],ctx=>{secondary=ctx.barreturn()=>secondary=undefined})consttimer=setInterval(()=>(primary??secondary)?.invoke(),1000)ctx.effect(()=>()=>clearInterval(timer))})awaitdelay(3000)console.log("unload FooService...")foo.dispose()awaitdelay(3000)console.log("register FooService...")ctx.plugin(FooService)插件内部两次调用inject方法基于注入的foo和bar服务注入了两个子插件。如果它们上线,意味着依赖的服务必然处于激活状态,此时它们分别对primary和secondary变量进行设置。由于这两个子插件没有复杂的操作,foo或者bar服务就是它们唯一的依赖,初始它们下线的唯一原因就是服务在可用,此时只需要将primary或者secondary设置为undefined即可。
在上面的演示程序中,我们会在插件注册3秒后调用FooService所在Fiber的disopose方法,意味着此时主服务不再可用,后备的降级服务BarService会自动顶上去。程序运行的输出结果体现了这一点。再等3秒,我们重新注册FoobarService,插件将再次使用此服务。
FooService.invoke()is invoked!FooService.invoke()is invoked!unload FooService... BarService.invoke()is invoked!BarService.invoke()is invoked!BarService.invoke()is invoked!register FooService... FooService.invoke()is invoked!FooService.invoke()is invoked!FooService.invoke()is invoked!...