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# 逐项对照

TypeScriptC#状态与差异
ctx.effect(execute, label?)ctx.Effect(disposer);ctx.Scope.OnDispose(disposer)近似。C# 直接登记 disposer,不执行一个返回 / yield disposer 的 effect body,也没有 label。
ctx.fiberctx.Scope;挂载方持有 PluginHandle职责拆分。插件内部可见 scope,控制句柄由调用 Plugin() 的一方持有。
fiber.uid无TS-only。C# 不公开 registry 内部 id。
fiber.ctxEffectScope.Context近似。指向当前 scope 的 Context;PluginHandle 不公开 activation Context。
fiber.configPluginHandle.Config近似。C# 是挂载时只读对象,没有句柄级原地 update。
fiber.stateEffectScope.State + PluginHandle.IsActive / IsDisposed职责拆分。没有一套与 TS 状态名完全相同的公开枚举。
fiber.disposePluginHandle.DisposeAsync()对应。终止挂载并等待当前 activation 清理。
fiber.store无公开快照TS-only。依赖通过 activation Context 的 Get<T>() / Require<T>() 读取。
fiber.inertiaPluginHandle.Completion;AwaitSettledAsync()近似。前者观察当前 apply,后者等待最新请求代次稳定。
fiber.namehandle.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?)无句柄级 APITS-only。Loader / HMR 以替换挂载完成配置变更。
EffectAction / Func<ValueTask> disposer近似。C# 不接受 promise / sync iterable / async iterable 的 effect body union。
Disposabledisposer delegate、IDisposable、IAsyncDisposable语言对应。不同注册对象用最适合的 .NET 生命周期接口。
EffectMeta无TS-only。
CordisErrorObjectDisposedException、PluginApplyException 等近似。C# 使用具体异常类型,没有统一 string code。
ValidationErrorDataAnnotations + 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() 的顺序是:

  1. 把当前 scope 及后代标为 Unloading,拒绝新注册;
  2. 取消 Cancellation,等待 activation quiescence;
  3. 按创建逆序回收 child scope;
  4. 顺序派发 dispose,参数为被回收的 Context;
  5. 按登记逆序运行当前 scope 的 disposer;
  6. 把所有独立失败聚合为 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 失败以 PluginApplyException fault。
  • 依赖消失导致的正常取消不把 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。
ValidationErrorCompositionLoader 的 DataAnnotations 校验失败报告为 CompositionException;直接调用 PluginCatalog.Create 时由 ValidationException 报告。
多个 disposer 失败scope 尽力完成所有清理,再抛 AggregateException。

C# 不提供统一的 CordisError.Code 字符串枚举;调用方按具体异常类型和结构处理,同时保留原始 inner exception。

在 GitHub 上编辑此页