Fiber
dsh 的 Fiber 同时表示一次插件挂载、当前 activation、effect 容器和配置状态。
Tether 把它拆成两个公开对象:EffectScope 拥有可回收资源,PluginHandle 控制插件的激活代次。
对照基准是 dsh dsh-v0.1.1-rc.1 的
Fiber 参考页 与
fiber.ts。
C# 实现见 EffectScope.cs 和
PluginHandle.cs。
对象关系
Context.Plugin(...) ──返回──> PluginHandle(挂载身份,跨重启保持)
│
├─ pending:没有 activation scope
└─ active:每个代次创建新的 fork Context
│
└─ EffectScope(本代 effect owner)
插件 ApplyAsync 收到的 Context 上,ctx.Scope 就是当前 activation 的 effect owner。依赖消失、手动重启或
apply 失败时,该 scope 被回收;同一个 PluginHandle 之后可以创建新 scope 进入下一代。
TS → C# 逐项对照
| TypeScript | C# | 状态与差异 |
|---|---|---|
ctx.effect(execute, label?) | ctx.Effect(disposer);ctx.Scope.OnDispose(disposer) | 近似。C# 直接登记 disposer,不执行一个返回 / yield disposer 的 effect body,也没有 label。 |
ctx.fiber | ctx.Scope;挂载方持有 PluginHandle | 职责拆分。插件内部可见 scope,控制句柄由调用 Plugin() 的一方持有。 |
fiber.uid | 无 | TS-only。C# 不公开 registry 内部 id。 |
fiber.ctx | EffectScope.Context | 近似。指向当前 scope 的 Context;PluginHandle 不公开 activation Context。 |
fiber.config | PluginHandle.Config | 近似。C# 是挂载时只读对象,没有句柄级原地 update。 |
fiber.state | EffectScope.State + PluginHandle.IsActive / IsDisposed | 职责拆分。没有一套与 TS 状态名完全相同的公开枚举。 |
fiber.dispose | PluginHandle.DisposeAsync() | 对应。终止挂载并等待当前 activation 清理。 |
fiber.store | 无公开快照 | TS-only。依赖通过 activation Context 的 Get<T>() / Require<T>() 读取。 |
fiber.inertia | PluginHandle.Completion;AwaitSettledAsync() | 近似。前者观察当前 apply,后者等待最新请求代次稳定。 |
fiber.name | handle.Plugin.GetType() | 近似。C# 没有专门 display-name 属性。 |
fiber.assertActive() | 无显式方法 | 语言差异。向 unloading scope 注册资源会抛 ObjectDisposedException。 |
fiber.effect(...) | EffectScope.OnDispose(...) | 近似。见 effect 模型差异。 |
fiber.getEffects() | 无 | TS-only。C# 当前没有带 label 的 effect 诊断树。 |
await fiber / fiber.await() | await handle.AwaitSettledAsync();await handle.Completion | 对应。推荐前者观察最新代次。 |
fiber.restart() | RestartAsync() | 对应。另有用于生命周期环的 RequestRestart()。 |
fiber.update(config, noSave?) | 无句柄级 API | TS-only。Loader / HMR 以替换挂载完成配置变更。 |
Effect | Action / Func<ValueTask> disposer | 近似。C# 不接受 promise / sync iterable / async iterable 的 effect body union。 |
Disposable | disposer delegate、IDisposable、IAsyncDisposable | 语言对应。不同注册对象用最适合的 .NET 生命周期接口。 |
EffectMeta | 无 | TS-only。 |
CordisError | ObjectDisposedException、PluginApplyException 等 | 近似。C# 使用具体异常类型,没有统一 string code。 |
ValidationError | DataAnnotations + CompositionException | 近似。配置绑定和校验位于 Loader,而不是 Fiber 类型内部。 |
EffectScope
公共属性
public Context Context { get; }
public EffectScope? Parent { get; }
public EffectScopeState State { get; }
public bool IsDisposed { get; }
public CancellationToken Cancellation { get; }
IsDisposed 在 State 为 Unloading 或 Disposed 时都为 true,表达的是“不再接受注册”,不是只表示清理已经结束。
Cancellation 在当前 scope 或任一祖先开始 disposal 时取消;插件中的长期异步工作应监听它。
EffectScopeState
public enum EffectScopeState
{
Active,
Unloading,
Disposed,
}
| 状态 | 可观察语义 |
|---|---|
Active | 接受 disposer、服务发布、订阅与子 scope。 |
Unloading | 整棵待回收子树已拒绝新注册,取消与清理正在进行。 |
Disposed | 清理结束;DisposeAsync() 可观察最终结果。 |
它不是 TS Fiber 的完整 state:pending / failed 属于 Registry 对插件句柄的分类,不属于某个 activation scope。
登记清理
public IDisposable OnDispose(Action disposer);
public IDisposable OnDispose(Func<ValueTask> disposer);
返回的 IDisposable 只会取消登记而不执行 disposer。Context 的 Effect(...) / OnDispose(...) 是方便入口,
但返回 void;需要提前撤销登记时才直接使用 Scope.OnDispose(...)。
TS fiber.effect(execute, label) 会立即执行 execute,收集单个、promise 或 iterable 形式的 disposer,
并返回一个“提前执行整组清理”的函数。C# 没有这层 effect group;资源 API 自己的注册对象承担提前清理,scope 是最终兜底。
回收
public ValueTask DisposeAsync();
public void RequestDispose();
DisposeAsync() 的顺序是:
- 把当前 scope 及后代标为
Unloading,拒绝新注册; - 取消
Cancellation,等待 activation quiescence; - 按创建逆序回收 child scope;
- 顺序派发
dispose,参数为被回收的 Context; - 按登记逆序运行当前 scope 的 disposer;
- 把所有独立失败聚合为
AggregateException,状态最终进入Disposed。
重复调用加入同一个 disposal outcome。清理回调同步段里对自身或祖先直接 DisposeAsync() 会被识别为生命周期环并立即返回;
回调一旦 yield,若仍需触发同一生命周期对象,应调用 RequestDispose() 发起而不等待。
PluginHandle
公共属性
public IPlugin Plugin { get; }
public object? Config { get; }
public bool IsActive { get; }
public Task Completion { get; }
public bool IsDisposed { get; }
Plugin和Config是挂载身份的一部分,句柄存续期间不更换。IsActive只表示当前插件 apply 已进入 active;缺依赖、失败、正在切代时可以为false。Completion在每次 activation 时替换,完成代表该次 apply 结束;非取消 apply 失败以PluginApplyExceptionfault。- 依赖消失导致的正常取消不把
Completion标为失败。 IsDisposed表示 terminal unmount 已开始,不表示所有异步清理已经结束。
因为 Completion 会被下一代替换,跨并发服务变更等待“当前最新状态”时应使用 AwaitSettledAsync()。
等待稳定代次
public ValueTask<long> AwaitSettledAsync();
该方法反复观察最新 settlement:等待过程中旧代被 supersede 时继续追踪新代;最终代失败则原样重抛,成功时返回稳定的 单调代次号。若句柄已开始 dispose,则等待终止清理并返回当时的最终代次。
这最接近 TS fiber.await() / thenable fiber,但 C# 句柄本身不是 awaitable,避免“await 到底观察哪一代”的隐式规则。
重启
public ValueTask RestartAsync();
public void RequestRestart();
RestartAsync() 回收当前 activation,再重新检查 inject:依赖齐全则创建新 fork apply;缺依赖则回到 pending;
上次 apply 失败的句柄也可借此复活。调用会等待这次 reconcile,并传播清理或 apply 失败。
句柄已经 terminal dispose 时,RestartAsync() 与 RequestRestart() 都是 no-op;这与 TS fiber.restart()
在已 dispose 时抛 CordisError('INACTIVE_EFFECT') 不同。
从该句柄自己的清理同步段调用 RestartAsync() 是幂等 no-op;已 yield 的清理若需要请求重启,应使用
RequestRestart(),避免等待由自己阻塞的生命周期。
终止挂载
public ValueTask DisposeAsync();
public void RequestDispose();
DisposeAsync() 把句柄从 activation graph 移除并等待当前 scope 清理。它是 terminal 操作:之后不会因服务重新出现而激活。
重复调用共享同一个结果。RequestDispose() 是不等待版本,专供会形成生命周期环的清理路径。
配置更新差异
TS fiber.update(config, noSave) 在 fiber 上校验新配置,先跑 internal/update waterfall,再重启同一 fiber。
C# 当前没有公开的 PluginHandle.UpdateAsync:
Context.Plugin(plugin, config)在每次 activation 前把同一挂载配置交给IConfigurablePlugin.AcceptConfig;PluginCatalog用配置类型与 DataAnnotations 绑定、校验 YAML;MountedComposition.ReplaceAsync(...)以 stable id 对组合行做事务替换;HotReloadWatcher先编译候选;编译成功后释放旧句柄并尝试挂载候选,候选挂载失败时用旧脚本重建句柄。
因此配置变更的持久化、回滚和 HMR 边界属于 Loader / HMR,而不是核心 Fiber。
错误模型
| TS 错误 | C# 对应 |
|---|---|
CordisError('INACTIVE_EFFECT') | 在 unloading / disposed 层级创建 effect、fork、服务或插件时抛 ObjectDisposedException。 |
| 插件启动失败 | PluginApplyException,Plugin 指向失败插件,CleanupFailed 标记回滚也失败。 |
| apply 与 cleanup 同时失败 | PluginApplyException.InnerException 可为包含两者的 AggregateException。 |
ValidationError | CompositionLoader 的 DataAnnotations 校验失败报告为 CompositionException;直接调用 PluginCatalog.Create 时由 ValidationException 报告。 |
| 多个 disposer 失败 | scope 尽力完成所有清理,再抛 AggregateException。 |
C# 不提供统一的 CordisError.Code 字符串枚举;调用方按具体异常类型和结构处理,同时保留原始 inner exception。