413 lines
16 KiB
C#
413 lines
16 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
namespace ShrinkContext
|
|
{
|
|
/// <summary>声明式配置条目(论文定义 74 的原型子集):</summary>
|
|
public sealed class ShrinkLoaderEntry
|
|
{
|
|
public ShrinkLoaderEntry(string id, string component, object? config = null, bool disabled = false,
|
|
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));
|
|
if (string.IsNullOrEmpty(component))
|
|
throw new ArgumentException("Entry component must not be null or empty.", nameof(component));
|
|
|
|
Id = id;
|
|
Component = component;
|
|
Config = config;
|
|
Disabled = disabled;
|
|
Isolate = isolate;
|
|
Intercept = intercept;
|
|
}
|
|
|
|
/// <summary>稳定协调键:条目增删与更新的 diff 依据。</summary>
|
|
public string Id { get; }
|
|
|
|
/// <summary>组件目录名(论文条目的 url 字段)。</summary>
|
|
public string Component { get; }
|
|
|
|
/// <summary>绑定给组件 apply 的配置。</summary>
|
|
public object? Config { get; }
|
|
|
|
/// <summary>管理性关闭:true 时卸载对应纤程。</summary>
|
|
public bool Disabled { get; }
|
|
|
|
/// <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>
|
|
public sealed class ShrinkComponentCatalog
|
|
{
|
|
private readonly Dictionary<string, Func<IShrinkComponent>> _factories =
|
|
new(StringComparer.Ordinal);
|
|
|
|
public void Register(string name, Func<IShrinkComponent> factory)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
throw new ArgumentException("Component name must not be null or empty.", nameof(name));
|
|
_factories[name] = factory ?? throw new ArgumentNullException(nameof(factory));
|
|
}
|
|
|
|
public void Register<TComponent>(string name)
|
|
where TComponent : IShrinkComponent, new()
|
|
{
|
|
Register(name, static () => new TComponent());
|
|
}
|
|
|
|
public bool TryCreate(string name, out IShrinkComponent component)
|
|
{
|
|
if (_factories.TryGetValue(name, out var factory))
|
|
{
|
|
component = factory();
|
|
return component != null;
|
|
}
|
|
|
|
component = null!;
|
|
return false;
|
|
}
|
|
|
|
public bool Contains(string name) => _factories.ContainsKey(name);
|
|
}
|
|
|
|
/// <summary>加载器配置错误(重复条目 id、未知组件名等)。</summary>
|
|
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 metadata 可原地更新,不触发重载。
|
|
/// </summary>
|
|
public sealed class ShrinkContextLoader
|
|
{
|
|
private sealed class ManagedEntry
|
|
{
|
|
public string Component = string.Empty;
|
|
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)
|
|
{
|
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
|
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
if (_entries.TryGetValue(entryId, out var managed) && managed.Fiber != null)
|
|
{
|
|
fiber = managed.Fiber;
|
|
return true;
|
|
}
|
|
|
|
fiber = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 增量应用期望配置:不整体重建,只对差异部分执行最小破坏性操作。
|
|
/// 协调顺序确定性:先移除消失条目,再按声明顺序处理其余条目。
|
|
/// </summary>
|
|
public async UniTask ApplyAsync(IReadOnlyList<ShrinkLoaderEntry> desired)
|
|
{
|
|
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)
|
|
{
|
|
if (!desiredIds.Contains(id))
|
|
removedIds.Add(id);
|
|
}
|
|
|
|
foreach (var id in removedIds)
|
|
{
|
|
transaction.CurrentEntryId = id;
|
|
var managed = _entries[id];
|
|
if (managed.Fiber != null)
|
|
await _runtime.RetireAsync(managed.Fiber);
|
|
_entries.Remove(id);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
if (managed == null)
|
|
{
|
|
_entries[entry.Id] = new ManagedEntry
|
|
{
|
|
Component = entry.Component,
|
|
Config = entry.Config,
|
|
Disabled = true,
|
|
Isolate = entry.Isolate,
|
|
Intercept = entry.Intercept
|
|
};
|
|
}
|
|
else
|
|
{
|
|
if (managed.Fiber != null)
|
|
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;
|
|
}
|
|
|
|
if (managed != null && !managed.Disabled && managed.Fiber != null &&
|
|
managed.Component == entry.Component &&
|
|
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($"Component factory '{entry.Component}' returned no component.");
|
|
|
|
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)
|
|
return true;
|
|
if (a == null || b == null)
|
|
return false;
|
|
if (a.Count != b.Count)
|
|
return false;
|
|
|
|
foreach (var pair in a)
|
|
{
|
|
if (!b.TryGetValue(pair.Key, out var value) || value != pair.Value)
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|