Files
cneicy 6eebb8e8e4
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:49:58 +08:00

529 lines
20 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
namespace ShrinkContext
{
/// <summary>
/// Cordis 核心原型运行时:纤程注册表 + 共享余效应存储 + 惯性生命周期状态机
/// (论文算法 2-5 的 C#/UniTask 移植)。
///
/// 约定:
/// - 仅在主线程使用;
/// - 异步转换在同步可完成路径上内联跑完(无需 PlayerLoop 泵即可确定性测试);
/// - 目标(Target)按 inject 声明顺序记录 provider uid 视图,uid 永不复用,
/// 因此“同值不同提供者”的替换及多键 provider 映射变化都能触发重载。
/// </summary>
public sealed class ShrinkContextRuntime
{
private readonly ShrinkCtx _rootCtx;
private readonly List<ShrinkFiber> _fibers = new();
private readonly Dictionary<string, ShrinkBinding> _store = new();
private readonly Dictionary<string, HashSet<ShrinkFiber>> _injectIndex =
new(StringComparer.Ordinal);
private long _uidCounter;
private long _notificationDispatchCount;
private long _notificationCandidateVisitCount;
private int _lastNotificationCandidateCount;
public ShrinkContextRuntime()
{
_rootCtx = new ShrinkCtx(this, null, null, null);
}
public ShrinkCtx RootContext => _rootCtx;
public IReadOnlyList<ShrinkFiber> Fibers => _fibers;
// ---------------- 实例化(论文算法 4 ----------------
/// <summary>
/// 实例化组件为纤程:在父上下文(默认根上下文)上注册一个可逆效应,
/// 前向 = 启动子纤程生命周期,逆 = 强制子纤程目标为 ⊥ 并卸载——父卸载自动级联子卸载。
/// isolate 为该纤程自身的键解析叠加隔离域(不影响父上下文)。
/// </summary>
public ShrinkFiber Use(IShrinkComponent component, object? config = null, ShrinkCtx? parentCtx = null,
IReadOnlyDictionary<string, string>? isolate = null,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? intercept = null)
{
if (component == null)
throw new ArgumentNullException(nameof(component));
var parent = parentCtx ?? _rootCtx;
var fiber = new ShrinkFiber(++_uidCounter, component, parent.OwnerFiber, config);
var fiberCtx = parent.CreateChildForFiber(fiber);
if (isolate != null && isolate.Count > 0)
fiberCtx = fiberCtx.WithIsolate(isolate);
if (intercept != null && intercept.Count > 0)
fiberCtx = fiberCtx.WithIntercept(intercept);
fiber.Ctx = fiberCtx;
_fibers.Add(fiber);
IndexFiber(fiber);
var instantiationHandle = new ShrinkEffectHandle();
instantiationHandle.AttachInverse(() => RetireCoreAsync(fiber));
parent.Attach(instantiationHandle);
Refresh(fiber);
return fiber;
}
/// <summary>
/// 退役并卸载纤程(论文 O-Retire):目标强制 ⊥,等待纤程到达静止态。
/// 退役是永久的:之后不再参与依赖通知。
/// </summary>
public async UniTask RetireAsync(ShrinkFiber fiber)
{
if (fiber.Retired)
return;
UnindexFiber(fiber);
if (fiber.State == ShrinkFiberState.Inactive && !fiber.InTransition)
{
fiber.Retired = true;
return;
}
fiber.Retired = true;
fiber.Target = null;
if (!fiber.InTransition)
{
fiber.InTransition = true;
fiber.Inertia = RunTransitionAsync(fiber);
}
if (fiber.InTransition)
await fiber.Inertia;
}
/// <summary>卸载全部纤程并回滚根上下文效应。</summary>
public async UniTask ShutdownAsync()
{
foreach (var fiber in _fibers.ToArray())
await RetireAsync(fiber);
await _rootCtx.DisposeAsync();
}
// ---------------- 生命周期(论文算法 5 ----------------
/// <summary>
/// 重算目标:任一依赖键不可解析 → ⊥;否则为按 inject 键顺序记录的 provider uid 视图。
/// 目标变化即启动(或在惯性边界链式触发)reload / unload 转换。
/// </summary>
internal bool Refresh(ShrinkFiber fiber)
{
if (fiber.Retired)
return false;
var target = ComputeTarget(fiber);
if (TargetEquals(fiber.Target, target))
return false;
fiber.Target = target;
if (!fiber.InTransition)
{
fiber.InTransition = true;
fiber.Inertia = RunTransitionAsync(fiber);
}
return true;
}
private async UniTask RunTransitionAsync(ShrinkFiber fiber)
{
try
{
while (true)
{
if (fiber.Target is null)
{
// L-Leave:先停止供给(状态置 Unloading),再调度任何逆操作
fiber.State = ShrinkFiberState.Unloading;
await UnloadCoreAsync(fiber);
if (fiber.Target is null)
{
fiber.State = ShrinkFiberState.Inactive;
return;
}
// 卸载中途目标恢复 → 链式重载(惯性)
continue;
}
fiber.State = ShrinkFiberState.Loading;
var stable = await ReloadCoreAsync(fiber);
if (stable)
{
fiber.State = ShrinkFiberState.Active;
fiber.EverActive = true;
fiber.LastError = null;
Notify(fiber.Ctx, fiber.Component.Provide);
return;
}
// 装载中途目标变化(⊥ 或换提供者)→ 链式卸载(惯性)
}
}
finally
{
fiber.InTransition = false;
}
}
private async UniTask<bool> ReloadCoreAsync(ShrinkFiber fiber)
{
var target0 = fiber.Target!;
// 提交视图:装载期间(含自身卸载过程)读取同一份绑定(定理 63)
fiber._committed = ResolveView(fiber);
try
{
if (fiber.Component is IShrinkIterativeComponent iterative)
{
var stepper = iterative.ApplySteps(fiber.Ctx, fiber.Config);
// 守卫在每步步进边界检查目标稳定性(算法 1 的 guard 循环)
while (TargetEquals(fiber.Target, target0))
{
if (!await stepper.MoveNextAsync())
break;
var handle = new ShrinkEffectHandle();
handle.AttachInverse(stepper.Current);
fiber.Ctx.Attach(handle);
}
}
else
{
await fiber.Component.ApplyAsync(fiber.Ctx, fiber.Config);
}
}
catch (Exception ex)
{
// L-Raise:记录错误并强制走卸载路径,回收已执行的部分效应
fiber.LastError = ex;
fiber.Target = null;
return false;
}
return TargetEquals(fiber.Target, target0);
}
private async UniTask UnloadCoreAsync(ShrinkFiber fiber)
{
// 先通知依赖者并等待它们到达静止态(drain),再运行自身的逆操作(L-Unload 守卫)
var affected = Notify(fiber.Ctx, fiber.Component.Provide);
foreach (var dependent in affected)
{
if (dependent.InTransition)
await dependent.Inertia;
}
await fiber.Ctx.DisposeAsync();
// 卸载完成后才丢弃提交视图,保证自身拆除期间仍可读取依赖
fiber._committed = null;
}
// ---------------- 通知(论文算法 3 ----------------
/// <summary>
/// 将键变更传播给声明了该键、且 realm 解析一致的依赖纤程;
/// 返回本次被触发刷新的纤程(供卸载方等待)。
/// </summary>
internal IReadOnlyList<ShrinkFiber> Notify(ShrinkCtx sourceCtx, IReadOnlyCollection<string> keys)
{
var affected = new List<ShrinkFiber>();
if (keys.Count == 0)
return affected;
var candidates = new HashSet<ShrinkFiber>();
foreach (var key in keys)
{
if (_injectIndex.TryGetValue(key, out var indexed))
candidates.UnionWith(indexed);
}
_notificationDispatchCount++;
_lastNotificationCandidateCount = candidates.Count;
_notificationCandidateVisitCount += candidates.Count;
foreach (var fiber in candidates.OrderBy(candidate => candidate.Uid))
{
if (fiber.Retired)
continue;
var realmMatches = false;
foreach (var key in keys)
{
if (!fiber.InjectSet.Contains(key))
continue;
if (fiber.Ctx.ResolveRealm(key) != sourceCtx.ResolveRealm(key))
continue;
realmMatches = true;
break;
}
if (realmMatches && Refresh(fiber))
affected.Add(fiber);
}
return affected;
}
// ---------------- 余效应存储(论文算法 2 ----------------
internal ShrinkEffectHandle SetBinding(ShrinkCtx ctx, string key, object? value,
IShrinkCoeffectAccessPolicy? accessPolicy)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Key must not be null or empty.", nameof(key));
var realm = ctx.ResolveRealm(key);
var setter = ctx.OwnerFiber;
if (setter != null && !setter.ProvideSet.Contains(key))
throw new ShrinkUndeclaredSupplyException(key, setter.Name);
InstallBinding(realm, key, value, setter, ctx, accessPolicy);
var handle = new ShrinkEffectHandle();
handle.AttachInverse(() =>
{
RemoveBinding(realm, key, setter, ctx);
return UniTask.CompletedTask;
});
ctx.Attach(handle);
return handle;
}
private void InstallBinding(string realm, string key, object? value, ShrinkFiber? setter, ShrinkCtx ctx,
IShrinkCoeffectAccessPolicy? accessPolicy)
{
if (_store.TryGetValue(realm, out var existing) && !IsTakeoverAllowed(existing, setter))
throw new ShrinkSupplyConflictException(key, existing.Provider, setter);
_store[realm] = new ShrinkBinding(value, setter, key, accessPolicy);
Notify(ctx, new[] { key });
}
private void RemoveBinding(string realm, string key, ShrinkFiber? setter, ShrinkCtx ctx)
{
// 只撤回仍归属本设置者的绑定,不误删后继提供者
if (_store.TryGetValue(realm, out var current) && current.Provider == setter)
_store.Remove(realm);
Notify(ctx, new[] { key });
}
private static bool IsTakeoverAllowed(ShrinkBinding existing, ShrinkFiber? setter)
{
if (existing.Provider == null || existing.Provider == setter)
return true;
var state = existing.Provider.State;
return state == ShrinkFiberState.Unloading || state == ShrinkFiberState.Inactive;
}
/// <summary>解析绑定:realm 不存在、或提供者非 Active(正在装载/卸载/失活)时视为不可用。</summary>
internal ShrinkBinding? ResolveBinding(ShrinkCtx ctx, string key)
{
var realm = ctx.ResolveRealm(key);
if (!_store.TryGetValue(realm, out var binding))
return null;
if (binding.Provider != null && binding.Provider.State != ShrinkFiberState.Active)
return null;
return binding;
}
/// <summary>绕过介导规则的原始查找(编排层/测试用):active 提供者或无主绑定才命中。</summary>
public bool TryGetRaw<T>(ShrinkCtx ctx, string key, out T value)
{
value = default!;
var binding = ResolveBinding(ctx, key);
if (binding == null)
return false;
value = (T)binding.Value!;
return true;
}
public bool TryGetRaw<T>(ShrinkCtx ctx, ShrinkKey<T> key, out T value) => TryGetRaw(
ctx, (key ?? throw new ArgumentNullException(nameof(key))).Id, out value);
/// <summary>执行 provider 绑定的访问策略;无策略时直接返回原始值。</summary>
internal T ResolveAccess<T>(ShrinkCtx ctx, string key, ShrinkBinding binding)
{
object? value = binding.Value;
if (binding.AccessPolicy != null)
{
var access = new ShrinkCoeffectAccessContext(
key,
typeof(T),
ctx.OwnerFiber,
ctx.ResolveInterceptMetadata(key));
value = binding.AccessPolicy.Resolve(access, value);
}
if (value is T typed)
return typed;
if (value == null && (!typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) != null))
return default!;
throw new InvalidCastException(
$"Coeffect '{key}' resolved value of type '{value?.GetType().FullName ?? "<null>"}' " +
$"but consumer requested '{typeof(T).FullName}'.");
}
// ---------------- 目标计算 ----------------
private long[]? ComputeTarget(ShrinkFiber fiber)
{
var uids = new List<long>();
foreach (var key in fiber.Component.Inject)
{
var binding = ResolveBinding(fiber.Ctx, key);
if (binding == null)
return null;
uids.Add(binding.Provider?.Uid ?? 0);
}
// target 是 key -> provider uid 的视图,不是 uid 集合。保留 inject 声明顺序与重复 uid,
// 才能观察两个键在相同提供者集合之间互换的变化。
return uids.ToArray();
}
private Dictionary<string, ShrinkBinding> ResolveView(ShrinkFiber fiber)
{
var view = new Dictionary<string, ShrinkBinding>();
foreach (var key in fiber.Component.Inject)
{
var binding = ResolveBinding(fiber.Ctx, key);
if (binding != null)
view[key] = binding;
}
return view;
}
private static bool TargetEquals(long[]? a, long[]? b)
{
if (a == null || b == null)
return a == null && b == null;
if (a.Length != b.Length)
return false;
for (var i = 0; i < a.Length; i++)
{
if (a[i] != b[i])
return false;
}
return true;
}
private async UniTask RetireCoreAsync(ShrinkFiber fiber)
{
if (fiber.Retired)
return;
await RetireAsync(fiber);
}
private void IndexFiber(ShrinkFiber fiber)
{
foreach (var key in fiber.InjectSet)
{
if (!_injectIndex.TryGetValue(key, out var fibers))
{
fibers = new HashSet<ShrinkFiber>();
_injectIndex.Add(key, fibers);
}
fibers.Add(fiber);
}
}
private void UnindexFiber(ShrinkFiber fiber)
{
foreach (var key in fiber.InjectSet)
{
if (!_injectIndex.TryGetValue(key, out var fibers))
continue;
fibers.Remove(fiber);
if (fibers.Count == 0)
_injectIndex.Remove(key);
}
}
/// <summary>捕获不含可变运行时引用的诊断快照。</summary>
public ShrinkContextRuntimeDiagnostic CaptureDiagnostic()
{
var diagnostics = new List<ShrinkFiberDiagnostic>(_fibers.Count);
foreach (var fiber in _fibers.OrderBy(item => item.Uid))
{
var dependencies = new List<ShrinkDependencyDiagnostic>();
foreach (var key in fiber.Component.Inject ?? Array.Empty<string>())
{
var realm = fiber.Ctx.ResolveRealm(key);
var current = ResolveBinding(fiber.Ctx, key);
ShrinkBinding? committed = null;
fiber.Committed?.TryGetValue(key, out committed);
var potential = _fibers
.Where(candidate => !candidate.Retired && candidate.ProvideSet.Contains(key) &&
candidate.Ctx.ResolveRealm(key) == realm)
.Select(candidate => candidate.Uid)
.OrderBy(uid => uid)
.ToArray();
dependencies.Add(new ShrinkDependencyDiagnostic(
key,
realm,
current?.Provider?.Uid,
committed?.Provider?.Uid,
potential));
}
diagnostics.Add(new ShrinkFiberDiagnostic(
fiber.Uid,
fiber.Name,
GetDiagnosticStatus(fiber),
fiber.State,
fiber.Retired,
fiber.InTransition,
(fiber.Component.Inject ?? Array.Empty<string>()).ToArray(),
(fiber.Component.Provide ?? Array.Empty<string>()).ToArray(),
fiber.Target?.ToArray() ?? Array.Empty<long>(),
dependencies,
fiber.Ctx.GetLocalInterceptKeys().OrderBy(key => key, StringComparer.Ordinal).ToArray(),
fiber.LastError?.GetType().FullName,
fiber.LastError?.Message));
}
return new ShrinkContextRuntimeDiagnostic(
diagnostics,
_store.Count,
new ShrinkNotificationDiagnostic(
_notificationDispatchCount,
_notificationCandidateVisitCount,
_lastNotificationCandidateCount,
_injectIndex.Count));
}
private static ShrinkFiberDiagnosticStatus GetDiagnosticStatus(ShrinkFiber fiber)
{
if (fiber.Retired)
return ShrinkFiberDiagnosticStatus.Retired;
if (fiber.LastError != null)
return ShrinkFiberDiagnosticStatus.Failed;
return fiber.State switch
{
ShrinkFiberState.Loading => ShrinkFiberDiagnosticStatus.Loading,
ShrinkFiberState.Active => ShrinkFiberDiagnosticStatus.Active,
ShrinkFiberState.Unloading => ShrinkFiberDiagnosticStatus.Unloading,
ShrinkFiberState.Inactive when fiber.InjectSet.Count > 0 => ShrinkFiberDiagnosticStatus.Waiting,
_ => ShrinkFiberDiagnosticStatus.Inactive,
};
}
}
}