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

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;
}
}
}