feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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>组件目录:条目的组件名 → 组件工厂。装配期由宿主/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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>加载器配置错误(重复条目 id、未知组件名等)。</summary>
|
||||
public sealed class ShrinkLoaderException : Exception
|
||||
{
|
||||
public ShrinkLoaderException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 声明式组件加载器(论文 5.2 的原型子集):
|
||||
/// 编排者把期望组合表达为条目列表,加载器将其增量协调为纤程操作——
|
||||
/// 条目消失 → 退役;disabled → 卸载;组件/配置变化 → 重建;其余保持不动(幂等)。
|
||||
///
|
||||
/// 与完整实现的差异(后续阶段补齐):config 变化当前统一走重建而非组件自决 diff;
|
||||
/// isolate 变化走重建而非领域原地重分配;intercept 尚未支持。
|
||||
/// </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 ShrinkFiber? Fiber;
|
||||
}
|
||||
|
||||
private readonly ShrinkContextRuntime _runtime;
|
||||
private readonly ShrinkComponentCatalog _catalog;
|
||||
private readonly Dictionary<string, ManagedEntry> _entries = new(StringComparer.Ordinal);
|
||||
|
||||
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;
|
||||
|
||||
/// <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));
|
||||
|
||||
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}'.");
|
||||
}
|
||||
|
||||
// 1) 消失的条目退役(其依赖者由响应式通知自动停用,条目本身仍被管理)
|
||||
var removedIds = new List<string>();
|
||||
foreach (var id in _entries.Keys)
|
||||
{
|
||||
if (!desiredIds.Contains(id))
|
||||
removedIds.Add(id);
|
||||
}
|
||||
|
||||
foreach (var id in removedIds)
|
||||
{
|
||||
var managed = _entries[id];
|
||||
if (managed.Fiber != null)
|
||||
await _runtime.RetireAsync(managed.Fiber);
|
||||
_entries.Remove(id);
|
||||
}
|
||||
|
||||
// 2) 按声明顺序逐条分派
|
||||
foreach (var entry in desired)
|
||||
{
|
||||
_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
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (managed.Fiber != null)
|
||||
await _runtime.RetireAsync(managed.Fiber);
|
||||
managed.Fiber = null;
|
||||
managed.Disabled = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (managed != null && !managed.Disabled && managed.Fiber != null &&
|
||||
managed.Component == entry.Component &&
|
||||
Equals(managed.Config, entry.Config) &&
|
||||
IsolateEquals(managed.Isolate, entry.Isolate))
|
||||
{
|
||||
continue; // 幂等:无变化不打扰(被响应式停用的纤程保持管理,等待依赖回归)
|
||||
}
|
||||
|
||||
if (managed?.Fiber != null)
|
||||
await _runtime.RetireAsync(managed.Fiber);
|
||||
|
||||
if (!_catalog.TryCreate(entry.Component, out var component))
|
||||
throw new ShrinkLoaderException(
|
||||
$"Unknown component '{entry.Component}' requested by entry '{entry.Id}'.");
|
||||
|
||||
var fiber = _runtime.Use(component, entry.Config, _runtime.RootContext, entry.Isolate);
|
||||
_entries[entry.Id] = new ManagedEntry
|
||||
{
|
||||
Component = entry.Component,
|
||||
Config = entry.Config,
|
||||
Disabled = false,
|
||||
Isolate = entry.Isolate,
|
||||
Fiber = fiber
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c236b8aa6fec940449f9303b855a3989
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user