This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
|
||||
[assembly: InternalsVisibleTo("ShrinkEventBus.Editor")]
|
||||
[assembly: InternalsVisibleTo("ShrinkEventBus.Tests")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43e85f78c24f1d94ba7d06a4fa37e937
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b995a65fd1324255a5ade1ae1f0adf32
|
||||
timeCreated: 1760099076
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public enum EventResult
|
||||
{
|
||||
DEFAULT,
|
||||
ALLOW,
|
||||
DENY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5213a68ce8f433a98f272904a5d7bd2
|
||||
timeCreated: 1760098791
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb69f308f5c727542ac602709b02c628
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d5885f09c9a4de89cd95de44433bb17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32e8ae06929942288c57cd8c89b3d3a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f19286886fb64b3ba7bfbd010c540913
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aad8b06ecb53b8a4998fd9944673e537
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ffae3efb7664efcb82f2bbefbdd5a4e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92c1c7cd96e37c8458600728c7c0c57f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04b409c00e3144fd8d9f3efb7c2901b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fe48da79b2f4a10a338e497f4ce5ae1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user