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

This commit is contained in:
2026-08-26 02:50:18 +08:00
commit 1c921f5aec
86 changed files with 6934 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
[assembly: InternalsVisibleTo("ShrinkEventBus.Editor")]
[assembly: InternalsVisibleTo("ShrinkEventBus.Tests")]
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43e85f78c24f1d94ba7d06a4fa37e937
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+196
View File
@@ -0,0 +1,196 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
internal readonly struct ShrinkEventTrace
{
public ShrinkEventTrace(DateTime timestampUtc, Type eventType, ShrinkBusKey busKey,
ShrinkBusOptions options, int threadId, long elapsedTimestampTicks,
ShrinkPostResult result, bool isAsync)
{
TimestampUtc = timestampUtc;
EventType = eventType;
BusKey = busKey;
Scheduler = options.Scheduler;
DispatchMode = options.DispatchMode;
ThreadId = threadId;
ElapsedTimestampTicks = elapsedTimestampTicks;
Result = result;
IsAsync = isAsync;
}
public DateTime TimestampUtc { get; }
public Type EventType { get; }
public ShrinkBusKey BusKey { get; }
public ShrinkBusSchedulerKind Scheduler { get; }
public ShrinkDispatchMode DispatchMode { get; }
public int ThreadId { get; }
public long ElapsedTimestampTicks { get; }
public ShrinkPostResult Result { get; }
public bool IsAsync { get; }
}
public static class EventBus
{
private sealed class GlobalBusResolver : IShrinkBusResolver
{
public IShrinkEventBus GetBus(ShrinkBusKey key)
{
if (TryGetBus(key, out var bus))
return bus;
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
}
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
EventBus.TryGetBus(key, out bus);
}
private static readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> Buses = new();
internal static readonly IShrinkBusResolver Resolver = new GlobalBusResolver();
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
private static Action<IShrinkEvent, Type, ShrinkBusKey>? _posted;
private static Action<ShrinkEventTrace>? _detailedPosted;
internal static event Action<IShrinkEvent, Type, ShrinkBusKey> Posted
{
add
{
_posted += value;
}
remove
{
_posted -= value;
}
}
internal static bool HasPostedObservers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Volatile.Read(ref _posted) != null;
}
internal static event Action<ShrinkEventTrace> DetailedPosted
{
add => _detailedPosted += value;
remove => _detailedPosted -= value;
}
internal static bool HasDetailedPostedObservers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Volatile.Read(ref _detailedPosted) != null;
}
public static IShrinkEventBus Default => DefaultBus;
public static IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
var bus = BuildBus(key, options);
if (!Buses.TryAdd(key, bus))
{
bus.Dispose();
throw new InvalidOperationException($"Bus '{key}' is already registered.");
}
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
return bus;
}
public static IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
var bus = Buses.GetOrAdd(key, busKey => BuildBus(busKey, options));
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
return bus;
}
public static bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
Buses.TryGetValue(key, out bus!);
public static bool RemoveBus(ShrinkBusKey key)
{
if (key == ShrinkBusKey.Game || !Buses.TryRemove(key, out var bus))
return false;
ShrinkStaticBindingRegistry.DetachBus(key);
bus.Dispose();
return true;
}
public static ShrinkPostResult Post<TEvent>(in TEvent eventData)
where TEvent : IShrinkEvent => DefaultBus.Post(in eventData);
public static UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default)
where TEvent : IShrinkEvent => DefaultBus.PostAsync(eventData, cancellationToken);
public static IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target is not IShrinkGeneratedSubscriber generated)
throw new InvalidOperationException(
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
return generated.AttachGenerated(Resolver, defaultBus ?? ShrinkBusKey.Game);
}
internal static IReadOnlyList<(ShrinkBusKey Key, ShrinkBusOptions Options, int Subscribers)> Snapshot()
{
var snapshot = new List<(ShrinkBusKey, ShrinkBusOptions, int)>(Buses.Count);
foreach (var pair in Buses)
{
var subscribers = pair.Value is ShrinkEventBusInstance instance
? instance.SubscriberCount
: 0;
snapshot.Add((pair.Key, pair.Value.Options, subscribers));
}
return snapshot;
}
private static IShrinkEventBus CreateDefaultBus()
{
var bus = BuildBus(ShrinkBusKey.Game, ShrinkBusOptions.MainThread());
Buses[ShrinkBusKey.Game] = bus;
ShrinkStaticBindingRegistry.AttachForBus(ShrinkBusKey.Game, Resolver);
return bus;
}
private static IShrinkEventBus BuildBus(ShrinkBusKey key, ShrinkBusOptions options) =>
new ShrinkEventBusBuilder()
.WithKey(key)
.WithOptions(options)
.WithPostObserver(NotifyPostedObject)
.Build();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void NotifyPostedObject(IShrinkEvent eventData, Type eventType, ShrinkBusKey key)
{
_posted?.Invoke(eventData, eventType, key);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void NotifyDetailedPosted(ShrinkEventTrace trace)
{
var observers = Volatile.Read(ref _detailedPosted);
if (observers == null)
return;
try
{
observers(trace);
}
catch (Exception exception)
{
UnityEngine.Debug.LogException(exception);
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b995a65fd1324255a5ade1ae1f0adf32
timeCreated: 1760099076
+9
View File
@@ -0,0 +1,9 @@
namespace ShrinkEventBus
{
public enum EventResult
{
DEFAULT,
ALLOW,
DENY
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5213a68ce8f433a98f272904a5d7bd2
timeCreated: 1760098791
+23
View File
@@ -0,0 +1,23 @@
#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public interface IShrinkEventBus : IDisposable
{
ShrinkBusKey Key { get; }
ShrinkBusOptions Options { get; }
ShrinkPostResult Post<TEvent>(in TEvent eventData)
where TEvent : IShrinkEvent;
UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default)
where TEvent : IShrinkEvent;
IDisposable Attach(object target);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb69f308f5c727542ac602709b02c628
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+546
View File
@@ -0,0 +1,546 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
internal interface IShrinkBusScheduler : IDisposable
{
bool IsOnSchedulerThread { get; }
bool TryPost(Action action);
UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken);
UniTask ShutdownAsync(bool drain, TimeSpan timeout);
}
internal sealed class ShrinkQueueFullException : InvalidOperationException
{
public ShrinkQueueFullException(string schedulerName)
: base($"Scheduler queue '{schedulerName}' is full.")
{
}
}
internal static class ShrinkBusSchedulerFactory
{
public static IShrinkBusScheduler Create(ShrinkBusOptions options, string name)
{
return options.Scheduler switch
{
ShrinkBusSchedulerKind.MainThread => new ShrinkMainThreadScheduler(name, options),
ShrinkBusSchedulerKind.DedicatedThread => new ShrinkDedicatedThreadScheduler(name, options),
ShrinkBusSchedulerKind.TaskPool => new ShrinkTaskPoolScheduler(name, options),
_ => new ShrinkInlineScheduler()
};
}
}
internal sealed class ShrinkSchedulerWorkItem
{
private readonly UniTaskCompletionSource? _completion;
public ShrinkSchedulerWorkItem(Func<UniTask> action, bool awaitable)
{
Action = action ?? throw new ArgumentNullException(nameof(action));
if (awaitable)
_completion = new UniTaskCompletionSource();
}
public Func<UniTask> Action { get; }
public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask;
public void Complete() => _completion?.TrySetResult();
public void Fail(Exception exception) => _completion?.TrySetException(exception);
public void Cancel(CancellationToken cancellationToken) => _completion?.TrySetCanceled(cancellationToken);
public void Drop(string schedulerName) =>
_completion?.TrySetException(new ShrinkQueueFullException(schedulerName));
}
internal sealed class ShrinkSchedulerQueue : IDisposable
{
private readonly ConcurrentQueue<ShrinkSchedulerWorkItem> _queue = new();
private readonly SemaphoreSlim _slots;
private readonly ShrinkQueueOverflowPolicy _overflowPolicy;
private readonly string _name;
private int _disposed;
public ShrinkSchedulerQueue(string name, ShrinkBusOptions options)
{
_name = name;
_overflowPolicy = options.OverflowPolicy;
_slots = new SemaphoreSlim(options.QueueCapacity, options.QueueCapacity);
}
public bool IsEmpty => _queue.IsEmpty;
public bool TryEnqueue(ShrinkSchedulerWorkItem item)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (!_slots.Wait(0))
{
if (_overflowPolicy != ShrinkQueueOverflowPolicy.DropOldest ||
!_queue.TryDequeue(out var dropped))
return false;
dropped.Drop(_name);
_slots.Release();
if (!_slots.Wait(0))
return false;
}
_queue.Enqueue(item);
return true;
}
public async UniTask EnqueueAsync(ShrinkSchedulerWorkItem item, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(_name);
if (_overflowPolicy == ShrinkQueueOverflowPolicy.Wait)
{
await _slots.WaitAsync(cancellationToken).AsUniTask(useCurrentSynchronizationContext: false);
if (Volatile.Read(ref _disposed) != 0)
{
_slots.Release();
throw new ObjectDisposedException(_name);
}
_queue.Enqueue(item);
return;
}
if (!TryEnqueue(item))
throw new ShrinkQueueFullException(_name);
}
public bool TryDequeue(out ShrinkSchedulerWorkItem item)
{
if (!_queue.TryDequeue(out item!))
return false;
_slots.Release();
return true;
}
public void DropPending()
{
while (_queue.TryDequeue(out var item))
{
_slots.Release();
item.Drop(_name);
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
DropPending();
_slots.Dispose();
}
}
internal static class ShrinkSchedulerWorkItemRunner
{
public static async UniTask RunAsync(ShrinkSchedulerWorkItem item)
{
try
{
await item.Action();
item.Complete();
}
catch (OperationCanceledException ex)
{
item.Cancel(ex.CancellationToken);
}
catch (Exception ex)
{
item.Fail(ex);
ShrinkEventDiagnostics.LogException(ex);
}
}
public static void RunBlocking(ShrinkSchedulerWorkItem item)
{
try
{
item.Action().GetAwaiter().GetResult();
item.Complete();
}
catch (OperationCanceledException ex)
{
item.Cancel(ex.CancellationToken);
}
catch (Exception ex)
{
item.Fail(ex);
ShrinkEventDiagnostics.LogException(ex);
}
}
}
internal sealed class ShrinkInlineScheduler : IShrinkBusScheduler
{
public bool IsOnSchedulerThread => true;
public bool TryPost(Action action)
{
action();
return true;
}
public UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return action();
}
public UniTask ShutdownAsync(bool drain, TimeSpan timeout) => UniTask.CompletedTask;
public void Dispose() { }
}
internal sealed class ShrinkMainThreadScheduler : IShrinkBusScheduler
{
private readonly ShrinkSchedulerQueue _queue;
private int _pumpScheduled;
private int _disposed;
public ShrinkMainThreadScheduler(string name, ShrinkBusOptions options)
{
_queue = new ShrinkSchedulerQueue(name, options);
}
public bool IsOnSchedulerThread => PlayerLoopHelper.IsMainThread;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (IsOnSchedulerThread)
{
action();
return true;
}
var item = new ShrinkSchedulerWorkItem(() =>
{
action();
return UniTask.CompletedTask;
}, awaitable: false);
if (!_queue.TryEnqueue(item))
return false;
SchedulePump();
return true;
}
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(ShrinkMainThreadScheduler));
cancellationToken.ThrowIfCancellationRequested();
if (IsOnSchedulerThread)
{
await action();
return;
}
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
await _queue.EnqueueAsync(item, cancellationToken);
SchedulePump();
await item.Completion.AttachExternalCancellation(cancellationToken);
}
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
{
Interlocked.Exchange(ref _disposed, 1);
if (!drain)
_queue.DropPending();
else
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
_queue, () => Volatile.Read(ref _pumpScheduled) != 0, timeout);
_queue.Dispose();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.Dispose();
}
private void SchedulePump()
{
if (Interlocked.Exchange(ref _pumpScheduled, 1) != 0)
return;
UniTask.Void(PumpAsync);
}
private async UniTaskVoid PumpAsync()
{
await UniTask.SwitchToMainThread();
try
{
while (_queue.TryDequeue(out var item))
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
}
finally
{
Interlocked.Exchange(ref _pumpScheduled, 0);
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
SchedulePump();
}
}
}
internal sealed class ShrinkTaskPoolScheduler : IShrinkBusScheduler
{
private readonly ShrinkSchedulerQueue _queue;
private readonly int _maxConcurrency;
private int _workers;
private int _disposed;
public ShrinkTaskPoolScheduler(string name, ShrinkBusOptions options)
{
_queue = new ShrinkSchedulerQueue(name, options);
_maxConcurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
}
public bool IsOnSchedulerThread => false;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
var item = new ShrinkSchedulerWorkItem(() =>
{
action();
return UniTask.CompletedTask;
}, awaitable: false);
if (!_queue.TryEnqueue(item))
return false;
ScheduleWorkers();
return true;
}
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(ShrinkTaskPoolScheduler));
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
await _queue.EnqueueAsync(item, cancellationToken);
ScheduleWorkers();
await item.Completion.AttachExternalCancellation(cancellationToken);
}
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
{
Interlocked.Exchange(ref _disposed, 1);
if (!drain)
_queue.DropPending();
else
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
_queue, () => Volatile.Read(ref _workers) != 0, timeout);
_queue.Dispose();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.Dispose();
}
private void ScheduleWorkers()
{
while (!_queue.IsEmpty)
{
var current = Volatile.Read(ref _workers);
if (current >= _maxConcurrency ||
Interlocked.CompareExchange(ref _workers, current + 1, current) != current)
return;
UniTask.Void(WorkerAsync);
}
}
private async UniTaskVoid WorkerAsync()
{
await UniTask.SwitchToThreadPool();
try
{
while (_queue.TryDequeue(out var item))
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
}
finally
{
Interlocked.Decrement(ref _workers);
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
ScheduleWorkers();
}
}
}
internal sealed class ShrinkDedicatedThreadScheduler : IShrinkBusScheduler
{
private readonly ShrinkSchedulerQueue _queue;
private readonly AutoResetEvent _signal = new(false);
private readonly ManualResetEventSlim _stopped = new(false);
private readonly Thread _thread;
private int _disposed;
private int _threadId;
private int _shutdownTimedOut;
private int _waitHandlesDisposed;
public ShrinkDedicatedThreadScheduler(string name, ShrinkBusOptions options)
{
_queue = new ShrinkSchedulerQueue(name, options);
_thread = new Thread(Run)
{
IsBackground = true,
Name = $"ShrinkBus:{name}"
};
_thread.Start();
}
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == Volatile.Read(ref _threadId);
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (IsOnSchedulerThread)
{
action();
return true;
}
var item = new ShrinkSchedulerWorkItem(() =>
{
action();
return UniTask.CompletedTask;
}, awaitable: false);
if (!_queue.TryEnqueue(item))
return false;
_signal.Set();
return true;
}
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0)
throw new ObjectDisposedException(nameof(ShrinkDedicatedThreadScheduler));
if (IsOnSchedulerThread)
{
await action();
return;
}
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
await _queue.EnqueueAsync(item, cancellationToken);
_signal.Set();
await item.Completion.AttachExternalCancellation(cancellationToken);
}
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
{
Interlocked.Exchange(ref _disposed, 1);
if (!drain)
_queue.DropPending();
_signal.Set();
var waitMs = timeout == Timeout.InfiniteTimeSpan
? Timeout.Infinite
: Math.Max(0, (int)Math.Min(int.MaxValue, timeout.TotalMilliseconds));
var stopped = await UniTask.RunOnThreadPool(() => _stopped.Wait(waitMs));
if (!stopped)
{
_queue.DropPending();
Volatile.Write(ref _shutdownTimedOut, 1);
try
{
if (_stopped.IsSet)
DisposeWaitHandles();
}
catch (ObjectDisposedException)
{
}
return;
}
DisposeWaitHandles();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.DropPending();
Volatile.Write(ref _shutdownTimedOut, 1);
_signal.Set();
}
private void Run()
{
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
try
{
while (Volatile.Read(ref _disposed) == 0 || !_queue.IsEmpty)
{
if (!_queue.TryDequeue(out var item))
{
_signal.WaitOne(50);
continue;
}
ShrinkSchedulerWorkItemRunner.RunBlocking(item);
}
}
finally
{
_stopped.Set();
if (Volatile.Read(ref _shutdownTimedOut) != 0)
DisposeWaitHandles();
}
}
private void DisposeWaitHandles()
{
if (Interlocked.Exchange(ref _waitHandlesDisposed, 1) != 0)
return;
_queue.Dispose();
_signal.Dispose();
_stopped.Dispose();
}
}
internal static class ShrinkEventDiagnostics
{
public static void LogException(Exception exception)
{
#if UNITY_5_3_OR_NEWER
UnityEngine.Debug.LogException(exception);
#else
Trace.TraceError(exception.ToString());
#endif
}
}
internal static class ShrinkSchedulerShutdown
{
public static async UniTask WaitUntilDrainedAsync(ShrinkSchedulerQueue queue,
Func<bool> hasActiveWork, TimeSpan timeout)
{
var started = Stopwatch.StartNew();
while (!queue.IsEmpty || hasActiveWork())
{
if (timeout != Timeout.InfiniteTimeSpan && started.Elapsed >= timeout)
{
queue.DropPending();
return;
}
await UniTask.Delay(1, ignoreTimeScale: true);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d5885f09c9a4de89cd95de44433bb17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+47
View File
@@ -0,0 +1,47 @@
using System;
namespace ShrinkEventBus
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkEventSubscriberAttribute : Attribute
{
public string OwnerId { get; set; } = string.Empty;
public string DefaultBus { get; set; } = string.Empty;
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class ShrinkSubscribeAttribute : Attribute
{
public string Bus { get; set; } = string.Empty;
public ShrinkEventPriority Priority { get; set; } = ShrinkEventPriority.Normal;
public int NumericPriority { get; set; }
public bool ReceiveCanceled { get; set; }
public ShrinkSubscribeAttribute() { }
public ShrinkSubscribeAttribute(ShrinkEventPriority priority, bool receiveCanceled = false)
{
Priority = priority;
ReceiveCanceled = receiveCanceled;
}
public ShrinkSubscribeAttribute(int priority)
{
NumericPriority = priority;
Priority = priority switch
{
>= 100 => ShrinkEventPriority.Highest,
>= 50 => ShrinkEventPriority.High,
>= 0 => ShrinkEventPriority.Normal,
>= -50 => ShrinkEventPriority.Low,
_ => ShrinkEventPriority.Lowest
};
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
public sealed class ShrinkEventAttribute : Attribute
{
public ShrinkDispatchMode Dispatch { get; set; } = ShrinkDispatchMode.Ordered;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32e8ae06929942288c57cd8c89b3d3a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+293
View File
@@ -0,0 +1,293 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public interface IShrinkBusResolver
{
IShrinkEventBus GetBus(ShrinkBusKey key);
bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus);
}
public interface IShrinkGeneratedSubscriber
{
IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null);
}
public sealed class ShrinkEventBinding : IDisposable
{
private readonly List<IDisposable> _items = new();
private bool _disposed;
public void Add(IDisposable subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
if (_disposed)
{
subscription.Dispose();
throw new ObjectDisposedException(nameof(ShrinkEventBinding));
}
_items.Add(subscription);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
for (var i = _items.Count - 1; i >= 0; i--)
_items[i].Dispose();
_items.Clear();
}
}
public static class ShrinkStaticBindingRegistry
{
private sealed class Entry
{
public long Id;
public ShrinkBusKey Key;
public Func<IShrinkBusResolver, IDisposable> Factory = null!;
}
private static readonly object Gate = new();
private static readonly List<Entry> Entries = new();
private static readonly Dictionary<long, IDisposable> Bindings = new();
private static long _nextId;
public static void Register(ShrinkBusKey key,
Func<IShrinkBusResolver, IDisposable> factory)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory));
lock (Gate)
{
var entry = new Entry
{
Id = Interlocked.Increment(ref _nextId),
Key = key,
Factory = factory
};
Entries.Add(entry);
TryAttachLocked(entry, EventBus.Resolver);
}
}
public static void Register<TEvent>(string bus, Action<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
where TEvent : IShrinkEvent
{
var key = ShrinkBusKey.Parse(bus);
Register(key, resolver => ShrinkGeneratedBinding.Subscribe(
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
}
public static void RegisterAsync<TEvent>(string bus, ShrinkAsyncEventHandler<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
where TEvent : IShrinkEvent
{
var key = ShrinkBusKey.Parse(bus);
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsync(
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
}
public static void RegisterAsyncLegacy<TEvent>(string bus, Func<TEvent, UniTask> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
where TEvent : IShrinkEvent
{
var key = ShrinkBusKey.Parse(bus);
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsyncLegacy(
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
}
internal static void AttachForBus(ShrinkBusKey key, IShrinkBusResolver resolver)
{
lock (Gate)
{
for (var i = 0; i < Entries.Count; i++)
{
if (Entries[i].Key == key)
TryAttachLocked(Entries[i], resolver);
}
}
}
internal static void DetachBus(ShrinkBusKey key)
{
lock (Gate)
{
for (var i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
if (entry.Key != key || !Bindings.TryGetValue(entry.Id, out var binding))
continue;
Bindings.Remove(entry.Id);
binding.Dispose();
}
}
}
private static void TryAttachLocked(Entry entry, IShrinkBusResolver resolver)
{
if (Bindings.ContainsKey(entry.Id) || !resolver.TryGetBus(entry.Key, out _))
return;
Bindings.Add(entry.Id, entry.Factory(resolver));
}
}
public sealed class ShrinkEventBusHost : IShrinkBusResolver, IDisposable
{
private readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> _buses = new();
private bool _disposed;
public IEnumerable<IShrinkEventBus> Buses => _buses.Values;
public IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
if (options == null)
throw new ArgumentNullException(nameof(options));
var bus = new ShrinkEventBusBuilder()
.WithKey(key)
.WithOptions(options)
.Build();
if (!_buses.TryAdd(key, bus))
{
if (bus is IDisposable disposable)
disposable.Dispose();
throw new InvalidOperationException($"Bus '{key}' is already registered.");
}
return bus;
}
public IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, Func<ShrinkBusOptions> optionsFactory)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
if (optionsFactory == null)
throw new ArgumentNullException(nameof(optionsFactory));
return _buses.GetOrAdd(key, busKey =>
new ShrinkEventBusBuilder()
.WithKey(busKey)
.WithOptions(optionsFactory())
.Build());
}
public IShrinkEventBus GetBus(ShrinkBusKey key)
{
if (_buses.TryGetValue(key, out var bus))
return bus;
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
}
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) => _buses.TryGetValue(key, out bus!);
public IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target is IShrinkGeneratedSubscriber generated)
return generated.AttachGenerated(this, defaultBus);
var bus = GetBus(defaultBus ?? ShrinkBusKey.Game);
return bus.Attach(target);
}
public bool RemoveBus(ShrinkBusKey key)
{
if (!_buses.TryRemove(key, out var bus))
return false;
if (bus is IDisposable disposable)
disposable.Dispose();
return true;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
foreach (var bus in _buses.Values)
{
if (bus is IDisposable disposable)
disposable.Dispose();
}
_buses.Clear();
}
public async UniTask ShutdownAsync()
{
if (_disposed)
return;
_disposed = true;
foreach (var bus in _buses.Values)
{
if (bus is ShrinkEventBusInstance instance)
await instance.ShutdownAsync();
else
bus.Dispose();
}
_buses.Clear();
}
}
public static class ShrinkGeneratedBinding
{
public static IShrinkBusResolver RuntimeResolver => EventBus.Resolver;
public static IDisposable Subscribe<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus, object? owner, Action<TEvent> handler, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
var bus = ResolveBus(resolver, defaultBus, configuredBus);
if (bus is not ShrinkEventBusInstance instance)
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
return instance.SubscribeGenerated(handler,
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
}
public static IDisposable SubscribeAsync<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus, object? owner, ShrinkAsyncEventHandler<TEvent> handler,
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
var bus = ResolveBus(resolver, defaultBus, configuredBus);
if (bus is not ShrinkEventBusInstance instance)
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
return instance.SubscribeGenerated(handler,
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
}
public static IDisposable SubscribeAsyncLegacy<TEvent>(IShrinkBusResolver resolver,
ShrinkBusKey? defaultBus, string? configuredBus, object? owner,
Func<TEvent, UniTask> handler, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
return SubscribeAsync<TEvent>(resolver, defaultBus, configuredBus, owner,
(eventData, _) => handler(eventData), priority, numericPriority, receiveCanceled);
}
private static IShrinkEventBus ResolveBus(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
string? configuredBus)
{
if (resolver == null)
throw new ArgumentNullException(nameof(resolver));
var key = !string.IsNullOrWhiteSpace(configuredBus)
? ShrinkBusKey.Parse(configuredBus)
: defaultBus ?? ShrinkBusKey.Game;
return resolver.GetBus(key);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f19286886fb64b3ba7bfbd010c540913
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ShrinkEventBus.Runtime",
"rootNamespace": "ShrinkEventBus",
"references": [
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f9a7e109b2ecd4946ba9202d2288af15
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+36
View File
@@ -0,0 +1,36 @@
#nullable enable
using System;
namespace ShrinkEventBus
{
public sealed class ShrinkEventBusBuilder
{
internal ShrinkBusKey Key { get; private set; } = ShrinkBusKey.Game;
internal ShrinkBusOptions Options { get; private set; } = ShrinkBusOptions.Inline();
internal Action<IShrinkEvent, Type, ShrinkBusKey>? PostObserver { get; private set; }
public ShrinkEventBusBuilder WithKey(ShrinkBusKey key)
{
Key = key;
return this;
}
public ShrinkEventBusBuilder WithOptions(ShrinkBusOptions options)
{
Options = (options ?? throw new ArgumentNullException(nameof(options))).CloneValidated();
return this;
}
public IShrinkEventBus Build() => Options.Scheduler == ShrinkBusSchedulerKind.Inline
? new ShrinkInlineEventBusInstance(this)
: new ShrinkEventBusInstance(this);
internal ShrinkEventBusBuilder WithPostObserver(
Action<IShrinkEvent, Type, ShrinkBusKey> observer)
{
PostObserver = observer ?? throw new ArgumentNullException(nameof(observer));
return this;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aad8b06ecb53b8a4998fd9944673e537
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+272
View File
@@ -0,0 +1,272 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
public delegate UniTask ShrinkAsyncEventHandler<in TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent;
internal interface IShrinkEventChannel
{
int Count { get; }
bool RemoveSubscription(long subscriptionId);
void Clear();
void DetachSlot(int slot);
}
internal static class ShrinkEventChannelSlots<TEvent> where TEvent : IShrinkEvent
{
private static readonly object Gate = new();
private static ShrinkEventChannel<TEvent>?[] _slots =
Array.Empty<ShrinkEventChannel<TEvent>?>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ShrinkEventChannel<TEvent>? Get(int slot)
{
var snapshot = _slots;
return (uint)slot < (uint)snapshot.Length ? snapshot[slot] : null;
}
public static void Set(int slot, ShrinkEventChannel<TEvent> channel)
{
lock (Gate)
{
var current = _slots;
var length = current.Length;
if (length <= slot)
{
length = Math.Max(4, length);
while (length <= slot)
length *= 2;
}
var next = new ShrinkEventChannel<TEvent>?[length];
Array.Copy(current, next, current.Length);
next[slot] = channel;
Volatile.Write(ref _slots, next);
}
}
public static void Clear(int slot, ShrinkEventChannel<TEvent> channel)
{
lock (Gate)
{
var current = _slots;
if ((uint)slot >= (uint)current.Length || !ReferenceEquals(current[slot], channel))
return;
var next = (ShrinkEventChannel<TEvent>?[])current.Clone();
next[slot] = null;
Volatile.Write(ref _slots, next);
}
}
}
internal sealed class ShrinkEventChannel<TEvent> : IShrinkEventChannel
where TEvent : IShrinkEvent
{
private static readonly bool SupportsCancellation =
typeof(IShrinkCancelableEvent).IsAssignableFrom(typeof(TEvent));
private sealed class HandlerEntry
{
public long SubscriptionId;
public long RegistrationOrder;
public Action<TEvent>? SyncHandler;
public ShrinkAsyncEventHandler<TEvent>? AsyncHandler;
public ShrinkSubscribeDescriptor Descriptor;
}
private readonly object _gate = new();
private readonly List<HandlerEntry> _entries = new();
private HandlerEntry[] _snapshot = Array.Empty<HandlerEntry>();
private Action<TEvent>? _syncDispatcher;
public int Count => _snapshot.Length;
public void Add(long subscriptionId, long registrationOrder, Action<TEvent> handler,
ShrinkSubscribeDescriptor descriptor)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (_gate)
{
_entries.Add(new HandlerEntry
{
SubscriptionId = subscriptionId,
RegistrationOrder = registrationOrder,
SyncHandler = handler,
Descriptor = descriptor
});
RebuildSnapshot();
}
}
public void Add(long subscriptionId, long registrationOrder,
ShrinkAsyncEventHandler<TEvent> handler, ShrinkSubscribeDescriptor descriptor)
{
if (handler == null)
throw new ArgumentNullException(nameof(handler));
lock (_gate)
{
_entries.Add(new HandlerEntry
{
SubscriptionId = subscriptionId,
RegistrationOrder = registrationOrder,
AsyncHandler = handler,
Descriptor = descriptor
});
RebuildSnapshot();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ShrinkPostResult Post(in TEvent eventData)
{
var syncDispatcher = _syncDispatcher;
if (syncDispatcher != null)
{
syncDispatcher(eventData);
return new ShrinkPostResult(true, true, false);
}
var snapshot = _snapshot;
var handled = false;
for (var i = 0; i < snapshot.Length; i++)
{
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
continue;
if (entry.SyncHandler != null)
entry.SyncHandler(eventData);
else if (entry.AsyncHandler != null)
entry.AsyncHandler(eventData, CancellationToken.None).Forget(ShrinkEventDiagnostics.LogException);
else
continue;
handled = true;
}
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
}
public async UniTask<ShrinkPostResult> PostAsync(TEvent eventData, ShrinkDispatchMode dispatchMode,
CancellationToken cancellationToken)
{
var snapshot = _snapshot;
var handled = false;
if (dispatchMode == ShrinkDispatchMode.Parallel)
{
var tasks = new List<UniTask>(snapshot.Length);
for (var i = 0; i < snapshot.Length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
continue;
if (entry.SyncHandler != null)
entry.SyncHandler(eventData);
else if (entry.AsyncHandler != null)
tasks.Add(entry.AsyncHandler(eventData, cancellationToken));
else
continue;
handled = true;
}
if (tasks.Count > 0)
await UniTask.WhenAll(tasks);
}
else
{
for (var i = 0; i < snapshot.Length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var entry = snapshot[i];
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
continue;
if (entry.SyncHandler != null)
entry.SyncHandler(eventData);
else if (entry.AsyncHandler != null)
await entry.AsyncHandler(eventData, cancellationToken);
else
continue;
handled = true;
}
}
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
}
public bool RemoveSubscription(long subscriptionId)
{
lock (_gate)
{
var removed = _entries.RemoveAll(entry => entry.SubscriptionId == subscriptionId) > 0;
RebuildSnapshot();
return removed;
}
}
public void Clear()
{
lock (_gate)
{
_entries.Clear();
_snapshot = Array.Empty<HandlerEntry>();
_syncDispatcher = null;
}
}
public void DetachSlot(int slot) => ShrinkEventChannelSlots<TEvent>.Clear(slot, this);
private void RebuildSnapshot()
{
_entries.Sort(static (left, right) =>
{
var leftOrder = left.Descriptor.NumericPriority == 0
? (int)left.Descriptor.Priority * 1000
: -left.Descriptor.NumericPriority;
var rightOrder = right.Descriptor.NumericPriority == 0
? (int)right.Descriptor.Priority * 1000
: -right.Descriptor.NumericPriority;
var priority = leftOrder.CompareTo(rightOrder);
return priority != 0
? priority
: left.RegistrationOrder.CompareTo(right.RegistrationOrder);
});
_snapshot = _entries.ToArray();
Action<TEvent>? dispatcher = null;
if (!SupportsCancellation)
{
for (var i = 0; i < _snapshot.Length; i++)
{
var handler = _snapshot[i].SyncHandler;
if (handler == null)
{
dispatcher = null;
break;
}
dispatcher += handler;
}
}
Volatile.Write(ref _syncDispatcher, dispatcher);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsCanceled(TEvent eventData)
{
if (!SupportsCancellation)
return false;
return ((IShrinkCancelableEvent)(object)eventData!).IsCanceled;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ffae3efb7664efcb82f2bbefbdd5a4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+392
View File
@@ -0,0 +1,392 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace ShrinkEventBus
{
internal class ShrinkEventBusInstance : IShrinkEventBus
{
private sealed class SingleBusResolver : IShrinkBusResolver
{
private readonly IShrinkEventBus _bus;
public SingleBusResolver(IShrinkEventBus bus) => _bus = bus;
public IShrinkEventBus GetBus(ShrinkBusKey key) => key == _bus.Key
? _bus
: throw new KeyNotFoundException($"Bus '{key}' is not available from '{_bus.Key}'.");
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus)
{
bus = _bus;
return key == _bus.Key;
}
}
private sealed class EventSubscription : IDisposable
{
private ShrinkEventBusInstance? _owner;
private readonly long _subscriptionId;
public EventSubscription(ShrinkEventBusInstance owner, long subscriptionId)
{
_owner = owner;
_subscriptionId = subscriptionId;
}
public void Dispose()
{
Interlocked.Exchange(ref _owner, null)?.UnregisterSubscription(_subscriptionId);
}
}
private static int _nextBusSlot;
private readonly Dictionary<Type, IShrinkEventChannel> _channels = new();
private readonly object _channelGate = new();
private readonly IShrinkBusScheduler _scheduler;
private readonly Action<IShrinkEvent, Type, ShrinkBusKey>? _postObserver;
private readonly int _busSlot;
private readonly bool _isInline;
private long _nextSubscriptionId;
private int _subscriberCount;
private int _disposed;
public ShrinkEventBusInstance(ShrinkEventBusBuilder builder)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
Key = builder.Key;
Options = builder.Options.CloneValidated();
_busSlot = Interlocked.Increment(ref _nextBusSlot);
_scheduler = ShrinkBusSchedulerFactory.Create(Options, Key.Id);
_postObserver = builder.PostObserver;
_isInline = Options.Scheduler == ShrinkBusSchedulerKind.Inline;
}
public ShrinkBusKey Key { get; }
public ShrinkBusOptions Options { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual ShrinkPostResult Post<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (Volatile.Read(ref _disposed) != 0)
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), false);
if (_isInline || _scheduler.IsOnSchedulerThread)
return Dispatch(in eventData);
var queuedEvent = eventData;
if (_scheduler.TryPost(() => Dispatch(in queuedEvent)))
return new ShrinkPostResult(true, false, false);
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull), false);
}
public virtual UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default) where TEvent : IShrinkEvent
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (Volatile.Read(ref _disposed) != 0)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true));
if (cancellationToken.IsCancellationRequested)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled), true));
if (_isInline || _scheduler.IsOnSchedulerThread)
return DispatchAsync(eventData, cancellationToken);
return PostScheduledAsync(eventData, cancellationToken);
}
private async UniTask<ShrinkPostResult> PostScheduledAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent
{
var result = default(ShrinkPostResult);
var dispatchStarted = false;
try
{
await _scheduler.PostAsync(async () =>
{
dispatchStarted = true;
result = await DispatchAsync(eventData, cancellationToken);
}, cancellationToken);
return result;
}
catch (ShrinkQueueFullException)
{
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.QueueFull), true);
}
catch (ObjectDisposedException)
{
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
var canceled = new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled);
return dispatchStarted ? canceled : TraceWithoutDispatch(in eventData, canceled, true);
}
}
public IDisposable Attach(object target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target is not IShrinkGeneratedSubscriber generated)
throw new InvalidOperationException(
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
return generated.AttachGenerated(new SingleBusResolver(this), Key);
}
internal IDisposable SubscribeGenerated<TEvent>(Action<TEvent> handler,
ShrinkSubscribeDescriptor descriptor) where TEvent : IShrinkEvent
{
var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId);
GetOrCreateChannel<TEvent>().Add(subscriptionId, subscriptionId, handler, descriptor);
Interlocked.Increment(ref _subscriberCount);
return new EventSubscription(this, subscriptionId);
}
internal IDisposable SubscribeGenerated<TEvent>(ShrinkAsyncEventHandler<TEvent> handler,
ShrinkSubscribeDescriptor descriptor) where TEvent : IShrinkEvent
{
var subscriptionId = Interlocked.Increment(ref _nextSubscriptionId);
GetOrCreateChannel<TEvent>().Add(subscriptionId, subscriptionId, handler, descriptor);
Interlocked.Increment(ref _subscriberCount);
return new EventSubscription(this, subscriptionId);
}
internal int SubscriberCount
{
get
{
return Math.Max(0, Volatile.Read(ref _subscriberCount));
}
}
internal async UniTask ShutdownAsync()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
await _scheduler.ShutdownAsync(Options.DrainOnShutdown, Options.ShutdownTimeout);
ClearChannels();
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_scheduler.Dispose();
ClearChannels();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected ShrinkPostResult Dispatch<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
return DispatchObserved(in eventData);
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
var result = channel == null
? new ShrinkPostResult(true, false, false)
: channel.Post(in eventData);
if (_postObserver != null && EventBus.HasPostedObservers)
_postObserver(eventData, typeof(TEvent), Key);
return result;
}
protected async UniTask<ShrinkPostResult> DispatchAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent
{
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
return await DispatchAsyncObserved(eventData, cancellationToken);
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
var result = channel == null
? ShrinkPostResult.Completed(false)
: await channel.PostAsync(eventData, Options.DispatchMode, cancellationToken);
if (_postObserver != null && EventBus.HasPostedObservers)
_postObserver(eventData, typeof(TEvent), Key);
return result;
}
private ShrinkPostResult DispatchObserved<TEvent>(in TEvent eventData) where TEvent : IShrinkEvent
{
var startedAt = Stopwatch.GetTimestamp();
var threadId = Thread.CurrentThread.ManagedThreadId;
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
ShrinkPostResult result;
try
{
result = channel == null
? new ShrinkPostResult(true, false, false)
: channel.Post(in eventData);
}
catch
{
var failedResult = new ShrinkPostResult(true, channel != null,
eventData is IShrinkCancelableEvent cancelable && cancelable.IsCanceled,
ShrinkPostFailure.HandlerException);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, Stopwatch.GetTimestamp() - startedAt, failedResult, false));
throw;
}
var elapsedTicks = Stopwatch.GetTimestamp() - startedAt;
if (EventBus.HasPostedObservers)
_postObserver!(eventData, typeof(TEvent), Key);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, elapsedTicks, result, false));
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected ShrinkPostResult TraceWithoutDispatch<TEvent>(in TEvent eventData,
ShrinkPostResult result, bool isAsync) where TEvent : IShrinkEvent
{
if (_postObserver != null && EventBus.HasDetailedPostedObservers)
{
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, Thread.CurrentThread.ManagedThreadId, 0L, result, isAsync));
}
return result;
}
private async UniTask<ShrinkPostResult> DispatchAsyncObserved<TEvent>(TEvent eventData,
CancellationToken cancellationToken) where TEvent : IShrinkEvent
{
var startedAt = Stopwatch.GetTimestamp();
var threadId = Thread.CurrentThread.ManagedThreadId;
var channel = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
ShrinkPostResult result;
try
{
result = channel == null
? ShrinkPostResult.Completed(false)
: await channel.PostAsync(eventData, Options.DispatchMode, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
var canceledResult = new ShrinkPostResult(true, channel != null, false,
ShrinkPostFailure.Canceled);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, Stopwatch.GetTimestamp() - startedAt, canceledResult, true));
throw;
}
catch
{
var failedResult = new ShrinkPostResult(true, channel != null,
eventData is IShrinkCancelableEvent cancelable && cancelable.IsCanceled,
ShrinkPostFailure.HandlerException);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, Stopwatch.GetTimestamp() - startedAt, failedResult, true));
throw;
}
var elapsedTicks = Stopwatch.GetTimestamp() - startedAt;
if (EventBus.HasPostedObservers)
_postObserver!(eventData, typeof(TEvent), Key);
EventBus.NotifyDetailedPosted(new ShrinkEventTrace(DateTime.UtcNow, typeof(TEvent), Key,
Options, threadId, elapsedTicks, result, true));
return result;
}
private ShrinkEventChannel<TEvent> GetOrCreateChannel<TEvent>() where TEvent : IShrinkEvent
{
var existing = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
if (existing != null)
return existing;
lock (_channelGate)
{
existing = ShrinkEventChannelSlots<TEvent>.Get(_busSlot);
if (existing != null)
return existing;
var channel = new ShrinkEventChannel<TEvent>();
_channels.Add(typeof(TEvent), channel);
ShrinkEventChannelSlots<TEvent>.Set(_busSlot, channel);
return channel;
}
}
private void UnregisterSubscription(long subscriptionId)
{
lock (_channelGate)
{
foreach (var channel in _channels.Values)
{
if (channel.RemoveSubscription(subscriptionId))
{
Interlocked.Decrement(ref _subscriberCount);
break;
}
}
}
}
private void ClearChannels()
{
lock (_channelGate)
{
foreach (var channel in _channels.Values)
{
channel.DetachSlot(_busSlot);
channel.Clear();
}
_channels.Clear();
Volatile.Write(ref _subscriberCount, 0);
}
}
protected bool IsDisposed
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Volatile.Read(ref _disposed) != 0;
}
}
internal sealed class ShrinkInlineEventBusInstance : ShrinkEventBusInstance
{
public ShrinkInlineEventBusInstance(ShrinkEventBusBuilder builder) : base(builder)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override ShrinkPostResult Post<TEvent>(in TEvent eventData)
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (IsDisposed)
return TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), false);
return Dispatch(in eventData);
}
public override UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
CancellationToken cancellationToken = default)
{
if (eventData is null)
throw new ArgumentNullException(nameof(eventData));
if (IsDisposed)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
ShrinkPostResult.Rejected(ShrinkPostFailure.BusStopped), true));
if (cancellationToken.IsCancellationRequested)
return UniTask.FromResult(TraceWithoutDispatch(in eventData,
new ShrinkPostResult(true, false, false, ShrinkPostFailure.Canceled), true));
return DispatchAsync(eventData, cancellationToken);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92c1c7cd96e37c8458600728c7c0c57f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+213
View File
@@ -0,0 +1,213 @@
#nullable enable
using System;
using System.Runtime.CompilerServices;
namespace ShrinkEventBus
{
/// <summary>Marker shared by managed, Unity and ECS event payloads.</summary>
public interface IShrinkEvent
{
}
public interface IShrinkCancelableEvent : IShrinkEvent
{
bool IsCanceled { get; }
void SetCanceled(bool value);
}
public interface IShrinkResultEvent<TResult> : IShrinkEvent
{
TResult Result { get; }
void SetResult(TResult result);
}
public enum ShrinkEventPriority
{
Highest = 0,
High = 1,
Normal = 2,
Low = 3,
Lowest = 4,
Monitor = 5
}
public enum ShrinkDispatchMode
{
Ordered = 0,
Parallel = 1
}
public enum ShrinkBusSchedulerKind
{
Inline = 0,
MainThread = 1,
DedicatedThread = 2,
TaskPool = 3
}
public enum ShrinkQueueOverflowPolicy
{
Reject = 0,
DropNewest = 1,
DropOldest = 2,
Wait = 3
}
public enum ShrinkPostFailure
{
None = 0,
BusStopped = 1,
QueueFull = 2,
Canceled = 3,
HandlerException = 4,
InvalidEvent = 5
}
public readonly struct ShrinkPostResult
{
private const int AcceptedMask = 1 << 0;
private const int HandledMask = 1 << 1;
private const int CanceledMask = 1 << 2;
private const int FailureShift = 8;
private readonly int _value;
public ShrinkPostResult(bool accepted, bool handled, bool canceled,
ShrinkPostFailure failure = ShrinkPostFailure.None)
{
_value = (accepted ? AcceptedMask : 0) |
(handled ? HandledMask : 0) |
(canceled ? CanceledMask : 0) |
((int)failure << FailureShift);
}
public bool Accepted => (_value & AcceptedMask) != 0;
public bool Handled => (_value & HandledMask) != 0;
public bool Canceled => (_value & CanceledMask) != 0;
public ShrinkPostFailure Failure => (ShrinkPostFailure)((uint)_value >> FailureShift);
public bool Succeeded => Accepted && Failure == ShrinkPostFailure.None;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ShrinkPostResult Rejected(ShrinkPostFailure failure) =>
new(false, false, false, failure);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ShrinkPostResult Completed(bool handled, bool canceled = false) =>
new(true, handled, canceled);
}
public readonly struct ShrinkBusKey : IEquatable<ShrinkBusKey>
{
public ShrinkBusKey(string scope, string name)
{
if (string.IsNullOrWhiteSpace(scope))
throw new ArgumentException("Bus scope must not be empty.", nameof(scope));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Bus name must not be empty.", nameof(name));
Scope = scope.Trim().ToLowerInvariant();
Name = name.Trim();
}
public string Scope { get; }
public string Name { get; }
public string Id => Scope == "game" && Name == "default" ? "game" : $"{Scope}:{Name}";
public static ShrinkBusKey Game => new("game", "default");
public static ShrinkBusKey Server => new("server", "default");
public static ShrinkBusKey Scene(string sceneId) => new("scene", sceneId);
public static ShrinkBusKey Mod(string modId) => new("mod", modId);
public static ShrinkBusKey World(string worldId) => new("world", worldId);
public static ShrinkBusKey Parse(string? value)
{
if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "game", StringComparison.OrdinalIgnoreCase))
return Game;
var normalized = value.Trim();
var separator = normalized.IndexOf(':');
return separator <= 0 || separator == normalized.Length - 1
? new ShrinkBusKey("custom", normalized)
: new ShrinkBusKey(normalized.Substring(0, separator), normalized.Substring(separator + 1));
}
public bool Equals(ShrinkBusKey other) =>
string.Equals(Scope, other.Scope, StringComparison.OrdinalIgnoreCase) &&
string.Equals(Name, other.Name, StringComparison.Ordinal);
public override bool Equals(object? obj) => obj is ShrinkBusKey other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Scope.ToLowerInvariant(), Name);
public override string ToString() => Id;
public static bool operator ==(ShrinkBusKey left, ShrinkBusKey right) => left.Equals(right);
public static bool operator !=(ShrinkBusKey left, ShrinkBusKey right) => !left.Equals(right);
}
public sealed class ShrinkBusOptions
{
public ShrinkBusSchedulerKind Scheduler { get; set; } = ShrinkBusSchedulerKind.Inline;
public ShrinkDispatchMode DispatchMode { get; set; } = ShrinkDispatchMode.Ordered;
public int QueueCapacity { get; set; } = 4096;
public int MaxConcurrency { get; set; } = Math.Max(1, Environment.ProcessorCount);
public ShrinkQueueOverflowPolicy OverflowPolicy { get; set; } = ShrinkQueueOverflowPolicy.Reject;
public bool DrainOnShutdown { get; set; } = true;
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
public static ShrinkBusOptions Inline(ShrinkDispatchMode mode = ShrinkDispatchMode.Ordered) =>
new() { Scheduler = ShrinkBusSchedulerKind.Inline, DispatchMode = mode };
public static ShrinkBusOptions MainThread(int queueCapacity = 4096) =>
new() { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = queueCapacity };
public static ShrinkBusOptions DedicatedThread(int queueCapacity = 4096) =>
new() { Scheduler = ShrinkBusSchedulerKind.DedicatedThread, QueueCapacity = queueCapacity };
public static ShrinkBusOptions TaskPool(int maxConcurrency = 0, int queueCapacity = 4096) =>
new()
{
Scheduler = ShrinkBusSchedulerKind.TaskPool,
DispatchMode = ShrinkDispatchMode.Parallel,
QueueCapacity = queueCapacity,
MaxConcurrency = maxConcurrency > 0 ? maxConcurrency : Math.Max(1, Environment.ProcessorCount)
};
internal ShrinkBusOptions CloneValidated()
{
if (QueueCapacity <= 0)
throw new ArgumentOutOfRangeException(nameof(QueueCapacity));
if (MaxConcurrency <= 0)
throw new ArgumentOutOfRangeException(nameof(MaxConcurrency));
if (ShutdownTimeout < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(ShutdownTimeout));
if (OverflowPolicy == ShrinkQueueOverflowPolicy.Wait && Scheduler == ShrinkBusSchedulerKind.Inline)
throw new InvalidOperationException("Inline buses do not have a queue and cannot use Wait overflow policy.");
return new ShrinkBusOptions
{
Scheduler = Scheduler,
DispatchMode = DispatchMode,
QueueCapacity = QueueCapacity,
MaxConcurrency = MaxConcurrency,
OverflowPolicy = OverflowPolicy,
DrainOnShutdown = DrainOnShutdown,
ShutdownTimeout = ShutdownTimeout
};
}
}
public readonly struct ShrinkSubscribeDescriptor
{
public ShrinkSubscribeDescriptor(string? bus, ShrinkEventPriority priority,
int numericPriority, bool receiveCanceled)
{
Bus = bus ?? string.Empty;
Priority = priority;
NumericPriority = numericPriority;
ReceiveCanceled = receiveCanceled;
}
public string Bus { get; }
public ShrinkEventPriority Priority { get; }
public int NumericPriority { get; }
public bool ReceiveCanceled { get; }
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 04b409c00e3144fd8d9f3efb7c2901b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+71
View File
@@ -0,0 +1,71 @@
#nullable enable
using System;
using UnityEngine;
namespace ShrinkEventBus
{
/// <summary>
/// Optional Unity lifecycle host for generated event bindings. It does not require
/// the target component to inherit from an SDK base class.
/// </summary>
[DisallowMultipleComponent]
public sealed class ShrinkMonoEventScope : MonoBehaviour
{
[SerializeField] private string bus = "game";
[SerializeField] private bool includeChildren;
private IDisposable? _binding;
private ShrinkEventBinding? _bindingGroup;
public string BusId
{
get => bus;
set => bus = value ?? string.Empty;
}
private void OnEnable()
{
RefreshBindings();
}
private void OnDisable()
{
ReleaseBindings();
}
public void RefreshBindings()
{
ReleaseBindings();
var key = ShrinkBusKey.Parse(bus);
if (!EventBus.TryGetBus(key, out var targetBus))
targetBus = EventBus.GetOrCreateBus(key, ShrinkBusOptions.MainThread());
var components = includeChildren
? GetComponentsInChildren<MonoBehaviour>(true)
: GetComponents<MonoBehaviour>();
for (var i = 0; i < components.Length; i++)
TryAttach(components[i], targetBus);
}
public void ReleaseBindings()
{
_binding?.Dispose();
_binding = null;
_bindingGroup = null;
}
private void TryAttach(MonoBehaviour target, IShrinkEventBus targetBus)
{
if (target == null || ReferenceEquals(target, this))
return;
if (target is not IShrinkGeneratedSubscriber)
return;
var binding = targetBus.Attach(target);
if (_bindingGroup == null)
_bindingGroup = new ShrinkEventBinding();
_bindingGroup.Add(binding);
_binding = _bindingGroup;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fe48da79b2f4a10a338e497f4ce5ae1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: