chore: initialize standalone UPM package
Publish UPM package / publish (push) Failing after 1s

This commit is contained in:
2026-08-26 02:49:58 +08:00
commit 6eebb8e8e4
50 changed files with 3458 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
name: Publish UPM package
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
steps:
- name: Fetch tagged revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
test -n "$ref"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkContext.Core.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Validate immutable release version
shell: bash
run: |
set -eu
tag="$(git describe --exact-match --tags HEAD)"
version="$(node -p "require('./package.json').version")"
test "$tag" = "v$version"
npm pack --dry-run
- name: Publish to ShrinkSDK registry
shell: bash
run: |
set -eu
: "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
npmrc="$HOME/.npmrc"
cleanup() { rm -f "$npmrc"; }
trap cleanup EXIT
printf '%s\n' \
'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \
'//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc"
npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/
+39
View File
@@ -0,0 +1,39 @@
name: Verify standalone Unity package
on:
workflow_dispatch:
jobs:
editmode:
runs-on: unity-2022.3.62f3
container:
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
volumes:
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
steps:
- name: Fetch selected revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/ShrinkContext.Core.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Run package EditMode tests
shell: bash
run: |
set -eu
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin"
"$unity_bin" \
-batchmode \
-nographics \
-quit \
-projectPath "$PWD/Development~/UnityProject" \
-runTests \
-testPlatform EditMode \
-testResults "$PWD/TestResults/editmode.xml" \
-logFile "$PWD/TestResults/unity.log"
+10
View File
@@ -0,0 +1,10 @@
/Development~/UnityProject/[Ll]ibrary/
/Development~/UnityProject/[Tt]emp/
/Development~/UnityProject/[Oo]bj/
/Development~/UnityProject/[Ll]ogs/
/Development~/UnityProject/[Uu]ser[Ss]ettings/
/Development~/UnityProject/TestResults/
/Tools~/**/[Bb]in/
/Tools~/**/[Oo]bj/
*.user
*.DotSettings.user
+8
View File
@@ -0,0 +1,8 @@
.git/
.gitea/
Development~/
Tools~/
*.csproj
*.sln
*.user
*.DotSettings.user
+6
View File
@@ -0,0 +1,6 @@
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Ll]ogs/
[Uu]ser[Ss]ettings/
TestResults/
@@ -0,0 +1,15 @@
{
"scopedRegistries": [
{
"name": "ShrinkSDK",
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
"scopes": [
"com.cneicy"
]
}
],
"dependencies": {
"com.unity.test-framework": "1.1.33",
"com.cneicy.shrink-context-core": "file:../../.."
}
}
@@ -0,0 +1,2 @@
m_EditorVersion: 2022.3.62f3
m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7)
+69
View File
@@ -0,0 +1,69 @@
# ShrinkContext.Core
《Cordis: A Programming Paradigm for Spatiotemporal Composability》核心机制的 Unity/C# 实现。当前架构与边界见仓库根 `DESIGN.md`;已完成的迁移过程归档于 `Docs/Archive/CORDIS_MIGRATION.completed.md`
## 定位
时空可组合性的**元框架核心库**:不规定任何领域,只提供动态组合的两个通用原语——
- **可逆效应**(时间可组合性,论文 3.1 / 算法 1):`ctx.Effect / EffectAsync`,前向执行时产出逆操作,运行时按 LIFO 累积,卸载即回滚;
- **响应式余效应**(空间可组合性,论文 3.2 / 算法 2、3):`ctx.Set / Get`,依赖键的安装/撤回自动通知依赖者,组件在依赖全部可用时激活、失效时停用、换提供者时重载;
- **纤程生命周期**(论文 4 / 算法 4、5):`runtime.Use(component)` 实例化组件为纤程,惯性状态机 `Loading → Active → Unloading → Inactive`,卸载先等待依赖者排空再回滚自身效应,父卸载级联子卸载。
- **声明式协调与访问介导**:加载器以事务更新期望组合,失败时恢复旧组合;`isolate / intercept` 控制解析域与服务视图,组件供给受 `Provide` 声明约束。
## 目录结构
| 目录 | 说明 |
|---|---|
| `Runtime/Context/` | `ShrinkCtx`(一等上下文、隔离派生、介导访问)与 `ShrinkEffectHandle`(算法 1 |
| `Runtime/Coeffects/` | 绑定、强类型版本键、访问策略与异常类型 |
| `Runtime/Fibers/` | `IShrinkComponent`inject/provide/apply)、`ShrinkFiber`、分步效应接口 |
| `Runtime/Loader/` | 声明式加载器:`ShrinkLoaderEntry` / `ShrinkComponentCatalog` / `ShrinkContextLoader` 增量协调、事务恢复与条目诊断 |
| `Runtime/` | `ShrinkContextRuntime`(注册表 + 算法 2-5 + notify 倒排索引)、诊断快照与 Domain Reload 重置 |
| `Tests/` | EditMode 测试:对照论文定理 7/16/20/63/64 与算法 1/2/3/4/5/6 的可观察行为 + 加载器协调行为 |
## 论文 → API 对照
| 论文概念 | 本实现 |
|---|---|
| `ctx.effect(callback)` → dispose | `ShrinkCtx.Effect / EffectAsync``ShrinkEffectHandle.DisposeAsync`(armed 单次执行,等待进行中前向) |
| `ctx.get/set`(算法 2 | `ShrinkCtx.Set<T>`(可逆,安装+通知 / 撤回+通知) |
| `ctx.isolate(key, realm)` | `ShrinkCtx.Isolate`(派生子上下文,丢弃即恢复) |
| `ctx.intercept(key, metadata)` | `ShrinkCtx.Intercept`(派生访问元数据;provider 通过 `IShrinkCoeffectAccessPolicy` 返回受限视图或拒绝访问) |
| 代理介导访问(算法 6 | `ShrinkCtx.Get<T>/TryGet`(沿纤程链解析已提交视图;未声明/未激活/策略拒绝分别抛出不同异常) |
| `ctx.use(component)`(算法 4 | `ShrinkContextRuntime.Use`(实例化是父上下文的可逆效应 → 级联卸载;支持条目级 isolate 叠加) |
| 声明式配置 + 协调(论文 5.2) | `ShrinkContextLoader.ApplyAsync`(预校验期望组合;条目消失→退役;组件/配置变化→重建;intercept 变化→原位更新;失败→恢复旧组合) |
| refresh/reload/unload(算法 5 | `RunTransitionAsync`(惯性链式转换;卸载先 drain 依赖者) |
| notify(算法 3 | `ShrinkContextRuntime.Notify``key → inject fibers` 倒排索引选候选,再按 realm 过滤) |
| target = 按 inject 顺序解析的提供者 uid | `ShrinkFiber.Target`(保留键顺序与重复依赖;uid 永不复用,因此同值换提供者也会重载) |
| `𝔈iterΓ` 分步效应 | `IShrinkIterativeComponent.ApplySteps`(自定义 UniTask 步进器,步进边界守卫 → 部分回滚;不用 IAsyncEnumerable,规避 Mono ValueTask 迭代器的非内联调度) |
| fiber.committed 提交视图 | `ShrinkFiber.Committed`(装载前提交,拆除完才丢弃) |
## 测试覆盖(对照论文元理论)
- 定理 7(恢复初始上下文)/ 定理 16(LIFO)/ 选择性撤销 / armed 幂等 / dispose 等待进行中前向
- set 可逆性、介导访问纪律(UNDECLARED / INACTIVE)、隔离域派生与丢弃恢复
- 未声明供给回滚、访问策略拒绝、intercept 原位更新与只读服务视图
- 依赖驱动激活、缺依赖静默非活动、晚到提供者激活
- 退役次序:依赖者先于提供者逆操作排空(drain-before-inverse
- 提供者替换重载(含同值替换,uid 语义)
- 父子级联、供给冲突、循环依赖静止(论文 6.5)
- 惯性:装载中途退役永不 Active、链式卸载(论文 4.3.3
- 分步效应部分回滚(定理 64
- apply 失败的部分效应回滚(L-Raise)
- 加载器失败恢复旧组合、跨 realm 替换隔离、notify 倒排索引候选计数与运行时诊断
- Domain Reload 重置
## 当前边界
- `intercept` 是经 `ShrinkCtx.Get` 的能力介导,不是对不可信 DLL 的内存、反射、文件或网络沙箱。
- loader 的 isolate 变化仍通过重建条目生效;尚未提供运行中 fiber 的原位 realm 迁移。
- 强类型版本键是新增契约,现有字符串键不会在阶段 5 被一次性重写。
- Core 保持不依赖具体配置资产;`ShrinkContext.AppAdapter` 提供 ScriptableObject/JSON 组合、Editor 诊断窗和可重复容量基准。
- 主线程宿主与 Unity PlayerLoop 调度仍由上层应用负责。
- 当前上下文只协调单进程内组件;跨进程/独立服务器使用 Network 合同,不共享 fiber 或事务。
## 约定
- 仅主线程使用;异步转换在同步可完成路径上内联跑完(测试无需 PlayerLoop 泵即可确定性断言)。
- 测试中的 `TestAwait.Run` 同步阻塞等待,仅用于保证延续同步完成的测试代码。
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1d4324f03a9bc5241a5021b5ae5bfd06
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba47b2c9b35184a4c89870601cdaa5e8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 40e4fc95f69522e46b43fbb4dade4bb9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+154
View File
@@ -0,0 +1,154 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace ShrinkContext
{
/// <summary>共享存储中的一条依赖绑定:键解析到 realm 后对应的值与提供者纤程。</summary>
public sealed class ShrinkBinding
{
public ShrinkBinding(object? value, ShrinkFiber? provider, string key,
IShrinkCoeffectAccessPolicy? accessPolicy = null)
{
Value = value;
Provider = provider;
Key = key;
AccessPolicy = accessPolicy;
}
public object? Value { get; }
public ShrinkFiber? Provider { get; }
public string Key { get; }
public IShrinkCoeffectAccessPolicy? AccessPolicy { get; }
}
/// <summary>
/// 带命名空间与主版本的类型化余效应键。现有字符串声明可继续使用 <see cref="Id"/>
/// Set/Get 重载则在调用点固定值类型,逐步收口字符串键的碰撞与接口漂移。
/// </summary>
public sealed class ShrinkKey<T>
{
public ShrinkKey(string packageName, string name, int majorVersion = 1)
{
if (string.IsNullOrWhiteSpace(packageName))
throw new ArgumentException("Package name must not be empty.", nameof(packageName));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Key name must not be empty.", nameof(name));
if (majorVersion <= 0)
throw new ArgumentOutOfRangeException(nameof(majorVersion), "Major version must be positive.");
PackageName = packageName.Trim();
Name = name.Trim();
MajorVersion = majorVersion;
Id = $"{PackageName}/{Name}@v{MajorVersion}";
}
public string PackageName { get; }
public string Name { get; }
public int MajorVersion { get; }
public string Id { get; }
public override string ToString() => Id;
}
/// <summary>依赖值被读取时的访问上下文;metadata 来自访问方上下文上的 intercept 合并结果。</summary>
public sealed class ShrinkCoeffectAccessContext
{
internal ShrinkCoeffectAccessContext(string key, Type requestedType, ShrinkFiber? consumer,
IReadOnlyDictionary<string, object?> metadata)
{
Key = key;
RequestedType = requestedType;
Consumer = consumer;
Metadata = metadata;
}
public string Key { get; }
public Type RequestedType { get; }
public ShrinkFiber? Consumer { get; }
public IReadOnlyDictionary<string, object?> Metadata { get; }
public bool TryGetMetadata<T>(string name, out T value)
{
if (Metadata.TryGetValue(name, out var raw) && raw is T typed)
{
value = typed;
return true;
}
value = default!;
return false;
}
}
/// <summary>
/// provider 侧访问策略。它可以依据 intercept metadata 返回降权包装或拒绝访问,
/// 但不会改变依赖满足关系,也不会构成不可信代码沙箱。
/// </summary>
public interface IShrinkCoeffectAccessPolicy
{
object? Resolve(ShrinkCoeffectAccessContext context, object? value);
}
/// <summary>访问了纤程链上没有任何一方声明的依赖键(论文算法 6 的 UNDECLARED_ACCESS)。</summary>
public sealed class ShrinkUndeclaredAccessException : Exception
{
public ShrinkUndeclaredAccessException(string key)
: base($"Undeclared coeffect access: '{key}'. Declare it in the component's Inject list.")
{
Key = key;
}
public string Key { get; }
}
/// <summary>纤程声明了依赖键但尚未激活就访问(论文算法 6 的 INACTIVE_ACCESS)。</summary>
public sealed class ShrinkInactiveAccessException : Exception
{
public ShrinkInactiveAccessException(string key, string fiberName)
: base($"Inactive coeffect access: '{key}' declared by fiber '{fiberName}' but not committed yet.")
{
Key = key;
}
public string Key { get; }
}
/// <summary>组件写入了未在 Provide 中声明的键。</summary>
public sealed class ShrinkUndeclaredSupplyException : Exception
{
public ShrinkUndeclaredSupplyException(string key, string fiberName)
: base($"Undeclared coeffect supply: '{key}' is not listed in fiber '{fiberName}' Provide keys.")
{
Key = key;
FiberName = fiberName;
}
public string Key { get; }
public string FiberName { get; }
}
/// <summary>intercept 访问策略拒绝了本次依赖读取。</summary>
public sealed class ShrinkCoeffectAccessDeniedException : Exception
{
public ShrinkCoeffectAccessDeniedException(string key, string message)
: base($"Coeffect access denied for '{key}': {message}")
{
Key = key;
}
public string Key { get; }
}
/// <summary>两个活跃纤程试图供给同一个 realm(论文定义 45 的供给不相交约束)。</summary>
public sealed class ShrinkSupplyConflictException : Exception
{
public ShrinkSupplyConflictException(string key, ShrinkFiber? existing, ShrinkFiber? setter)
: base($"Supply conflict on key '{key}': existing provider " +
$"'{existing?.Name ?? "<ambient>"}' conflicts with '{setter?.Name ?? "<ambient>"}'.")
{
Key = key;
}
public string Key { get; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2dcad9bdadb1607498dc233cfc1b5a06
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4906e85b1734b944ca870fa011dd3a98
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+300
View File
@@ -0,0 +1,300 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace ShrinkContext
{
/// <summary>
/// 一等上下文(论文 Γ∞ 的运行时对应物)。
/// - 树结构:由 <see cref="Isolate"/> 派生隔离子上下文,或由运行时为纤程派生子上下文;
/// - 效应:本上下文注册的可逆效应按注册逆序(LIFO)在 <see cref="DisposeAsync"/> 中回滚;
/// - 余效应:Set/Get 委托给运行时共享存储,键先经隔离表解析为 realm 再查表。
/// </summary>
public sealed class ShrinkCtx
{
private readonly List<ShrinkEffectHandle> _effects = new();
private readonly Dictionary<string, string>? _isolateOverlay;
private Dictionary<string, IReadOnlyDictionary<string, object?>>? _interceptOverlay;
internal ShrinkCtx(ShrinkContextRuntime runtime, ShrinkCtx? parent, ShrinkFiber? ownerFiber,
Dictionary<string, string>? isolateOverlay,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? interceptOverlay = null)
{
Runtime = runtime;
Parent = parent;
OwnerFiber = ownerFiber;
_isolateOverlay = isolateOverlay;
ReplaceIntercept(interceptOverlay);
}
public ShrinkContextRuntime Runtime { get; }
public ShrinkCtx? Parent { get; }
public ShrinkFiber? OwnerFiber { get; }
// ---------------- 效应(论文 3.1 / 算法 1 ----------------
/// <summary>同步前向 + 可选同步逆操作的可逆效应;前向立即执行。</summary>
public ShrinkEffectHandle Effect(Action forward, Action? inverse = null)
{
if (forward == null)
throw new ArgumentNullException(nameof(forward));
forward();
var handle = new ShrinkEffectHandle();
if (inverse != null)
handle.AttachInverse(() =>
{
inverse();
return UniTask.CompletedTask;
});
Attach(handle);
return handle;
}
/// <summary>异步前向,完成时产出异步逆操作(论文 𝔈Γ)。DisposeAsync 会等待进行中的前向。</summary>
public async UniTask<ShrinkEffectHandle> EffectAsync(Func<UniTask<Func<UniTask>>> effectFn)
{
if (effectFn == null)
throw new ArgumentNullException(nameof(effectFn));
var handle = new ShrinkEffectHandle();
var gate = new UniTaskCompletionSource();
handle.BeginForward(gate.Task);
Attach(handle);
Func<UniTask>? inverse = null;
try
{
inverse = await effectFn();
handle.CompleteForward(inverse);
}
finally
{
// 先登记逆操作再放行 gate,保证等待前向的 DisposeAsync 能读到完整逆链
gate.TrySetResult();
}
return handle;
}
internal void Attach(ShrinkEffectHandle handle) => _effects.Add(handle);
/// <summary>
/// 前向已在外部执行完毕的效应:仅登记逆操作(供桥接适配器把既有 API 的"注册/注销"对包装为可逆效应)。
/// </summary>
public ShrinkEffectHandle EffectInverse(Func<UniTask> inverse)
{
if (inverse == null)
throw new ArgumentNullException(nameof(inverse));
var handle = new ShrinkEffectHandle();
handle.AttachInverse(inverse);
Attach(handle);
return handle;
}
/// <summary>按 LIFO 顺序执行本上下文注册的全部逆操作。</summary>
public async UniTask DisposeAsync()
{
for (var i = _effects.Count - 1; i >= 0; i--)
await _effects[i].DisposeAsync();
_effects.Clear();
}
// ---------------- 余效应(论文 3.2 / 算法 2 ----------------
/// <summary>
/// 提供依赖(set 是可逆效应):立即安装绑定并通知依赖者;
/// 逆操作撤回绑定并再次通知。句柄同时挂在当前上下文上,随上下文卸载自动撤回。
/// </summary>
public ShrinkEffectHandle Set<T>(string key, T value) => Runtime.SetBinding(this, key, value, null);
/// <summary>提供带访问策略的依赖;策略只在介导 Get 时运行,原始编排查找不会运行策略。</summary>
public ShrinkEffectHandle Set<T>(string key, T value, IShrinkCoeffectAccessPolicy accessPolicy) =>
Runtime.SetBinding(this, key, value, accessPolicy ?? throw new ArgumentNullException(nameof(accessPolicy)));
public ShrinkEffectHandle Set<T>(ShrinkKey<T> key, T value) =>
Set((key ?? throw new ArgumentNullException(nameof(key))).Id, value);
public ShrinkEffectHandle Set<T>(ShrinkKey<T> key, T value, IShrinkCoeffectAccessPolicy accessPolicy) =>
Set((key ?? throw new ArgumentNullException(nameof(key))).Id, value, accessPolicy);
/// <summary>
/// 代理介导的依赖访问(论文算法 6):沿纤程链解析已提交视图;
/// 声明了但未激活的键抛 <see cref="ShrinkInactiveAccessException"/>
/// 全链未声明的键抛 <see cref="ShrinkUndeclaredAccessException"/>。
/// </summary>
public T Get<T>(string key)
{
var fiber = OwnerFiber;
while (fiber != null)
{
if (fiber.Committed != null && fiber.Committed.TryGetValue(key, out var binding))
return Runtime.ResolveAccess<T>(this, key, binding);
if (fiber.InjectSet.Contains(key))
throw new ShrinkInactiveAccessException(key, fiber.Name);
fiber = fiber.Parent;
}
throw new ShrinkUndeclaredAccessException(key);
}
public T Get<T>(ShrinkKey<T> key) => Get<T>(
(key ?? throw new ArgumentNullException(nameof(key))).Id);
/// <summary>与 <see cref="Get{T}"/> 相同的介导规则;未激活返回 false 而不抛异常。</summary>
public bool TryGet<T>(string key, out T value)
{
var fiber = OwnerFiber;
while (fiber != null)
{
if (fiber.Committed != null && fiber.Committed.TryGetValue(key, out var binding))
{
value = Runtime.ResolveAccess<T>(this, key, binding);
return true;
}
if (fiber.InjectSet.Contains(key))
break;
fiber = fiber.Parent;
}
value = default!;
return false;
}
public bool TryGet<T>(ShrinkKey<T> key, out T value) => TryGet(
(key ?? throw new ArgumentNullException(nameof(key))).Id, out value);
/// <summary>
/// 派生隔离子上下文(论文定义 28/29 的派生实现):
/// 覆盖 key 的 realm 解析;父上下文不受影响;丢弃子上下文即隐式恢复。
/// </summary>
public ShrinkCtx Isolate(string key, string realm)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key must not be null or empty.", nameof(key));
if (string.IsNullOrEmpty(realm))
throw new ArgumentException("Realm must not be null or empty.", nameof(realm));
return new ShrinkCtx(Runtime, this, OwnerFiber, new Dictionary<string, string> { [key] = realm });
}
public ShrinkCtx Isolate<T>(ShrinkKey<T> key, string realm) => Isolate(
(key ?? throw new ArgumentNullException(nameof(key))).Id, realm);
/// <summary>
/// 派生带拦截元数据的子上下文。子级同名 metadata 覆盖父级;丢弃子上下文即恢复。
/// 该操作不改变 realm、provider 或 target,因此不会触发 fiber 重载。
/// </summary>
public ShrinkCtx Intercept(string key, IReadOnlyDictionary<string, object?> metadata)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Key must not be null or empty.", nameof(key));
if (metadata == null)
throw new ArgumentNullException(nameof(metadata));
var metadataCopy = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var pair in metadata)
metadataCopy[pair.Key] = pair.Value;
var overlay = new Dictionary<string, IReadOnlyDictionary<string, object?>>(StringComparer.Ordinal)
{
[key] = metadataCopy
};
return new ShrinkCtx(Runtime, this, OwnerFiber, null, overlay);
}
public ShrinkCtx Intercept<T>(ShrinkKey<T> key, IReadOnlyDictionary<string, object?> metadata) =>
Intercept((key ?? throw new ArgumentNullException(nameof(key))).Id, metadata);
internal ShrinkCtx CreateChildForFiber(ShrinkFiber fiber) => new(Runtime, this, fiber, null);
/// <summary>在派生子上下文中叠加隔离覆盖(同键后者优先),供加载器为条目配置隔离域。</summary>
internal ShrinkCtx WithIsolate(IReadOnlyDictionary<string, string> isolate)
{
var overlay = new Dictionary<string, string>();
if (_isolateOverlay != null)
{
foreach (var pair in _isolateOverlay)
overlay[pair.Key] = pair.Value;
}
foreach (var pair in isolate)
overlay[pair.Key] = pair.Value;
return new ShrinkCtx(Runtime, this, OwnerFiber, overlay);
}
/// <summary>在当前纤程上下文上叠加 loader 条目配置的 intercept。</summary>
internal ShrinkCtx WithIntercept(
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>> intercept) =>
new(Runtime, this, OwnerFiber, null, interceptOverlay: intercept);
/// <summary>原地替换本层 intercept,供 loader 在不重载 fiber 的情况下更新访问策略。</summary>
internal void ReplaceIntercept(
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object?>>? intercept)
{
if (intercept == null || intercept.Count == 0)
{
_interceptOverlay = null;
return;
}
var copy = new Dictionary<string, IReadOnlyDictionary<string, object?>>(StringComparer.Ordinal);
foreach (var pair in intercept)
{
if (string.IsNullOrWhiteSpace(pair.Key))
throw new ArgumentException("Intercept key must not be empty.", nameof(intercept));
if (pair.Value == null)
throw new ArgumentException($"Intercept metadata for '{pair.Key}' must not be null.",
nameof(intercept));
var metadataCopy = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var metadataPair in pair.Value)
metadataCopy[metadataPair.Key] = metadataPair.Value;
copy[pair.Key] = metadataCopy;
}
_interceptOverlay = copy;
}
/// <summary>解析 key 在本上下文中归属的 realm:沿父链找最近的隔离覆盖,默认 realm 为 key 自身。</summary>
internal string ResolveRealm(string key)
{
var current = this;
while (current != null)
{
if (current._isolateOverlay != null && current._isolateOverlay.TryGetValue(key, out var realm))
return realm;
current = current.Parent;
}
return key;
}
/// <summary>按根到叶顺序合并拦截元数据;越接近访问方的上下文优先。</summary>
internal IReadOnlyDictionary<string, object?> ResolveInterceptMetadata(string key)
{
var chain = new Stack<ShrinkCtx>();
for (var current = this; current != null; current = current.Parent)
chain.Push(current);
var merged = new Dictionary<string, object?>(StringComparer.Ordinal);
while (chain.Count > 0)
{
var current = chain.Pop();
if (current._interceptOverlay == null ||
!current._interceptOverlay.TryGetValue(key, out var metadata))
continue;
foreach (var pair in metadata)
merged[pair.Key] = pair.Value;
}
return merged;
}
internal IReadOnlyCollection<string> GetLocalInterceptKeys() =>
_interceptOverlay?.Keys ?? (IReadOnlyCollection<string>)Array.Empty<string>();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3c4e5dd3f658ca14a929dd41f2466927
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+61
View File
@@ -0,0 +1,61 @@
#nullable enable
using System;
using Cysharp.Threading.Tasks;
namespace ShrinkContext
{
/// <summary>
/// 单个可逆效应的句柄(论文算法 1 的 effect 封装)。
/// 前向执行时产出逆操作并累积在句柄内部;DisposeAsync 至多执行一次(armed 语义),
/// 若前向仍在进行中则先等待其完成再运行累积的逆操作(dispose = await task; recover())。
/// </summary>
public sealed class ShrinkEffectHandle
{
private static readonly Func<UniTask> Nop = static () => UniTask.CompletedTask;
private bool _armed = true;
private UniTask _forwardTask;
private bool _hasForwardTask;
private Func<UniTask> _inverseChain = Nop;
public bool IsArmed => _armed;
/// <summary>登记一个进行中的异步前向;DisposeAsync 会先等待它完成。</summary>
internal void BeginForward(UniTask forwardTask)
{
_forwardTask = forwardTask;
_hasForwardTask = true;
}
/// <summary>前向完成后登记其逆操作;新逆操作在链首执行(LIFO)。</summary>
internal void CompleteForward(Func<UniTask> inverse)
{
if (inverse == null)
return;
var previous = _inverseChain;
_inverseChain = async () =>
{
await inverse();
await previous();
};
}
/// <summary>直接附加逆操作(等效于前向已同步完成)。</summary>
internal void AttachInverse(Func<UniTask> inverse) => CompleteForward(inverse);
public async UniTask DisposeAsync()
{
if (!_armed)
return;
_armed = false;
if (_hasForwardTask && _forwardTask.Status == UniTaskStatus.Pending)
await _forwardTask;
var chain = _inverseChain;
_inverseChain = Nop;
await chain();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6baa51f655b3c9a4d9dc0b1dbdce8461
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea9777e12a1bd8e40a2afbce45d23622
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+47
View File
@@ -0,0 +1,47 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace ShrinkContext
{
/// <summary>
/// 组件 = (inject 依赖键集合 d, provide 供给键集合 p, apply 效应函数 e)(论文定义 43)。
/// apply 内部通过 ctx.Set 提供依赖、ctx.Get 消费依赖、ctx.Effect 注册可逆效应;
/// 组件的拆卸由运行时按已追踪的逆操作自动推导,无需手写卸载路径。
/// </summary>
public interface IShrinkComponent
{
string Name { get; }
/// <summary>声明的依赖键集合(论文 d):全部可用时组件才激活。</summary>
IReadOnlyList<string> Inject { get; }
/// <summary>供给键集合(论文 p):apply 的效应不应写入 p 之外的键。</summary>
IReadOnlyList<string> Provide { get; }
/// <summary>效应函数(论文 e):激活时执行;其中注册的效应在停用时按 LIFO 回滚。</summary>
UniTask ApplyAsync(ShrinkCtx ctx, object? config);
}
/// <summary>
/// 分步效应组件(论文 𝔈iterΓ):apply 以步进器逐步产出逆操作。
/// 目标在步进边界发生变化时中断迭代并回滚已执行步骤(部分回滚,论文 4.3.2 / 定理 64)。
/// 使用自定义 UniTask 步进协议而非 IAsyncEnumerable
/// Mono 的 ValueTask 迭代器恢复经同步上下文调度,会破坏同步内联的确定性语义。
/// </summary>
public interface IShrinkIterativeComponent : IShrinkComponent
{
IShrinkStepEffectEnumerator ApplySteps(ShrinkCtx ctx, object? config);
}
/// <summary>分步效应步进器:MoveNextAsync 执行该步前向并使 Current 指向其逆操作。</summary>
public interface IShrinkStepEffectEnumerator
{
/// <summary>推进到下一步;返回 false 表示迭代结束。前向在该调用内执行完毕。</summary>
UniTask<bool> MoveNextAsync();
/// <summary>最近一次 MoveNextAsync 成功后该步的逆操作。</summary>
Func<UniTask> Current { get; }
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc9d2e1bcf849da42a70c6c720e1c770
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+68
View File
@@ -0,0 +1,68 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace ShrinkContext
{
public enum ShrinkFiberState
{
Inactive = 0,
Loading = 1,
Active = 2,
Unloading = 3,
}
/// <summary>
/// 纤程(论文定义 44):组件的一次运行时实例化。
/// 生命周期由 Target(期望提供者集合,null 即 ⊥)与惯性状态机驱动:
/// 一旦进入转换即运行至完成,之后才响应新的目标变化(算法 5)。
/// </summary>
public sealed class ShrinkFiber
{
internal ShrinkFiber(long uid, IShrinkComponent component, ShrinkFiber? parent, object? config)
{
Uid = uid;
Component = component;
Parent = parent;
Config = config;
InjectSet = new HashSet<string>(component.Inject ?? Array.Empty<string>(), StringComparer.Ordinal);
ProvideSet = new HashSet<string>(component.Provide ?? Array.Empty<string>(), StringComparer.Ordinal);
}
public long Uid { get; }
public string Name => Component.Name;
public IShrinkComponent Component { get; }
/// <summary>实例化该纤程时的父纤程(根级为 null);父纤程卸载会级联到本纤程。</summary>
public ShrinkFiber? Parent { get; }
/// <summary>本纤程运行所在的派生子上下文。</summary>
public ShrinkCtx Ctx { get; internal set; } = null!;
public object? Config { get; }
public ShrinkFiberState State { get; internal set; } = ShrinkFiberState.Inactive;
/// <summary>是否到达过 Active(用于断言“从未激活”)。</summary>
public bool EverActive { get; internal set; }
/// <summary>最近一次 apply 抛出的异常;成功激活后清空。</summary>
public Exception? LastError { get; internal set; }
/// <summary>已提交视图(算法 5 reload 提交 / 算法 6 读取):依赖键 → 绑定快照。</summary>
public IReadOnlyDictionary<string, ShrinkBinding>? Committed => _committed;
/// <summary>进行中转换的句柄;任务在纤程到达静止态(Active 或 Inactive)时完成。</summary>
public UniTask Inertia { get; internal set; }
internal Dictionary<string, ShrinkBinding>? _committed;
internal long[]? Target;
internal bool InTransition;
internal bool Retired;
internal HashSet<string> InjectSet { get; }
internal HashSet<string> ProvideSet { get; }
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 812996226a7accb44b00963f89ff15fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f54de3c6db6ef7f4097d692f8ac11e38
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+412
View File
@@ -0,0 +1,412 @@
#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;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c236b8aa6fec940449f9303b855a3989
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ShrinkContext.Core.Runtime",
"rootNamespace": "ShrinkContext",
"references": [
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 768c7f0b5000fcc4084d4c4de00ff868
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+28
View File
@@ -0,0 +1,28 @@
#nullable enable
using UnityEngine;
namespace ShrinkContext
{
/// <summary>
/// 默认运行时入口 + Domain Reload 安全重置。
/// 不自动启动任何组件;上层 composition root 通过 ShrinkContextLoader 提交期望组合。
/// </summary>
public static class ShrinkContextDefaults
{
private static ShrinkContextRuntime? _default;
public static ShrinkContextRuntime Default => _default ??= new ShrinkContextRuntime();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStaticState()
{
_default = null;
}
/// <summary>测试用:丢弃默认运行时,下次访问时重建(模拟 Domain Reload)。</summary>
public static void ResetForTesting()
{
_default = null;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2734645dd1546e4fa4b496fda9349d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+112
View File
@@ -0,0 +1,112 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace ShrinkContext
{
public enum ShrinkFiberDiagnosticStatus
{
Inactive = 0,
Waiting = 1,
Loading = 2,
Active = 3,
Unloading = 4,
Failed = 5,
Retired = 6,
}
/// <summary>单个依赖键在快照时刻的解析、提交与潜在提供者。</summary>
public sealed class ShrinkDependencyDiagnostic
{
public ShrinkDependencyDiagnostic(string key, string realm, long? currentProviderUid,
long? committedProviderUid, IReadOnlyList<long> potentialProviderUids)
{
Key = key;
Realm = realm;
CurrentProviderUid = currentProviderUid;
CommittedProviderUid = committedProviderUid;
PotentialProviderUids = potentialProviderUids;
}
public string Key { get; }
public string Realm { get; }
public long? CurrentProviderUid { get; }
public long? CommittedProviderUid { get; }
public IReadOnlyList<long> PotentialProviderUids { get; }
public bool IsSatisfied => CurrentProviderUid.HasValue;
}
/// <summary>一个 fiber 的稳定只读诊断投影,不暴露可变运行时对象。</summary>
public sealed class ShrinkFiberDiagnostic
{
public ShrinkFiberDiagnostic(long uid, string name, ShrinkFiberDiagnosticStatus status,
ShrinkFiberState lifecycleState, bool retired, bool inTransition,
IReadOnlyList<string> inject, IReadOnlyList<string> provide,
IReadOnlyList<long> targetProviderUids,
IReadOnlyList<ShrinkDependencyDiagnostic> dependencies,
IReadOnlyList<string> interceptKeys,
string? errorType, string? errorMessage)
{
Uid = uid;
Name = name;
Status = status;
LifecycleState = lifecycleState;
Retired = retired;
InTransition = inTransition;
Inject = inject;
Provide = provide;
TargetProviderUids = targetProviderUids;
Dependencies = dependencies;
InterceptKeys = interceptKeys;
ErrorType = errorType;
ErrorMessage = errorMessage;
}
public long Uid { get; }
public string Name { get; }
public ShrinkFiberDiagnosticStatus Status { get; }
public ShrinkFiberState LifecycleState { get; }
public bool Retired { get; }
public bool InTransition { get; }
public IReadOnlyList<string> Inject { get; }
public IReadOnlyList<string> Provide { get; }
public IReadOnlyList<long> TargetProviderUids { get; }
public IReadOnlyList<ShrinkDependencyDiagnostic> Dependencies { get; }
public IReadOnlyList<string> InterceptKeys { get; }
public string? ErrorType { get; }
public string? ErrorMessage { get; }
}
public sealed class ShrinkNotificationDiagnostic
{
public ShrinkNotificationDiagnostic(long dispatchCount, long candidateVisitCount,
int lastCandidateCount, int indexedKeyCount)
{
DispatchCount = dispatchCount;
CandidateVisitCount = candidateVisitCount;
LastCandidateCount = lastCandidateCount;
IndexedKeyCount = indexedKeyCount;
}
public long DispatchCount { get; }
public long CandidateVisitCount { get; }
public int LastCandidateCount { get; }
public int IndexedKeyCount { get; }
}
/// <summary>运行时整体快照:fiber、绑定与 notify 索引状态。</summary>
public sealed class ShrinkContextRuntimeDiagnostic
{
public ShrinkContextRuntimeDiagnostic(IReadOnlyList<ShrinkFiberDiagnostic> fibers,
int bindingCount, ShrinkNotificationDiagnostic notifications)
{
Fibers = fibers;
BindingCount = bindingCount;
Notifications = notifications;
}
public IReadOnlyList<ShrinkFiberDiagnostic> Fibers { get; }
public int BindingCount { get; }
public ShrinkNotificationDiagnostic Notifications { get; }
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ee8044af3885c84a9b86b101af8ae28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+528
View File
@@ -0,0 +1,528 @@
#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,
};
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9abfe7bff0c65d4b8dae01a0fba1c09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1dae93ae310c91b42a482e989015132c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+156
View File
@@ -0,0 +1,156 @@
#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using ShrinkContext;
namespace ShrinkContext.Tests
{
/// <summary>
/// 余效应操作与介导访问(论文 3.2 / 算法 2、6):
/// set 是可逆效应;代理介导的 get 拒绝未声明/未激活访问;隔离域派生独立绑定。
/// </summary>
public class CoeffectAccessTests
{
private ShrinkContextRuntime _runtime = null!;
private ShrinkCtx _ctx = null!;
[SetUp]
public void SetUp()
{
_runtime = new ShrinkContextRuntime();
_ctx = _runtime.RootContext;
}
[Test]
public void Set_IsReversibleEffect_EarlyWithdrawAndContextDispose()
{
var handle = _ctx.Set("cfg", 42);
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "cfg", out var v1));
Assert.AreEqual(42, v1);
// 早期手动撤回
TestAwait.Run(handle.DisposeAsync());
Assert.IsFalse(_runtime.TryGetRaw<int>(_ctx, "cfg", out _));
// 再次安装后随上下文卸载自动撤回
_ctx.Set("cfg", 43);
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "cfg", out var v2));
Assert.AreEqual(43, v2);
TestAwait.Run(_ctx.DisposeAsync());
Assert.IsFalse(_runtime.TryGetRaw<int>(_ctx, "cfg", out _), "set 的逆操作必须移除绑定");
}
[Test]
public void MediatedGet_RejectsUndeclaredKey_InApply()
{
var provider = new ProviderComponent("provider", "real", "v");
var fiber = _runtime.Use(provider);
Assert.AreEqual(ShrinkFiberState.Active, fiber.State);
var greedy = new GreedyConsumerComponent("greedy", "real", "undeclared");
var greedyFiber = _runtime.Use(greedy);
Assert.AreEqual(ShrinkFiberState.Inactive, greedyFiber.State);
Assert.IsInstanceOf<ShrinkUndeclaredAccessException>(greedyFiber.LastError,
"未声明的依赖访问在访问点被拒绝,并使装载失败(算法 6 UNDECLARED_ACCESS");
}
[Test]
public void MediatedGet_ThrowsWhenOwnKeyNotActivated()
{
var consumer = new ConsumerComponent("consumer", "missing");
var fiber = _runtime.Use(consumer);
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State,
"依赖缺失时组件保持非活动,而不是抛错(响应式余效应)");
Assert.Throws<ShrinkInactiveAccessException>(() => fiber.Ctx.Get<object>("missing"));
}
[Test]
public void Isolate_DerivesIndependentRealm_RestoreByDiscard()
{
_ctx.Set("k", 1);
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "k", out var rootValue));
Assert.AreEqual(1, rootValue);
var isolated = _ctx.Isolate("k", "test-realm");
// 隔离域内初始为空:逻辑键相同但 realm 不同
Assert.IsFalse(_runtime.TryGetRaw<int>(isolated, "k", out _));
isolated.Set("k", 2);
Assert.IsTrue(_runtime.TryGetRaw<int>(isolated, "k", out var isolatedValue));
Assert.AreEqual(2, isolatedValue);
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "k", out var rootValueAfter));
Assert.AreEqual(1, rootValueAfter, "隔离域写入不得影响父上下文绑定");
// 派生上下文的恢复是隐式的:丢弃即恢复(论文定义 27 派生实现)
TestAwait.Run(isolated.DisposeAsync());
Assert.IsFalse(_runtime.TryGetRaw<int>(isolated, "k", out _));
Assert.IsTrue(_runtime.TryGetRaw<int>(_ctx, "k", out var rootValueFinal));
Assert.AreEqual(1, rootValueFinal);
}
[Test]
public void TypedKey_PreservesVersionedIdentityAcrossMediatedAccess()
{
var key = new ShrinkKey<string>("test.package", "typed-service", 2);
Assert.AreEqual("test.package/typed-service@v2", key.Id);
_runtime.Use(new PolicyProviderComponent(key.Id), "typed-value");
var consumer = _runtime.Use(new PolicyConsumerComponent(key.Id));
Assert.AreEqual(ShrinkFiberState.Active, consumer.State);
Assert.AreEqual("typed-value", consumer.Ctx.Get(key));
}
[Test]
public void Intercept_MergesRootToLeafAndChildMetadataWins()
{
const string key = "policy.service";
_runtime.Use(new PolicyProviderComponent(key), "value");
var consumer = _runtime.Use(new PolicyConsumerComponent(key));
Assert.AreEqual(ShrinkFiberState.Active, consumer.State);
var parent = consumer.Ctx.Intercept(key, new Dictionary<string, object?>
{
["prefix"] = "parent-",
["suffix"] = "-parent"
});
var child = parent.Intercept(key, new Dictionary<string, object?>
{
["suffix"] = "-child"
});
Assert.AreEqual("parent-value-child", child.Get<string>(key));
}
/// <summary>声明一个键却访问另一个未声明键的组件(验证能力介导纪律)。</summary>
private sealed class GreedyConsumerComponent : IShrinkComponent
{
private readonly string[] _inject;
public GreedyConsumerComponent(string name, string declaredKey, string undeclaredKey)
{
Name = name;
UndeclaredKey = undeclaredKey;
_inject = new[] { declaredKey };
}
public string Name { get; }
public string UndeclaredKey { get; }
public IReadOnlyList<string> Inject => _inject;
public IReadOnlyList<string> Provide => Array.Empty<string>();
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Get<object>(UndeclaredKey);
return UniTask.CompletedTask;
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9521a5ada1510c24d8e17fee319ff808
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+131
View File
@@ -0,0 +1,131 @@
#nullable enable
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using ShrinkContext;
namespace ShrinkContext.Tests
{
/// <summary>
/// 可逆效应语义(论文 3.1 / 算法 1):
/// 定理 7(恢复到初始上下文)、定理 16(LIFO 逐步还原)、armed 单次执行、异步前向等待。
/// </summary>
public class EffectTrackingTests
{
private ShrinkContextRuntime _runtime = null!;
private ShrinkCtx _ctx = null!;
[SetUp]
public void SetUp()
{
_runtime = new ShrinkContextRuntime();
_ctx = _runtime.RootContext;
}
[Test]
public void DisposeAsync_RestoresInitialState_Theorem7()
{
var shared = new Dictionary<string, int>();
_ctx.Effect(() => shared["a"] = 1, () => shared.Remove("a"));
_ctx.Effect(() => shared["b"] = 2, () => shared.Remove("b"));
_ctx.Effect(() => shared["c"] = 3, () => shared.Remove("c"));
Assert.AreEqual(3, shared.Count);
TestAwait.Run(_ctx.DisposeAsync());
Assert.IsEmpty(shared, "累加器回滚后上下文必须恢复到初始状态(定理 7)");
}
[Test]
public void ManualDispose_UndoesOnlyItsOwnEffect()
{
var shared = new Dictionary<string, int>();
_ctx.Effect(() => shared["a"] = 1, () => shared.Remove("a"));
var effectB = _ctx.Effect(() => shared["b"] = 2, () => shared.Remove("b"));
_ctx.Effect(() => shared["c"] = 3, () => shared.Remove("c"));
TestAwait.Run(effectB.DisposeAsync());
Assert.IsFalse(shared.ContainsKey("b"));
Assert.IsTrue(shared.ContainsKey("a"));
Assert.IsTrue(shared.ContainsKey("c"), "选择性撤销不得影响其它效应(𝔈Γ 提升)");
TestAwait.Run(_ctx.DisposeAsync());
Assert.IsEmpty(shared);
}
[Test]
public void ManualDispose_IsIdempotent_ArmedSemantics()
{
var undoCount = 0;
var handle = _ctx.Effect(() => { }, () => undoCount++);
TestAwait.Run(handle.DisposeAsync());
TestAwait.Run(handle.DisposeAsync());
TestAwait.Run(_ctx.DisposeAsync());
Assert.AreEqual(1, undoCount, "逆操作至多执行一次(armed 语义)");
}
[Test]
public void AsyncInverses_RunInLifoOrder()
{
var log = new List<string>();
var h1 = TestAwait.Run(_ctx.EffectAsync(async () =>
{
log.Add("f1");
return () =>
{
log.Add("i1");
return UniTask.CompletedTask;
};
}));
var h2 = TestAwait.Run(_ctx.EffectAsync(async () =>
{
log.Add("f2");
return async () =>
{
log.Add("i2");
await UniTask.CompletedTask;
};
}));
TestAwait.Run(h2.DisposeAsync());
CollectionAssert.AreEqual(new[] { "f1", "f2", "i2" }, log);
TestAwait.Run(_ctx.DisposeAsync());
CollectionAssert.AreEqual(new[] { "f1", "f2", "i2", "i1" }, log, "后注册的逆操作先执行(LIFO");
}
[Test]
public void DisposeAsync_WaitsForInflightForwardBeforeRecovering()
{
var log = new List<string>();
var gate = new UniTaskCompletionSource();
var effectTask = _ctx.EffectAsync(async () =>
{
await gate.Task;
log.Add("fwd");
return () =>
{
log.Add("inv");
return UniTask.CompletedTask;
};
});
// 前向仍在进行中时启动卸载:dispose 应先等待前向完成,再执行其产出的逆操作
var disposeTask = _ctx.DisposeAsync();
Assert.IsEmpty(log, "前向未完成时不得提前执行恢复");
gate.TrySetResult();
TestAwait.Run(disposeTask);
var handle = TestAwait.Run(effectTask);
CollectionAssert.AreEqual(new[] { "fwd", "inv" }, log, "dispose = await task; recover()");
Assert.IsFalse(handle.IsArmed);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ec8f710e1ae95f4dbcb4d1b7eb471c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+323
View File
@@ -0,0 +1,323 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using ShrinkContext;
namespace ShrinkContext.Tests
{
/// <summary>
/// 纤程生命周期(论文算法 4、5 与元理论 4.4 的可观察行为):
/// 依赖驱动激活/停用、提供者替换重载、drain-before-inverse 次序、
/// 父子级联、供给冲突、循环依赖静止、惯性链式转换、分步部分回滚、错误路径回滚。
/// </summary>
public class FiberLifecycleTests
{
private ShrinkContextRuntime _runtime = null!;
[SetUp]
public void SetUp()
{
_runtime = new ShrinkContextRuntime();
}
[Test]
public void ProviderReady_ConsumerActivates()
{
var provider = new ProviderComponent("provider", "svc", "v1");
var consumer = new ConsumerComponent("consumer", "svc");
var providerFiber = _runtime.Use(provider);
var consumerFiber = _runtime.Use(consumer);
Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State);
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
Assert.AreEqual("v1", consumer.LastSeen);
Assert.AreEqual(1, consumer.LoadCount);
}
[Test]
public void MissingDependency_StaysInactiveThenActivatesOnLateProvider()
{
var consumer = new ConsumerComponent("consumer", "svc");
var consumerFiber = _runtime.Use(consumer);
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State,
"依赖缺失时静默保持非活动,不抛错、不加载");
Assert.AreEqual(0, consumer.LoadCount);
var provider = new ProviderComponent("provider", "svc", "late");
var providerFiber = _runtime.Use(provider);
Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State);
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State,
"提供者出现后依赖者被响应式激活(activating 转换)");
Assert.AreEqual("late", consumer.LastSeen);
}
[Test]
public void RetireProvider_DrainsDependentsBeforeOwnInverses()
{
var log = new List<string>();
var provider = new OrderProviderComponent("provider", "svc", "v1", log);
var consumer = new OrderConsumerComponent("consumer", "svc", log);
var providerFiber = _runtime.Use(provider);
var consumerFiber = _runtime.Use(consumer);
CollectionAssert.AreEqual(new[] { "provider-effect", "consumer-load" }, log);
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
TestAwait.Run(_runtime.RetireAsync(providerFiber));
CollectionAssert.AreEqual(
new[] { "provider-effect", "consumer-load", "consumer-undo", "provider-undo" },
log,
"依赖者必须先于提供者的逆操作完成卸载(L-Unload 守卫:drain dependents");
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State);
Assert.AreEqual(ShrinkFiberState.Inactive, providerFiber.State);
}
[Test]
public void ProviderReplacement_ReloadsDependent()
{
var first = new ProviderComponent("p1", "svc", "v1");
var consumer = new ConsumerComponent("consumer", "svc");
var firstFiber = _runtime.Use(first);
var consumerFiber = _runtime.Use(consumer);
Assert.AreEqual("v1", consumer.LastSeen);
TestAwait.Run(_runtime.RetireAsync(firstFiber));
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State);
var second = new ProviderComponent("p2", "svc", "v2");
var secondFiber = _runtime.Use(second);
Assert.AreEqual(ShrinkFiberState.Active, secondFiber.State);
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
Assert.AreEqual("v2", consumer.LastSeen);
Assert.AreEqual(2, consumer.LoadCount, "提供者替换必须触发依赖者重载");
}
[Test]
public void ProviderReplacement_SameValueStillReloads_UidSemantics()
{
var first = new ProviderComponent("p1", "svc", "same");
var consumer = new ConsumerComponent("consumer", "svc");
var firstFiber = _runtime.Use(first);
var consumerFiber = _runtime.Use(consumer);
TestAwait.Run(_runtime.RetireAsync(firstFiber));
var second = new ProviderComponent("p2", "svc", "same");
_runtime.Use(second);
Assert.AreEqual(2, consumer.LoadCount,
"目标按提供者 uid 而非值比较:同值不同提供者的替换也要重载(定义 46)");
Assert.AreEqual("same", consumer.LastSeen);
}
[Test]
public void ParentRetire_CascadesToChildFiber()
{
var parent = new ProviderComponent("parent", "parent.svc", "pv");
var parentFiber = _runtime.Use(parent);
var child = new ConsumerComponent("child", "parent.svc");
var childFiber = _runtime.Use(child, null, parentFiber.Ctx);
Assert.AreEqual(ShrinkFiberState.Active, childFiber.State);
Assert.AreSame(parentFiber, childFiber.Parent);
TestAwait.Run(_runtime.RetireAsync(parentFiber));
Assert.AreEqual(ShrinkFiberState.Inactive, parentFiber.State);
Assert.AreEqual(ShrinkFiberState.Inactive, childFiber.State,
"父纤程卸载必须级联卸载子纤程(算法 4:实例化是父的可逆效应)");
}
[Test]
public void SupplyConflict_SecondProviderFailsAndKeepsFirst()
{
var first = new ProviderComponent("p1", "svc", "v1");
var firstFiber = _runtime.Use(first);
Assert.AreEqual(ShrinkFiberState.Active, firstFiber.State);
var second = new ProviderComponent("p2", "svc", "v2");
var secondFiber = _runtime.Use(second);
Assert.AreEqual(ShrinkFiberState.Inactive, secondFiber.State);
Assert.IsInstanceOf<ShrinkSupplyConflictException>(secondFiber.LastError,
"两个活跃纤程供给同一 realm 必须在生成阶段报错(供给不相交)");
Assert.IsTrue(_runtime.TryGetRaw<string>(_runtime.RootContext, "svc", out var value));
Assert.AreEqual("v1", value, "冲突失败后第一个提供者的绑定必须保持完好");
Assert.AreEqual(ShrinkFiberState.Active, firstFiber.State);
}
[Test]
public void UndeclaredSupply_FailsFiberAndLeavesNoBinding()
{
var fiber = _runtime.Use(new UndeclaredProviderComponent());
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State);
Assert.IsInstanceOf<ShrinkUndeclaredSupplyException>(fiber.LastError);
Assert.IsFalse(_runtime.TryGetRaw<int>(_runtime.RootContext, "not-declared", out _));
var snapshot = _runtime.CaptureDiagnostic();
var diagnostic = snapshot.Fibers.Single(item => item.Uid == fiber.Uid);
Assert.AreEqual(ShrinkFiberDiagnosticStatus.Failed, diagnostic.Status);
StringAssert.Contains("not-declared", diagnostic.ErrorMessage);
}
[Test]
public void DiagnosticSnapshot_ExplainsWaitingDependencyAndPotentialProvider()
{
var waiting = _runtime.Use(new ConsumerComponent("waiting", "svc"));
var blockedProvider = _runtime.Use(new ProviderComponent("blocked-provider", "svc")
{
Inject = new[] { "missing" }
});
var snapshot = _runtime.CaptureDiagnostic();
var waitingDiagnostic = snapshot.Fibers.Single(item => item.Uid == waiting.Uid);
Assert.AreEqual(ShrinkFiberDiagnosticStatus.Waiting, waitingDiagnostic.Status);
Assert.AreEqual(1, waitingDiagnostic.Dependencies.Count);
Assert.IsFalse(waitingDiagnostic.Dependencies[0].IsSatisfied);
CollectionAssert.Contains(waitingDiagnostic.Dependencies[0].PotentialProviderUids,
blockedProvider.Uid,
"等待链应显示声明了该键、但自身依赖未满足的潜在提供者");
}
[Test]
public void NotifyIndex_VisitsOnlyConsumersOfChangedKey()
{
var target = new ConsumerComponent("target", "target-key");
_runtime.Use(target);
for (var i = 0; i < 40; i++)
_runtime.Use(new ConsumerComponent("unrelated-" + i, "other-" + i));
_runtime.Use(new ProviderComponent("provider", "target-key", "value"));
var notifications = _runtime.CaptureDiagnostic().Notifications;
Assert.AreEqual(1, notifications.LastCandidateCount,
"notify 应从 key 倒排索引取得候选,不扫描 40 个无关 fiber");
Assert.AreEqual(41, notifications.IndexedKeyCount);
Assert.AreEqual("value", target.LastSeen);
}
[Test]
public void CircularDependencies_BothStayInactiveWithoutError()
{
var a = new ProviderComponent("a", "ka") { Inject = new[] { "kb" } };
var b = new ProviderComponent("b", "kb") { Inject = new[] { "ka" } };
var fiberA = _runtime.Use(a);
var fiberB = _runtime.Use(b);
Assert.AreEqual(ShrinkFiberState.Inactive, fiberA.State);
Assert.AreEqual(ShrinkFiberState.Inactive, fiberB.State);
Assert.IsNull(fiberA.LastError);
Assert.IsNull(fiberB.LastError,
"依赖循环表现为可预测的永久非活动,而不是死锁或异常(论文 6.5)");
}
[Test]
public void Inertia_MidLoadRetire_NeverActivatesAndChainsToUnload()
{
var gate1 = new UniTaskCompletionSource();
var gate2 = new UniTaskCompletionSource();
var first = new ProviderComponent("p1", "svc", "v1") { OnApply = () => gate1.Task };
var consumer = new ConsumerComponent("consumer", "svc");
var firstFiber = _runtime.Use(first);
Assert.AreEqual(ShrinkFiberState.Loading, firstFiber.State);
var consumerFiber = _runtime.Use(consumer);
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State,
"提供者仍在装载(未 Active)时,依赖者不可见其绑定");
// 装载中途退役:目标翻转 ⊥,但进行中的转换继续跑到惯性边界
var retireTask = _runtime.RetireAsync(firstFiber);
Assert.AreEqual(ShrinkFiberState.Loading, firstFiber.State,
"惯性:已进入的转换继续运行,之后才响应新目标");
var second = new ProviderComponent("p2", "svc", "v2") { OnApply = () => gate2.Task };
var secondFiber = _runtime.Use(second);
gate2.TrySetResult();
Assert.AreEqual(ShrinkFiberState.Active, secondFiber.State);
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
Assert.AreEqual("v2", consumer.LastSeen, "依赖者切换到新提供者");
gate1.TrySetResult();
TestAwait.Run(retireTask);
Assert.AreEqual(ShrinkFiberState.Inactive, firstFiber.State);
Assert.IsFalse(firstFiber.EverActive,
"装载中途目标变化的组件不得进入 Active,必须链式转入卸载并回滚");
Assert.IsNotNull(firstFiber.LastError, "旧提供者补装时撞上活跃的新提供者,按供给冲突失败");
Assert.IsTrue(_runtime.TryGetRaw<string>(_runtime.RootContext, "svc", out var value));
Assert.AreEqual("v2", value, "失败路径不得破坏新提供者的绑定");
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
}
[Test]
public void IterativeApply_TargetChangeInterruptsAndRollsBackExecutedSteps()
{
var component = new IterativeComponent("iterative", "svc", 3);
var fiber = _runtime.Use(component);
Assert.AreEqual(ShrinkFiberState.Loading, fiber.State);
component.Gates[0].TrySetResult();
Assert.AreEqual(1, component.ExecutedSteps);
// 转换仍在步进器等待中时翻转目标(不阻塞:延续由后续闸门驱动)
var retireTask = _runtime.RetireAsync(fiber);
// 放行第 2 步:执行完该步后,步进边界守卫发现目标已变 → 中断 + 回滚已执行步骤
component.Gates[1].TrySetResult();
TestAwait.Run(retireTask);
Assert.AreEqual(2, component.ExecutedSteps, "守卫在第 3 步之前中断迭代");
CollectionAssert.AreEqual(
new[] { "step1", "step2", "undo2", "undo1" },
component.Steps,
"部分回滚:仅已执行步骤的逆操作按 LIFO 运行(定理 64 转换内中断)");
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State);
Assert.IsFalse(fiber.EverActive);
Assert.IsFalse(_runtime.TryGetRaw<string>(_runtime.RootContext, "svc", out _),
"步骤中的 set 效应必须随回滚撤回");
}
[Test]
public void ApplyFailure_RollsBackPartialEffects()
{
var failing = new FailingComponent("failing", "svc");
var fiber = _runtime.Use(failing);
Assert.AreEqual(ShrinkFiberState.Inactive, fiber.State);
Assert.IsFalse(fiber.EverActive);
Assert.IsInstanceOf<InvalidOperationException>(fiber.LastError);
Assert.IsFalse(_runtime.TryGetRaw<string>(_runtime.RootContext, "svc", out _),
"apply 抛异常前注册的效应必须全部回滚(L-Raise → 卸载路径)");
}
[Test]
public void DomainReset_DefaultRuntimeIsRebuilt()
{
ShrinkContextDefaults.ResetForTesting();
var runtime = ShrinkContextDefaults.Default;
runtime.Use(new ProviderComponent("p", "k", "v"));
ShrinkContextDefaults.ResetForTesting();
var rebuilt = ShrinkContextDefaults.Default;
Assert.AreNotSame(runtime, rebuilt);
Assert.AreEqual(0, rebuilt.Fibers.Count, "Domain Reload 后注册表必须为空");
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ed0e47a6dd74e242809ce87c39833aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+312
View File
@@ -0,0 +1,312 @@
#nullable enable
using System;
using System.Collections.Generic;
using NUnit.Framework;
using ShrinkContext;
namespace ShrinkContext.Tests
{
/// <summary>
/// 声明式加载器(论文 5.2 的原型子集):
/// 增量协调、幂等、退役、disabled 开关、组件/配置变化重建、依赖缺失等待、隔离域条目。
/// </summary>
public class LoaderTests
{
private ShrinkContextRuntime _runtime = null!;
private ShrinkComponentCatalog _catalog = null!;
private ShrinkContextLoader _loader = null!;
[SetUp]
public void SetUp()
{
_runtime = new ShrinkContextRuntime();
_catalog = new ShrinkComponentCatalog();
_catalog.Register("provider", () => new ConfigProviderComponent("provider", "svc"));
_catalog.Register("provider.alt", () => new ConfigProviderComponent("provider.alt", "svc"));
_catalog.Register("consumer", () => new ConsumerComponent("consumer", "svc"));
_catalog.Register("failing", () => new FailingComponent("failing", "svc"));
_catalog.Register("policy.provider", () => new PolicyProviderComponent("policy.service"));
_catalog.Register("policy.consumer", () => new PolicyConsumerComponent("policy.service"));
_loader = new ShrinkContextLoader(_runtime, _catalog);
}
[Test]
public void Apply_CreatesEntriesAndActivates()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1"),
new("consumer-entry", "consumer"),
}));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var providerFiber));
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
Assert.AreEqual(ShrinkFiberState.Active, providerFiber.State);
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
}
[Test]
public void Apply_IsIdempotent_SecondApplyDoesNotReload()
{
var config = new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1"),
new("consumer-entry", "consumer"),
};
TestAwait.Run(_loader.ApplyAsync(config));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber));
TestAwait.Run(_loader.ApplyAsync(config));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var secondFiber));
Assert.AreSame(firstFiber, secondFiber, "无变化的条目不得重建纤程");
var providerComponent = (ConfigProviderComponent)secondFiber.Component;
Assert.AreEqual(1, providerComponent.ApplyCount, "幂等协调不触发重载");
}
[Test]
public void RemovedEntry_RetiresFiber_DependentDeactivatesButStaysManaged()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1"),
new("consumer-entry", "consumer"),
}));
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("consumer-entry", "consumer"),
}));
Assert.IsFalse(_loader.TryGetFiber("provider-entry", out _), "消失的条目不再被管理");
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State,
"提供者退役后依赖者被响应式停用,但条目仍被管理、等待依赖回归");
// 依赖回归后无需重建条目即可重新激活
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1"),
new("consumer-entry", "consumer"),
}));
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
}
[Test]
public void DisabledEntry_UnloadsAndReloadsOnEnable()
{
var enabled = new List<ShrinkLoaderEntry> { new("provider-entry", "provider", "v1") };
TestAwait.Run(_loader.ApplyAsync(enabled));
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1", disabled: true),
}));
Assert.IsFalse(_loader.TryGetFiber("provider-entry", out _),
"disabled 条目不持有纤程,但条目本身仍被管理");
CollectionAssert.Contains(_loader.ManagedEntryIds, "provider-entry");
TestAwait.Run(_loader.ApplyAsync(enabled));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var reloadedFiber));
Assert.AreEqual(ShrinkFiberState.Active, reloadedFiber.State, "取消 disabled 后重新加载");
}
[Test]
public void ComponentChange_RebuildsEntry()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1"),
new("consumer-entry", "consumer"),
}));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber));
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider.alt", "v2"),
new("consumer-entry", "consumer"),
}));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var rebuiltFiber));
Assert.AreNotSame(firstFiber, rebuiltFiber, "组件名变化必须重建条目(论文 id/url → 重建)");
Assert.AreEqual(ShrinkFiberState.Active, rebuiltFiber.State);
Assert.AreEqual("provider.alt", rebuiltFiber.Component.Name);
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
var consumer = (ConsumerComponent)consumerFiber.Component;
Assert.AreEqual("v2", consumer.LastSeen, "依赖者切换到重建后的新提供者");
}
[Test]
public void ConfigChange_ReloadsEntryWithNewConfig()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v1"),
new("consumer-entry", "consumer"),
}));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var firstFiber));
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "v2"),
new("consumer-entry", "consumer"),
}));
// 原型语义:配置变化走重建(组件自决 diff 属后续阶段)
Assert.AreEqual(ShrinkFiberState.Inactive, firstFiber.State);
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var rebuiltFiber));
Assert.AreNotSame(firstFiber, rebuiltFiber);
Assert.AreEqual("v2", rebuiltFiber.Config, "重建后的纤程携带新配置");
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
var consumer = (ConsumerComponent)consumerFiber.Component;
Assert.AreEqual("v2", consumer.LastSeen, "依赖者看到新配置的值");
}
[Test]
public void MissingDependency_WaitsWithoutError_ThenActivatesWhenProvided()
{
// 首轮只有消费者:不抛“依赖缺失”异常,条目保持被管理、纤程 Inactive
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("consumer-entry", "consumer"),
}));
Assert.IsTrue(_loader.TryGetFiber("consumer-entry", out var consumerFiber));
Assert.AreEqual(ShrinkFiberState.Inactive, consumerFiber.State);
// 次轮补上提供者:依赖者被响应式激活,无需重发消费者条目
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("consumer-entry", "consumer"),
new("provider-entry", "provider", "late"),
}));
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
Assert.AreEqual("late", ((ConsumerComponent)consumerFiber.Component).LastSeen);
}
[Test]
public void DuplicateEntryIds_Throw()
{
Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("dup", "provider", "v1"),
new("dup", "provider", "v2"),
})));
}
[Test]
public void UnknownComponent_ThrowsWithEntryContext()
{
var ex = Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(
new List<ShrinkLoaderEntry> { new("entry", "no.such.component") })));
StringAssert.Contains("no.such.component", ex!.Message);
StringAssert.Contains("entry", ex.Message);
}
[Test]
public void IsolatedEntries_SameKeyCoexistInDifferentRealms()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("p1", "provider", "from-r1", isolate: new Dictionary<string, string> { ["svc"] = "r1" }),
new("p2", "provider", "from-r2", isolate: new Dictionary<string, string> { ["svc"] = "r2" }),
new("c2", "consumer", isolate: new Dictionary<string, string> { ["svc"] = "r2" }),
}));
Assert.IsTrue(_loader.TryGetFiber("p1", out var f1));
Assert.IsTrue(_loader.TryGetFiber("p2", out var f2));
Assert.AreEqual(ShrinkFiberState.Active, f1.State, "不同隔离域的同键供给互不冲突");
Assert.AreEqual(ShrinkFiberState.Active, f2.State);
Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber));
Assert.AreEqual(ShrinkFiberState.Active, consumerFiber.State);
Assert.AreEqual("from-r2", ((ConsumerComponent)consumerFiber.Component).LastSeen,
"消费者按自身隔离域解析到 r2 的绑定");
}
[Test]
public void IsolatedProviderReplacement_DoesNotReloadOtherRealmConsumer()
{
var isolateR1 = new Dictionary<string, string> { ["svc"] = "r1" };
var isolateR2 = new Dictionary<string, string> { ["svc"] = "r2" };
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("p1", "provider", "r1-v1", isolate: isolateR1),
new("p2", "provider", "r2-v1", isolate: isolateR2),
new("c2", "consumer", isolate: isolateR2),
}));
Assert.IsTrue(_loader.TryGetFiber("c2", out var consumerFiber));
var consumer = (ConsumerComponent)consumerFiber.Component;
Assert.AreEqual(1, consumer.LoadCount);
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("p1", "provider", "r1-v2", isolate: isolateR1),
new("p2", "provider", "r2-v1", isolate: isolateR2),
new("c2", "consumer", isolate: isolateR2),
}));
Assert.AreEqual(1, consumer.LoadCount,
"r1 提供者替换不得触发只解析 r2 的消费者重载");
Assert.AreEqual("r2-v1", consumer.LastSeen);
}
[Test]
public void InterceptUpdate_ChangesAccessWithoutReloadingFiber()
{
var suffixA = new Dictionary<string, IReadOnlyDictionary<string, object?>>
{
["policy.service"] = new Dictionary<string, object?> { ["suffix"] = "-a" }
};
var suffixB = new Dictionary<string, IReadOnlyDictionary<string, object?>>
{
["policy.service"] = new Dictionary<string, object?> { ["suffix"] = "-b" }
};
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("policy-provider", "policy.provider", "base"),
new("policy-consumer", "policy.consumer", intercept: suffixA),
}));
Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var firstFiber));
var consumer = (PolicyConsumerComponent)firstFiber.Component;
Assert.AreEqual("base-a", consumer.Read());
Assert.AreEqual(1, consumer.ApplyCount);
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("policy-provider", "policy.provider", "base"),
new("policy-consumer", "policy.consumer", intercept: suffixB),
}));
Assert.IsTrue(_loader.TryGetFiber("policy-consumer", out var sameFiber));
Assert.AreSame(firstFiber, sameFiber);
Assert.AreEqual(1, consumer.ApplyCount, "intercept 元数据变化不应触发 reload");
Assert.AreEqual("base-b", consumer.Read(), "访问时应读取更新后的 metadata");
}
[Test]
public void FailedReplacement_RestoresPreviousLoaderCompositionAndReportsTransaction()
{
TestAwait.Run(_loader.ApplyAsync(new List<ShrinkLoaderEntry>
{
new("provider-entry", "provider", "stable")
}));
Assert.Throws<ShrinkLoaderException>(() => TestAwait.Run(_loader.ApplyAsync(
new List<ShrinkLoaderEntry> { new("provider-entry", "failing") })));
Assert.IsTrue(_loader.TryGetFiber("provider-entry", out var restored));
Assert.AreEqual(ShrinkFiberState.Active, restored.State);
Assert.AreEqual("stable", restored.Config);
Assert.IsNotNull(_loader.LastTransaction);
Assert.AreEqual(ShrinkLoaderTransactionPhase.Failed, _loader.LastTransaction!.Phase);
Assert.IsTrue(_loader.LastTransaction.RestoreAttempted);
Assert.IsTrue(_loader.LastTransaction.PreviousCompositionRestored);
StringAssert.Contains("failed during apply", _loader.LastTransaction.ErrorMessage);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1d3d814cbbc1964fbbdbbd819944624
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+22
View File
@@ -0,0 +1,22 @@
{
"name": "ShrinkContext.Core.Tests",
"rootNamespace": "ShrinkContext.Tests",
"references": [
"ShrinkContext.Core.Runtime",
"UniTask",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 61a3fcbd28ec0454cb1afe26d1efb9ee
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+341
View File
@@ -0,0 +1,341 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using ShrinkContext;
namespace ShrinkContext.Tests
{
/// <summary>提供者测试组件:把 Value 设置到供给键上;OnApply 作为装载闸门。</summary>
public sealed class ProviderComponent : IShrinkComponent
{
private readonly string[] _provide;
public ProviderComponent(string name, string key, object? value = null)
{
Name = name;
Value = value;
_provide = new[] { key };
}
public string Name { get; }
public object? Value { get; set; }
public IReadOnlyList<string> Inject { get; set; } = Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public Func<UniTask>? OnApply { get; set; }
public int ApplyCount { get; private set; }
public async UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ApplyCount++;
if (OnApply != null)
await OnApply();
foreach (var key in _provide)
ctx.Set(key, Value);
}
}
/// <summary>消费者测试组件:装载时读取依赖键的当前值;OnApply 作为装载闸门。</summary>
public sealed class ConsumerComponent : IShrinkComponent
{
private readonly string[] _inject;
public ConsumerComponent(string name, string key)
{
Name = name;
Key = key;
_inject = new[] { key };
}
public string Name { get; }
public string Key { get; }
public IReadOnlyList<string> Inject => _inject;
public IReadOnlyList<string> Provide => Array.Empty<string>();
public Func<UniTask>? OnApply { get; set; }
public int LoadCount { get; private set; }
public object? LastSeen { get; private set; }
public async UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
LoadCount++;
if (OnApply != null)
await OnApply();
LastSeen = ctx.Get<object>(Key);
}
}
/// <summary>记录效应顺序的提供者/消费者组合组件(验证 drain-before-inverse 次序)。</summary>
public sealed class OrderProviderComponent : IShrinkComponent
{
private readonly string[] _provide;
public OrderProviderComponent(string name, string key, object? value, List<string> log)
{
Name = name;
Key = key;
Value = value;
Log = log;
_provide = new[] { key };
}
public string Name { get; }
public string Key { get; }
public object? Value { get; }
public List<string> Log { get; }
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set(Key, Value);
ctx.Effect(
() => Log.Add("provider-effect"),
() => Log.Add("provider-undo"));
return UniTask.CompletedTask;
}
}
/// <summary>记录效应顺序的消费者组件。</summary>
public sealed class OrderConsumerComponent : IShrinkComponent
{
private readonly string[] _inject;
public OrderConsumerComponent(string name, string key, List<string> log)
{
Name = name;
Key = key;
Log = log;
_inject = new[] { key };
}
public string Name { get; }
public string Key { get; }
public List<string> Log { get; }
public IReadOnlyList<string> Inject => _inject;
public IReadOnlyList<string> Provide => Array.Empty<string>();
public object? LastSeen { get; private set; }
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
LastSeen = ctx.Get<object>(Key);
ctx.Effect(
() => Log.Add("consumer-load"),
() => Log.Add("consumer-undo"));
return UniTask.CompletedTask;
}
}
/// <summary>apply 中抛异常的组件:验证错误路径的部分回滚。</summary>
public sealed class FailingComponent : IShrinkComponent
{
private readonly string[] _provide;
public FailingComponent(string name, string key, Func<UniTask>? beforeThrow = null)
{
Name = name;
Key = key;
BeforeThrow = beforeThrow;
_provide = new[] { key };
}
public string Name { get; }
public string Key { get; }
public Func<UniTask>? BeforeThrow { get; set; }
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public async UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
if (BeforeThrow != null)
await BeforeThrow();
ctx.Set(Key, "poison");
throw new InvalidOperationException("apply failed: " + Name);
}
}
/// <summary>
/// 分步效应组件(论文 𝔈iterΓ):每步等待对应闸门放行后执行并产出逆操作;
/// 用于验证目标变化在步进边界中断迭代并部分回滚。
/// </summary>
public sealed class IterativeComponent : IShrinkComponent, IShrinkIterativeComponent
{
private readonly string[] _provide;
public IterativeComponent(string name, string key, int stepCount)
{
Name = name;
Key = key;
_provide = new[] { key };
Steps = new List<string>();
Gates = new List<UniTaskCompletionSource>();
for (var i = 0; i < stepCount; i++)
Gates.Add(new UniTaskCompletionSource());
}
public string Name { get; }
public string Key { get; }
public List<string> Steps { get; }
public List<UniTaskCompletionSource> Gates { get; }
public int ExecutedSteps { get; private set; }
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
throw new NotSupportedException("Iterative component must be driven via ApplySteps.");
}
public IShrinkStepEffectEnumerator ApplySteps(ShrinkCtx ctx, object? config) => new StepEnumerator(this, ctx);
private sealed class StepEnumerator : IShrinkStepEffectEnumerator
{
private readonly IterativeComponent _owner;
private readonly ShrinkCtx _ctx;
private int _index = -1;
public StepEnumerator(IterativeComponent owner, ShrinkCtx ctx)
{
_owner = owner;
_ctx = ctx;
}
public Func<UniTask> Current { get; private set; } = null!;
public async UniTask<bool> MoveNextAsync()
{
_index++;
if (_index >= _owner.Gates.Count)
return false;
await _owner.Gates[_index].Task;
var step = _index + 1;
_ctx.Set(_owner.Key, "step-" + step);
_owner.Steps.Add("step" + step);
_owner.ExecutedSteps++;
Current = () =>
{
_owner.Steps.Add("undo" + step);
return UniTask.CompletedTask;
};
return true;
}
}
}
/// <summary>配置驱动值的提供者组件:apply 时把纤程 config 写入供给键(加载器测试用)。</summary>
public sealed class ConfigProviderComponent : IShrinkComponent
{
private readonly string[] _provide;
public ConfigProviderComponent(string name, string key)
{
Name = name;
Key = key;
_provide = new[] { key };
}
public string Name { get; }
public string Key { get; }
public int ApplyCount { get; private set; }
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ApplyCount++;
ctx.Set(Key, config);
return UniTask.CompletedTask;
}
}
/// <summary>故意写入未声明键,验证 Provide 运行时契约。</summary>
public sealed class UndeclaredProviderComponent : IShrinkComponent
{
public string Name => "undeclared-provider";
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => Array.Empty<string>();
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set("not-declared", 1);
return UniTask.CompletedTask;
}
}
public sealed class MetadataSuffixPolicy : IShrinkCoeffectAccessPolicy
{
public object? Resolve(ShrinkCoeffectAccessContext context, object? value)
{
var text = value?.ToString() ?? string.Empty;
if (context.TryGetMetadata<string>("prefix", out var prefix))
text = prefix + text;
return context.TryGetMetadata<string>("suffix", out var suffix) ? text + suffix : text;
}
}
public sealed class PolicyProviderComponent : IShrinkComponent
{
private readonly string[] _provide;
public PolicyProviderComponent(string key)
{
Key = key;
_provide = new[] { key };
}
public string Key { get; }
public string Name => "policy-provider";
public IReadOnlyList<string> Inject => Array.Empty<string>();
public IReadOnlyList<string> Provide => _provide;
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
ctx.Set(Key, config?.ToString() ?? "value", new MetadataSuffixPolicy());
return UniTask.CompletedTask;
}
}
public sealed class PolicyConsumerComponent : IShrinkComponent
{
private readonly string[] _inject;
private ShrinkCtx? _ctx;
public PolicyConsumerComponent(string key)
{
Key = key;
_inject = new[] { key };
}
public string Key { get; }
public string Name => "policy-consumer";
public int ApplyCount { get; private set; }
public IReadOnlyList<string> Inject => _inject;
public IReadOnlyList<string> Provide => Array.Empty<string>();
public UniTask ApplyAsync(ShrinkCtx ctx, object? config)
{
_ctx = ctx;
ApplyCount++;
return UniTask.CompletedTask;
}
public string Read() => (_ctx ?? throw new InvalidOperationException("Consumer is not active."))
.Get<string>(Key);
}
/// <summary>测试辅助:同步等待 UniTask(测试保证全部延续同步完成,无死锁风险)。</summary>
public static class TestAwait
{
public static void Run(UniTask task)
{
task.GetAwaiter().GetResult();
}
public static T Run<T>(UniTask<T> task)
{
return task.GetAwaiter().GetResult();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b69edb9cb0df36d42bdce226b11ebb71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+23
View File
@@ -0,0 +1,23 @@
{
"name": "com.cneicy.shrink-context-core",
"version": "0.1.0",
"displayName": "ShrinkContext Core",
"description": "Cordis 时空可组合性范式的 Unity/C# 原型核心:可逆效应追踪、响应式余效应与组件纤程生命周期。",
"unity": "2022.3",
"dependencies": {
"com.cysharp.unitask": "2.5.10"
},
"keywords": [
"context",
"composability",
"effect",
"coeffect",
"fiber",
"cordis"
],
"author": {
"name": "cneicy",
"url": "https://git.crash.work/ShrinkSDK"
},
"documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkContext.Core"
}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 578cf78705b2fd149b54c94f558be58e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: