深色模式
Demo 3 · 依赖注入与组合
目标:跑通 inject 的"等待与级联",理解 isolate 作用域,并见识一个真实的时序陷阱。对应原理篇 02.3–02.4。代码:
demos/03-compose/main.ts。
运行
sh
cd demos
npm run demo:3预期输出(节选):
text
== 1. 先挂 reporter(依赖未满足,保持 PENDING)==
reporter fiber 状态:PENDING
== 2. 依次挂 database 与 cache ==
不 await 立即读 root.database = undefined
[database] 连接已建立:postgres://localhost/demo
await 之后读 root.database = [object Object]
[cache] 缓存服务已就绪(database 一定先就绪)
[reporter] 启动,看到的 database = postgres://localhost/demo
reporter fiber 状态:ACTIVE
== 4. isolate:子作用域里换一套 database ==
[database] 连接已建立:sqlite://:memory:
[reporter] 启动,看到的 database = sqlite://:memory:
== 5. 卸载 database:观察级联 ==
根 reporter fiber 状态:PENDING(回到 PENDING 等新 database)输出解读
① PENDING:等待不是错误
reporter 声明 inject: ['database', 'cache'],被挂载时两个服务都不存在。它的 fiber 停在 PENDING,不报错、不执行、静默等待。服务齐了才变成 ACTIVE。这就是"加载顺序由依赖推导"的运行形态。
② 时序陷阱:直接读属性的不可靠
text
不 await 立即读 root.database = undefined
await 之后读 root.database = [object Object]同一个 root.database,在提供者 fiber 加载完成前后读到的东西不一样。这正是 dsh 要求跨插件访问一律走 inject 的原因:inject 有类型保证(编译期)、有顺序保证(就绪才执行)、有级联保证(卸载一起走)。
③ 级联卸载
卸载 database 后,reporter 回到 PENDING、cache 被级联卸载,而子作用域(sqlite)不受影响。依赖关系是双向可追溯的:提供者没了,消费者全部退回等待;新的提供者出现,消费者自动复活。这就是 dsh 热重载的基础。
④ isolate:同名服务,两个世界
ts
const child = root.isolate('database') // database 服务走独立实现
child.plugin(DatabaseService, 'sqlite://:memory:')同一个 reporter 插件(同样的 inject 声明),挂在根作用域看到 postgres,挂在子作用域看到 sqlite。插件代码零改动,看到的世界由挂载位置决定——dsh 的 agent preset 就是这个机制:全局工具在根,agent 专属工具在 agent.ctx。
亲手做实验
实验 1:观察 cache 的启动日志
输出里 cache 只在 database 之后启动过。试着只挂 cache 不挂 database,cache 会怎样?
实验 2:两个 isolate 共享作用域
isolate('database') 传第二个参数(同一个 label symbol)给两个子上下文,它们会共享同一套 database 实现。验证一下。
实验 3:intercept
代码末尾的 intercept 只是演示了 API。试着给 database 服务加一个 config 类型(Service 的第二个类型参数),体会 intercept 如何为下方插件合并配置。
常见错误
| 现象 | 原因 |
|---|---|
cannot get required service "xxx" in inactive context | 在插件里读了 inject 声明之外的服务属性 |
| 插件永远不启动 | inject 的服务名拼错 / 提供者根本没挂 |
| 改了配置没生效 | 把"读属性"当成了依赖声明——记住正路是 inject |
下一步
前三个 Demo 都在 Cordis 的"真空"里。从 Demo 4 开始,我们进入 dsh 本体:把 Mock 适配器注册进真实的 LLM 服务。