feat(cordis): 完成阶段5访问介导与诊断

This commit is contained in:
2026-08-17 01:09:32 +08:00
parent de5ab449cd
commit 8738e633ee
35 changed files with 1540 additions and 50 deletions
@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
namespace ShrinkContext
@@ -9,7 +10,8 @@ namespace ShrinkContext
public sealed class ShrinkLoaderEntry
{
public ShrinkLoaderEntry(string id, string component, object? config = null, bool disabled = false,
IReadOnlyDictionary<string, string>? isolate = null)
IReadOnlyDictionary<string, string>? isolate = null,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? intercept = null)
{
if (string.IsNullOrEmpty(id))
throw new ArgumentException("Entry id must not be null or empty.", nameof(id));
@@ -21,6 +23,7 @@ namespace ShrinkContext
Config = config;
Disabled = disabled;
Isolate = isolate;
Intercept = intercept;
}
/// <summary>稳定协调键:条目增删与更新的 diff 依据。</summary>
@@ -37,6 +40,9 @@ namespace ShrinkContext
/// <summary>条目级隔离域:键 → realm,叠加到该纤程自身的解析。</summary>
public IReadOnlyDictionary<string, string>? Isolate { get; }
/// <summary>条目级访问元数据:依赖键 → metadata。更新时不改变 target,也不重载 fiber。</summary>
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? Intercept { get; }
}
/// <summary>组件目录:条目的组件名 → 组件工厂。装配期由宿主/CodeGen 注册,加载器不做全域反射扫描。</summary>
@@ -69,23 +75,68 @@ namespace ShrinkContext
component = null!;
return false;
}
public bool Contains(string name) => _factories.ContainsKey(name);
}
/// <summary>加载器配置错误(重复条目 id、未知组件名等)。</summary>
public sealed class ShrinkLoaderException : Exception
public class ShrinkLoaderException : Exception
{
public ShrinkLoaderException(string message) : base(message)
{
}
}
public enum ShrinkLoaderTransactionPhase
{
Idle = 0,
Validating = 1,
Removing = 2,
Applying = 3,
Restoring = 4,
Completed = 5,
Failed = 6,
}
/// <summary>最近一次声明式协调的稳定诊断结果。</summary>
public sealed class ShrinkLoaderTransactionDiagnostic
{
internal ShrinkLoaderTransactionDiagnostic(long generation)
{
Generation = generation;
}
public long Generation { get; }
public ShrinkLoaderTransactionPhase Phase { get; internal set; }
public string? CurrentEntryId { get; internal set; }
public string? ErrorType { get; internal set; }
public string? ErrorMessage { get; internal set; }
public bool RestoreAttempted { get; internal set; }
public bool PreviousCompositionRestored { get; internal set; }
public string? RestoreErrorType { get; internal set; }
public string? RestoreErrorMessage { get; internal set; }
}
public sealed class ShrinkLoaderRestoreException : ShrinkLoaderException
{
public ShrinkLoaderRestoreException(Exception applyError, Exception restoreError)
: base("Loader apply failed and restoring the previous composition also failed.")
{
ApplyError = applyError;
RestoreError = restoreError;
}
public Exception ApplyError { get; }
public Exception RestoreError { get; }
}
/// <summary>
/// 声明式组件加载器(论文 5.2 的原型子集):
/// 编排者把期望组合表达为条目列表,加载器将其增量协调为纤程操作——
/// 条目消失 → 退役;disabled → 卸载;组件/配置变化 → 重建;其余保持不动(幂等)。
///
/// 与完整实现的差异(后续阶段补齐):config 变化当前统一走重建而非组件自决 diff;
/// isolate 变化走重建而非领域原地重分配intercept 尚未支持
/// 与完整实现的差异:config 变化当前统一走重建而非组件自决 diff;
/// isolate 变化走重建而非领域原地重分配intercept metadata 可原地更新,不触发重载
/// </summary>
public sealed class ShrinkContextLoader
{
@@ -95,12 +146,15 @@ namespace ShrinkContext
public object? Config;
public bool Disabled;
public IReadOnlyDictionary<string, string>? Isolate;
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? Intercept;
public ShrinkFiber? Fiber;
}
private readonly ShrinkContextRuntime _runtime;
private readonly ShrinkComponentCatalog _catalog;
private readonly Dictionary<string, ManagedEntry> _entries = new(StringComparer.Ordinal);
private long _transactionGeneration;
private bool _applying;
public ShrinkContextLoader(ShrinkContextRuntime runtime, ShrinkComponentCatalog catalog)
{
@@ -111,6 +165,8 @@ namespace ShrinkContext
/// <summary>当前托管条目 id(含 disabled 但仍被管理的条目)。</summary>
public IEnumerable<string> ManagedEntryIds => _entries.Keys;
public ShrinkLoaderTransactionDiagnostic? LastTransaction { get; private set; }
/// <summary>查询条目当前纤程(可能处于 Inactive——依赖缺失时被响应式停用但仍被管理)。</summary>
public bool TryGetFiber(string entryId, out ShrinkFiber fiber)
{
@@ -132,15 +188,71 @@ namespace ShrinkContext
{
if (desired == null)
throw new ArgumentNullException(nameof(desired));
if (_applying)
throw new InvalidOperationException("A loader composition transaction is already running.");
var previous = CaptureEntries();
var transaction = new ShrinkLoaderTransactionDiagnostic(++_transactionGeneration);
LastTransaction = transaction;
_applying = true;
try
{
await ApplyCoreAsync(desired, transaction, restoring: false);
transaction.Phase = ShrinkLoaderTransactionPhase.Completed;
transaction.CurrentEntryId = null;
}
catch (Exception applyError)
{
transaction.ErrorType = applyError.GetType().FullName;
transaction.ErrorMessage = applyError.Message;
transaction.RestoreAttempted = true;
transaction.Phase = ShrinkLoaderTransactionPhase.Restoring;
transaction.CurrentEntryId = null;
try
{
await ApplyCoreAsync(previous, transaction, restoring: true);
transaction.PreviousCompositionRestored = true;
}
catch (Exception restoreError)
{
transaction.RestoreErrorType = restoreError.GetType().FullName;
transaction.RestoreErrorMessage = restoreError.Message;
transaction.Phase = ShrinkLoaderTransactionPhase.Failed;
transaction.CurrentEntryId = null;
throw new ShrinkLoaderRestoreException(applyError, restoreError);
}
transaction.Phase = ShrinkLoaderTransactionPhase.Failed;
transaction.CurrentEntryId = null;
throw;
}
finally
{
_applying = false;
}
}
private async UniTask ApplyCoreAsync(IReadOnlyList<ShrinkLoaderEntry> desired,
ShrinkLoaderTransactionDiagnostic transaction, bool restoring)
{
transaction.Phase = restoring
? ShrinkLoaderTransactionPhase.Restoring
: ShrinkLoaderTransactionPhase.Validating;
var desiredIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var entry in desired)
{
if (!desiredIds.Add(entry.Id))
throw new ShrinkLoaderException($"Duplicate loader entry id: '{entry.Id}'.");
if (!entry.Disabled && !_catalog.Contains(entry.Component))
throw new ShrinkLoaderException(
$"Unknown component '{entry.Component}' requested by entry '{entry.Id}'.");
}
// 1) 消失的条目退役(其依赖者由响应式通知自动停用,条目本身仍被管理)
if (!restoring)
transaction.Phase = ShrinkLoaderTransactionPhase.Removing;
var removedIds = new List<string>();
foreach (var id in _entries.Keys)
{
@@ -150,6 +262,7 @@ namespace ShrinkContext
foreach (var id in removedIds)
{
transaction.CurrentEntryId = id;
var managed = _entries[id];
if (managed.Fiber != null)
await _runtime.RetireAsync(managed.Fiber);
@@ -157,8 +270,11 @@ namespace ShrinkContext
}
// 2) 按声明顺序逐条分派
if (!restoring)
transaction.Phase = ShrinkLoaderTransactionPhase.Applying;
foreach (var entry in desired)
{
transaction.CurrentEntryId = entry.Id;
_entries.TryGetValue(entry.Id, out var managed);
if (entry.Disabled)
@@ -170,7 +286,8 @@ namespace ShrinkContext
Component = entry.Component,
Config = entry.Config,
Disabled = true,
Isolate = entry.Isolate
Isolate = entry.Isolate,
Intercept = entry.Intercept
};
}
else
@@ -179,6 +296,10 @@ namespace ShrinkContext
await _runtime.RetireAsync(managed.Fiber);
managed.Fiber = null;
managed.Disabled = true;
managed.Component = entry.Component;
managed.Config = entry.Config;
managed.Isolate = entry.Isolate;
managed.Intercept = entry.Intercept;
}
continue;
@@ -189,28 +310,61 @@ namespace ShrinkContext
Equals(managed.Config, entry.Config) &&
IsolateEquals(managed.Isolate, entry.Isolate))
{
if (!InterceptEquals(managed.Intercept, entry.Intercept))
{
managed.Fiber.Ctx.ReplaceIntercept(entry.Intercept);
managed.Intercept = entry.Intercept;
}
continue; // 幂等:无变化不打扰(被响应式停用的纤程保持管理,等待依赖回归)
}
if (managed?.Fiber != null)
{
await _runtime.RetireAsync(managed.Fiber);
managed.Fiber = null;
}
if (!_catalog.TryCreate(entry.Component, out var component))
throw new ShrinkLoaderException(
$"Unknown component '{entry.Component}' requested by entry '{entry.Id}'.");
throw new ShrinkLoaderException($"Component factory '{entry.Component}' returned no component.");
var fiber = _runtime.Use(component, entry.Config, _runtime.RootContext, entry.Isolate);
var fiber = _runtime.Use(component, entry.Config, _runtime.RootContext, entry.Isolate, entry.Intercept);
if (fiber.LastError != null)
{
var failure = fiber.LastError;
await _runtime.RetireAsync(fiber);
throw new ShrinkLoaderException(
$"Component '{entry.Component}' for entry '{entry.Id}' failed during apply: {failure.Message}");
}
_entries[entry.Id] = new ManagedEntry
{
Component = entry.Component,
Config = entry.Config,
Disabled = false,
Isolate = entry.Isolate,
Intercept = entry.Intercept,
Fiber = fiber
};
}
}
private IReadOnlyList<ShrinkLoaderEntry> CaptureEntries()
{
var entries = new List<ShrinkLoaderEntry>(_entries.Count);
foreach (var pair in _entries.OrderBy(item => item.Key, StringComparer.Ordinal))
{
var managed = pair.Value;
entries.Add(new ShrinkLoaderEntry(
pair.Key,
managed.Component,
managed.Config,
managed.Disabled,
managed.Isolate,
managed.Intercept));
}
return entries;
}
private static bool IsolateEquals(IReadOnlyDictionary<string, string>? a, IReadOnlyDictionary<string, string>? b)
{
if (a == null && b == null)
@@ -228,5 +382,31 @@ namespace ShrinkContext
return true;
}
private static bool InterceptEquals(
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? a,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? b)
{
if (a == null && b == null)
return true;
if (a == null || b == null || a.Count != b.Count)
return false;
foreach (var keyPair in a)
{
if (!b.TryGetValue(keyPair.Key, out var otherMetadata) ||
keyPair.Value.Count != otherMetadata.Count)
return false;
foreach (var metadataPair in keyPair.Value)
{
if (!otherMetadata.TryGetValue(metadataPair.Key, out var otherValue) ||
!Equals(metadataPair.Value, otherValue))
return false;
}
}
return true;
}
}
}