Compare commits
2
Commits
901ad18453
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b14d6d0117
|
||
|
|
0449e82c0c
|
No files matched your search
@@ -52,6 +52,9 @@ ExportedObj/
|
|||||||
.consulo/
|
.consulo/
|
||||||
*.csproj
|
*.csproj
|
||||||
!Tools~/DotNet/**/*.csproj
|
!Tools~/DotNet/**/*.csproj
|
||||||
|
!Samples~/DotNetConsole/*.csproj
|
||||||
|
/Samples~/DotNetConsole/[Bb]in/
|
||||||
|
/Samples~/DotNetConsole/[Oo]bj/
|
||||||
*.unityproj
|
*.unityproj
|
||||||
*.sln
|
*.sln
|
||||||
*.suo
|
*.suo
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。
|
本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。
|
||||||
|
|
||||||
|
## [2.3.2] - 2026-11-06
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- 诊断观察者列表在订阅或退订时生成不可变快照,消除每次诊断派发的列表分配,保留逐个观察者的异常隔离。
|
||||||
|
- 同步排队直接保存回调,移除额外闭包;主线程调度器复用泵送回调,保持延后事件的批次边界。
|
||||||
|
|
||||||
## [2.3.0] - 2026-11-06
|
## [2.3.0] - 2026-11-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<AssemblyName>ShrinkEventBus.Runtime</AssemblyName>
|
<AssemblyName>ShrinkEventBus.Runtime</AssemblyName>
|
||||||
<RootNamespace>ShrinkEventBus</RootNamespace>
|
<RootNamespace>ShrinkEventBus</RootNamespace>
|
||||||
<PackageId>ShrinkSDK.EventBus</PackageId>
|
<PackageId>ShrinkSDK.EventBus</PackageId>
|
||||||
<Version>2.3.1</Version>
|
<Version>2.3.2</Version>
|
||||||
<Description>ShrinkSDK typed event bus runtime.</Description>
|
<Description>ShrinkSDK typed event bus runtime.</Description>
|
||||||
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
|
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -166,23 +166,31 @@ public static class GlobalHandlers
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 `partial`,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。
|
Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;NuGet 包通过 `ShrinkSDK.CodeGen` 生成绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。
|
||||||
|
|
||||||
## Unity 与纯 .NET
|
## Unity 与纯 .NET
|
||||||
|
|
||||||
Unity 包依赖 UniTask:
|
Unity 和正式 NuGet 包 `ShrinkSDK.EventBus` 均使用 UniTask,可直接 `await`:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
UniTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
UniTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||||||
```
|
```
|
||||||
|
|
||||||
纯 .NET 实现在 `Tools~/DotNet/ShrinkEventBus.Core`:
|
普通 C# 项目无需安装 Unity 或 Godot。使用 .NET 8 或更新 SDK 新建控制台项目后配置包源并安装:
|
||||||
|
|
||||||
```csharp
|
```shell
|
||||||
ValueTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
dotnet new console -n EventBusDemo
|
||||||
|
cd EventBusDemo
|
||||||
|
dotnet new nugetconfig
|
||||||
|
dotnet nuget add source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --name ShrinkSDK --configfile nuget.config
|
||||||
|
dotnet add package ShrinkSDK.EventBus --version 2.3.2
|
||||||
```
|
```
|
||||||
|
|
||||||
两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。
|
UniTask、Runtime.Abstractions 和构建时 CodeGen 会随包自动还原,不需要手动添加分析器或项目引用。完整可运行示例见 [Samples~/DotNetConsole](Samples~/DotNetConsole/Program.cs),可用它替换新项目的 `Program.cs` 后执行 `dotnet run -c Release`。
|
||||||
|
|
||||||
|
控制台和后台服务建议显式创建 `Inline`、`DedicatedThread` 或 `TaskPool` Bus。`MainThread` 跨线程派发及 `PostDeferred` 需要宿主提供可用的主线程调度器;普通控制台没有 Unity/Godot 的帧循环。
|
||||||
|
|
||||||
|
`Tools~/DotNet/ShrinkEventBus.Core` 是独立的 ValueTask 工具实现,不是上述正式 NuGet 包的运行时。事件数据不应依赖 UniTask/ValueTask。
|
||||||
|
|
||||||
## ECS/Burst
|
## ECS/Burst
|
||||||
|
|
||||||
|
|||||||
+35
-7
@@ -57,6 +57,8 @@ namespace ShrinkEventBus
|
|||||||
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
|
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
|
||||||
private static Action<IShrinkEvent, Type, ShrinkBusKey>? _posted;
|
private static Action<IShrinkEvent, Type, ShrinkBusKey>? _posted;
|
||||||
private static Action<ShrinkEventTrace>? _detailedPosted;
|
private static Action<ShrinkEventTrace>? _detailedPosted;
|
||||||
|
private static readonly object DetailedObserverGate = new();
|
||||||
|
private static Action<ShrinkEventTrace>[]? _detailedObserverSnapshot;
|
||||||
|
|
||||||
internal static event Action<IShrinkEvent, Type, ShrinkBusKey> Posted
|
internal static event Action<IShrinkEvent, Type, ShrinkBusKey> Posted
|
||||||
{
|
{
|
||||||
@@ -78,14 +80,41 @@ namespace ShrinkEventBus
|
|||||||
|
|
||||||
internal static event Action<ShrinkEventTrace> DetailedPosted
|
internal static event Action<ShrinkEventTrace> DetailedPosted
|
||||||
{
|
{
|
||||||
add => _detailedPosted += value;
|
add
|
||||||
remove => _detailedPosted -= value;
|
{
|
||||||
|
lock (DetailedObserverGate)
|
||||||
|
{
|
||||||
|
_detailedPosted += value;
|
||||||
|
PublishDetailedObserverSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
lock (DetailedObserverGate)
|
||||||
|
{
|
||||||
|
_detailedPosted -= value;
|
||||||
|
PublishDetailedObserverSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PublishDetailedObserverSnapshot()
|
||||||
|
{
|
||||||
|
var invocationList = _detailedPosted?.GetInvocationList();
|
||||||
|
Action<ShrinkEventTrace>[]? snapshot = null;
|
||||||
|
if (invocationList != null)
|
||||||
|
{
|
||||||
|
snapshot = new Action<ShrinkEventTrace>[invocationList.Length];
|
||||||
|
for (var i = 0; i < snapshot.Length; i++)
|
||||||
|
snapshot[i] = (Action<ShrinkEventTrace>)invocationList[i];
|
||||||
|
}
|
||||||
|
Volatile.Write(ref _detailedObserverSnapshot, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool HasDetailedPostedObservers
|
internal static bool HasDetailedPostedObservers
|
||||||
{
|
{
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
get => Volatile.Read(ref _detailedPosted) != null;
|
get => Volatile.Read(ref _detailedObserverSnapshot) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IShrinkEventBus Default => DefaultBus;
|
public static IShrinkEventBus Default => DefaultBus;
|
||||||
@@ -212,15 +241,14 @@ namespace ShrinkEventBus
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static void NotifyDetailedPosted(ShrinkEventTrace trace)
|
internal static void NotifyDetailedPosted(ShrinkEventTrace trace)
|
||||||
{
|
{
|
||||||
var observers = Volatile.Read(ref _detailedPosted);
|
var observers = Volatile.Read(ref _detailedObserverSnapshot);
|
||||||
if (observers == null)
|
if (observers == null)
|
||||||
return;
|
return;
|
||||||
var invocationList = observers.GetInvocationList();
|
for (var i = 0; i < observers.Length; i++)
|
||||||
for (var i = 0; i < invocationList.Length; i++)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
((Action<ShrinkEventTrace>)invocationList[i])(trace);
|
observers[i](trace);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,15 +59,29 @@ namespace ShrinkEventBus
|
|||||||
internal sealed class ShrinkSchedulerWorkItem
|
internal sealed class ShrinkSchedulerWorkItem
|
||||||
{
|
{
|
||||||
private readonly UniTaskCompletionSource? _completion;
|
private readonly UniTaskCompletionSource? _completion;
|
||||||
|
private readonly Delegate _action;
|
||||||
|
|
||||||
|
public ShrinkSchedulerWorkItem(Action action)
|
||||||
|
{
|
||||||
|
_action = action ?? throw new ArgumentNullException(nameof(action));
|
||||||
|
}
|
||||||
|
|
||||||
public ShrinkSchedulerWorkItem(Func<UniTask> action, bool awaitable)
|
public ShrinkSchedulerWorkItem(Func<UniTask> action, bool awaitable)
|
||||||
{
|
{
|
||||||
Action = action ?? throw new ArgumentNullException(nameof(action));
|
_action = action ?? throw new ArgumentNullException(nameof(action));
|
||||||
if (awaitable)
|
if (awaitable)
|
||||||
_completion = new UniTaskCompletionSource();
|
_completion = new UniTaskCompletionSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<UniTask> Action { get; }
|
public UniTask Invoke()
|
||||||
|
{
|
||||||
|
if (_action is Action synchronousAction)
|
||||||
|
{
|
||||||
|
synchronousAction();
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
return ((Func<UniTask>)_action)();
|
||||||
|
}
|
||||||
public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask;
|
public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask;
|
||||||
|
|
||||||
public void Complete() => _completion?.TrySetResult();
|
public void Complete() => _completion?.TrySetResult();
|
||||||
@@ -169,7 +183,7 @@ namespace ShrinkEventBus
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await item.Action();
|
await item.Invoke();
|
||||||
item.Complete();
|
item.Complete();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
@@ -187,7 +201,7 @@ namespace ShrinkEventBus
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
item.Action().GetAwaiter().GetResult();
|
item.Invoke().GetAwaiter().GetResult();
|
||||||
item.Complete();
|
item.Complete();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
@@ -232,6 +246,7 @@ namespace ShrinkEventBus
|
|||||||
private readonly MainThreadDispatcherContract _dispatcher;
|
private readonly MainThreadDispatcherContract _dispatcher;
|
||||||
private readonly ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher? _deferredDispatcher;
|
private readonly ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher? _deferredDispatcher;
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
|
private readonly Action _pump;
|
||||||
private int _pumpScheduled;
|
private int _pumpScheduled;
|
||||||
private int _disposed;
|
private int _disposed;
|
||||||
private bool _discardPending;
|
private bool _discardPending;
|
||||||
@@ -241,6 +256,7 @@ namespace ShrinkEventBus
|
|||||||
_queue = new ShrinkSchedulerQueue(name, options);
|
_queue = new ShrinkSchedulerQueue(name, options);
|
||||||
_dispatcher = ShrinkEventBusRuntime.MainThreadDispatcher;
|
_dispatcher = ShrinkEventBusRuntime.MainThreadDispatcher;
|
||||||
_deferredDispatcher = _dispatcher as ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher;
|
_deferredDispatcher = _dispatcher as ShrinkSDK.Runtime.IShrinkDeferredMainThreadDispatcher;
|
||||||
|
_pump = () => PumpAsync().Forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOnSchedulerThread => _dispatcher.IsMainThread;
|
public bool IsOnSchedulerThread => _dispatcher.IsMainThread;
|
||||||
@@ -269,11 +285,7 @@ namespace ShrinkEventBus
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_disposed != 0) return ShrinkDeferredPostStatus.Stopped;
|
if (_disposed != 0) return ShrinkDeferredPostStatus.Stopped;
|
||||||
var item = new ShrinkSchedulerWorkItem(() =>
|
var item = new ShrinkSchedulerWorkItem(action);
|
||||||
{
|
|
||||||
action();
|
|
||||||
return UniTask.CompletedTask;
|
|
||||||
}, awaitable: false);
|
|
||||||
if (!_queue.TryEnqueue(item)) return ShrinkDeferredPostStatus.QueueFull;
|
if (!_queue.TryEnqueue(item)) return ShrinkDeferredPostStatus.QueueFull;
|
||||||
return SchedulePump() ? ShrinkDeferredPostStatus.Accepted : ShrinkDeferredPostStatus.SchedulerRejected;
|
return SchedulePump() ? ShrinkDeferredPostStatus.Accepted : ShrinkDeferredPostStatus.SchedulerRejected;
|
||||||
}
|
}
|
||||||
@@ -333,8 +345,8 @@ namespace ShrinkEventBus
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
accepted = _deferredDispatcher != null
|
accepted = _deferredDispatcher != null
|
||||||
? _deferredDispatcher.TryPostDeferred(() => PumpAsync().Forget())
|
? _deferredDispatcher.TryPostDeferred(_pump)
|
||||||
: _dispatcher.TryPost(() => PumpAsync().Forget());
|
: _dispatcher.TryPost(_pump);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -398,11 +410,7 @@ namespace ShrinkEventBus
|
|||||||
{
|
{
|
||||||
if (Volatile.Read(ref _disposed) != 0)
|
if (Volatile.Read(ref _disposed) != 0)
|
||||||
return false;
|
return false;
|
||||||
var item = new ShrinkSchedulerWorkItem(() =>
|
var item = new ShrinkSchedulerWorkItem(action);
|
||||||
{
|
|
||||||
action();
|
|
||||||
return UniTask.CompletedTask;
|
|
||||||
}, awaitable: false);
|
|
||||||
if (!_queue.TryEnqueue(item))
|
if (!_queue.TryEnqueue(item))
|
||||||
return false;
|
return false;
|
||||||
ScheduleWorkers();
|
ScheduleWorkers();
|
||||||
@@ -503,11 +511,7 @@ namespace ShrinkEventBus
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = new ShrinkSchedulerWorkItem(() =>
|
var item = new ShrinkSchedulerWorkItem(action);
|
||||||
{
|
|
||||||
action();
|
|
||||||
return UniTask.CompletedTask;
|
|
||||||
}, awaitable: false);
|
|
||||||
if (!_queue.TryEnqueue(item))
|
if (!_queue.TryEnqueue(item))
|
||||||
return false;
|
return false;
|
||||||
_signal.Set();
|
_signal.Set();
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ShrinkSDK.EventBus" Version="2.3.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using ShrinkEventBus;
|
||||||
|
|
||||||
|
using var host = new ShrinkEventBusHost();
|
||||||
|
var bus = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||||
|
var subscriber = new OrderSubscriber();
|
||||||
|
using var binding = host.Attach(subscriber);
|
||||||
|
var diagnostics = 0;
|
||||||
|
using var observation = EventBus.ObserveDiagnostics(_ => diagnostics++);
|
||||||
|
|
||||||
|
bus.Post(new OrderPlaced(2));
|
||||||
|
await bus.PostAsync(new OrderPlaced(3));
|
||||||
|
if (subscriber.Total != 5 || diagnostics != 2)
|
||||||
|
throw new InvalidOperationException("Generated subscription or diagnostics failed.");
|
||||||
|
|
||||||
|
var workerKey = new ShrinkBusKey("worker", "orders");
|
||||||
|
var worker = host.CreateBus(workerKey, ShrinkBusOptions.DedicatedThread());
|
||||||
|
var workerSubscriber = new OrderSubscriber();
|
||||||
|
using var workerBinding = host.Attach(workerSubscriber, workerKey);
|
||||||
|
await worker.PostAsync(new OrderPlaced(7));
|
||||||
|
if (workerSubscriber.Total != 7)
|
||||||
|
throw new InvalidOperationException("Dedicated-thread delivery failed.");
|
||||||
|
|
||||||
|
binding.Dispose();
|
||||||
|
bus.Post(new OrderPlaced(10));
|
||||||
|
if (subscriber.Total != 5)
|
||||||
|
throw new InvalidOperationException("Unsubscribe failed.");
|
||||||
|
|
||||||
|
await host.ShutdownAsync();
|
||||||
|
Console.WriteLine("EVENTBUS_CONSOLE_PASS: sync, async, diagnostics, worker, unsubscribe");
|
||||||
|
|
||||||
|
public readonly record struct OrderPlaced(int Amount) : IShrinkEvent;
|
||||||
|
|
||||||
|
[ShrinkEventSubscriber]
|
||||||
|
public sealed partial class OrderSubscriber
|
||||||
|
{
|
||||||
|
public int Total { get; private set; }
|
||||||
|
|
||||||
|
[ShrinkSubscribe]
|
||||||
|
private void OnOrder(OrderPlaced order) => Total += order.Amount;
|
||||||
|
}
|
||||||
@@ -73,6 +73,35 @@ namespace ShrinkEventBus.Tests
|
|||||||
Assert.AreEqual(1, secondObserverCalls);
|
Assert.AreEqual(1, secondObserverCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ObserveDiagnostics_MutationsApplyToNextDispatch()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
IDisposable second = null;
|
||||||
|
IDisposable added = null;
|
||||||
|
using var first = EventBus.ObserveDiagnostics(_ =>
|
||||||
|
{
|
||||||
|
calls.Add("first");
|
||||||
|
second.Dispose();
|
||||||
|
if (added == null)
|
||||||
|
added = EventBus.ObserveDiagnostics(__ => calls.Add("added"));
|
||||||
|
});
|
||||||
|
second = EventBus.ObserveDiagnostics(_ => calls.Add("second"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EventBus.NotifyDetailedPosted(default);
|
||||||
|
CollectionAssert.AreEqual(new[] { "first", "second" }, calls);
|
||||||
|
calls.Clear();
|
||||||
|
EventBus.NotifyDetailedPosted(default);
|
||||||
|
CollectionAssert.AreEqual(new[] { "first", "added" }, calls);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
second.Dispose();
|
||||||
|
added?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void PostDeferred_DoesNotDispatchBeforePump()
|
public void PostDeferred_DoesNotDispatchBeforePump()
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.cneicy.shrink-eventbus",
|
"name": "com.cneicy.shrink-eventbus",
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"displayName": "ShrinkEventBus",
|
"displayName": "ShrinkEventBus",
|
||||||
"description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。",
|
"description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。",
|
||||||
"unity": "2022.3",
|
"unity": "2022.3",
|
||||||
|
|||||||
Reference in new issue
Block a user