mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-24 20:34:29 +08:00
- 将 HistoryEntries 属性替换为 GetHistoryEntriesSnapshot() 方法,明确表达会返回集合副本 - 在批处理清理逻辑中添加 else 条件避免无效操作 - 更新测试代码使用新的快照获取方法 - 添加 System.Diagnostics 命名空间引用 - 修复批处理深度为零时的异常处理逻辑
1415 lines
51 KiB
C#
1415 lines
51 KiB
C#
using System.Diagnostics;
|
||
using GFramework.Core.Abstractions.Events;
|
||
using GFramework.Core.Abstractions.StateManagement;
|
||
using GFramework.Core.Events;
|
||
|
||
namespace GFramework.Core.StateManagement;
|
||
|
||
/// <summary>
|
||
/// 集中式状态容器的默认实现,用于统一管理复杂状态树的读取、归约和订阅通知。
|
||
/// 该类型定位于现有 BindableProperty 之上的可选能力,适合跨模块共享、需要统一变更入口、
|
||
/// 支持调试历史或需要中间件/诊断能力的状态场景,而不是替代所有简单字段级响应式属性。
|
||
/// </summary>
|
||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||
public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
||
{
|
||
/// <summary>
|
||
/// 当前 Store 使用的 action 匹配策略。
|
||
/// </summary>
|
||
private readonly StoreActionMatchingMode _actionMatchingMode;
|
||
|
||
/// <summary>
|
||
/// Dispatch 串行化门闩。
|
||
/// 该锁保证任意时刻只有一个 action 管线或历史跳转在提交状态,从而保持状态演进顺序确定,
|
||
/// 同时避免让耗时 middleware / reducer 长时间占用状态锁。
|
||
/// </summary>
|
||
private readonly object _dispatchGate = new();
|
||
|
||
/// <summary>
|
||
/// 历史快照缓冲区。
|
||
/// 当容量为 0 时该集合始终为空;启用后会保留当前时间线上的最近若干个状态快照。
|
||
/// </summary>
|
||
private readonly List<StoreHistoryEntry<TState>> _history = [];
|
||
|
||
/// <summary>
|
||
/// 历史缓冲区容量。
|
||
/// 该容量包含当前状态锚点,因此容量越小,可撤销的步数也越少。
|
||
/// </summary>
|
||
private readonly int _historyCapacity;
|
||
|
||
/// <summary>
|
||
/// 当前状态变化订阅者列表。
|
||
/// 使用显式订阅对象而不是委托链,便于处理原子初始化订阅、挂起补发和精确解绑。
|
||
/// </summary>
|
||
private readonly List<ListenerSubscription> _listeners = [];
|
||
|
||
/// <summary>
|
||
/// Store 内部所有可变状态的同步锁。
|
||
/// 该锁仅保护状态快照、订阅集合、缓存选择视图、历史记录和注册表本身的短临界区访问。
|
||
/// </summary>
|
||
private readonly object _lock = new();
|
||
|
||
/// <summary>
|
||
/// 已注册的中间件链,按添加顺序执行。
|
||
/// 每个条目都持有稳定身份,便于通过注销句柄精确移除而不影响其他同类中间件。
|
||
/// Dispatch 开始时会抓取快照,因此运行中的分发不会受到后续注册变化影响。
|
||
/// </summary>
|
||
private readonly List<MiddlewareRegistration> _middlewares = [];
|
||
|
||
/// <summary>
|
||
/// 按 action 注册类型组织的 reducer 注册表。
|
||
/// 默认使用精确类型匹配;启用多态匹配时,会在分发时按确定性的优先级扫描可赋值类型。
|
||
/// </summary>
|
||
private readonly Dictionary<Type, List<ReducerRegistration>> _reducers = [];
|
||
|
||
/// <summary>
|
||
/// 已缓存的局部状态选择视图。
|
||
/// 该缓存用于避免高频访问的 Model 属性在每次 getter 调用时都创建新的选择对象。
|
||
/// </summary>
|
||
private readonly Dictionary<string, object> _selectionCache = [];
|
||
|
||
/// <summary>
|
||
/// 用于判断状态是否发生有效变化的比较器。
|
||
/// </summary>
|
||
private readonly IEqualityComparer<TState> _stateComparer;
|
||
|
||
/// <summary>
|
||
/// 当前批处理嵌套深度。
|
||
/// 大于 0 时会推迟状态通知,直到最外层批处理结束。
|
||
/// </summary>
|
||
private int _batchDepth;
|
||
|
||
/// <summary>
|
||
/// 当前批处理中是否有待发送的最终状态通知。
|
||
/// </summary>
|
||
private bool _hasPendingBatchNotification;
|
||
|
||
/// <summary>
|
||
/// 当前历史游标位置。
|
||
/// 当未启用历史记录时,该值保持为 -1。
|
||
/// </summary>
|
||
private int _historyIndex = -1;
|
||
|
||
/// <summary>
|
||
/// 标记当前 Store 是否正在执行分发。
|
||
/// 该标记用于阻止同一 Store 的重入分发或在 dispatch 中执行历史跳转,避免产生难以推导的执行顺序。
|
||
/// </summary>
|
||
private bool _isDispatching;
|
||
|
||
/// <summary>
|
||
/// 最近一次分发的 action 类型。
|
||
/// </summary>
|
||
private Type? _lastActionType;
|
||
|
||
/// <summary>
|
||
/// 最近一次分发记录。
|
||
/// </summary>
|
||
private StoreDispatchRecord<TState>? _lastDispatchRecord;
|
||
|
||
/// <summary>
|
||
/// 最近一次真正改变状态的时间戳。
|
||
/// </summary>
|
||
private DateTimeOffset? _lastStateChangedAt;
|
||
|
||
/// <summary>
|
||
/// reducer 注册序号。
|
||
/// 该序号用于在多态匹配时为不同注册桶提供跨类型的稳定排序键。
|
||
/// </summary>
|
||
private long _nextReducerSequence;
|
||
|
||
/// <summary>
|
||
/// 当前批处理中最后一个应通知给订阅者的状态快照。
|
||
/// </summary>
|
||
private TState _pendingBatchState = default!;
|
||
|
||
/// <summary>
|
||
/// 当前 Store 持有的状态快照。
|
||
/// </summary>
|
||
private TState _state;
|
||
|
||
/// <summary>
|
||
/// 初始化一个新的 Store。
|
||
/// </summary>
|
||
/// <param name="initialState">Store 的初始状态。</param>
|
||
/// <param name="comparer">状态比较器;未提供时使用 <see cref="EqualityComparer{T}.Default"/>。</param>
|
||
/// <param name="historyCapacity">历史缓冲区容量;0 表示不启用历史记录。</param>
|
||
/// <param name="actionMatchingMode">reducer 的 action 匹配策略。</param>
|
||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="historyCapacity"/> 小于 0 时抛出。</exception>
|
||
public Store(
|
||
TState initialState,
|
||
IEqualityComparer<TState>? comparer = null,
|
||
int historyCapacity = 0,
|
||
StoreActionMatchingMode actionMatchingMode = StoreActionMatchingMode.ExactTypeOnly)
|
||
{
|
||
if (historyCapacity < 0)
|
||
{
|
||
throw new ArgumentOutOfRangeException(nameof(historyCapacity), historyCapacity,
|
||
"History capacity cannot be negative.");
|
||
}
|
||
|
||
_state = initialState;
|
||
_stateComparer = comparer ?? EqualityComparer<TState>.Default;
|
||
_historyCapacity = historyCapacity;
|
||
_actionMatchingMode = actionMatchingMode;
|
||
|
||
if (_historyCapacity > 0)
|
||
{
|
||
ResetHistoryToCurrentState(DateTimeOffset.UtcNow);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前状态快照。
|
||
/// </summary>
|
||
public TState State
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _state;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前是否可以撤销到更早的历史状态。
|
||
/// </summary>
|
||
public bool CanUndo
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _historyCapacity > 0 && _historyIndex > 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前是否可以重做到更晚的历史状态。
|
||
/// </summary>
|
||
public bool CanRedo
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _historyCapacity > 0 && _historyIndex >= 0 && _historyIndex < _history.Count - 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分发一个 action 并按顺序执行匹配的 reducer。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">action 的具体类型。</typeparam>
|
||
/// <param name="action">要分发的 action。</param>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
/// <exception cref="InvalidOperationException">当同一 Store 发生重入分发时抛出。</exception>
|
||
public void Dispatch<TAction>(TAction action)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(action);
|
||
|
||
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
|
||
IStoreMiddleware<TState>[] middlewaresSnapshot = Array.Empty<IStoreMiddleware<TState>>();
|
||
IStoreReducerAdapter[] reducersSnapshot = Array.Empty<IStoreReducerAdapter>();
|
||
IEqualityComparer<TState> stateComparerSnapshot = _stateComparer;
|
||
StoreDispatchContext<TState>? context = null;
|
||
TState notificationState = default!;
|
||
var hasNotification = false;
|
||
var enteredDispatchScope = false;
|
||
|
||
lock (_dispatchGate)
|
||
{
|
||
try
|
||
{
|
||
lock (_lock)
|
||
{
|
||
EnsureNotDispatching();
|
||
_isDispatching = true;
|
||
enteredDispatchScope = true;
|
||
context = new StoreDispatchContext<TState>(action!, _state);
|
||
stateComparerSnapshot = _stateComparer;
|
||
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
|
||
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
|
||
}
|
||
|
||
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
|
||
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
|
||
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
|
||
|
||
lock (_lock)
|
||
{
|
||
_lastActionType = context.ActionType;
|
||
_lastDispatchRecord = new StoreDispatchRecord<TState>(
|
||
context.Action,
|
||
context.PreviousState,
|
||
context.NextState,
|
||
context.HasStateChanged,
|
||
context.DispatchedAt);
|
||
|
||
if (!context.HasStateChanged)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
|
||
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
|
||
hasNotification = listenersSnapshot.Length > 0;
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
if (enteredDispatchScope)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_isDispatching = false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!hasNotification)
|
||
{
|
||
return;
|
||
}
|
||
|
||
NotifyListeners(listenersSnapshot, notificationState);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将当前状态回退到上一个历史点。
|
||
/// </summary>
|
||
/// <exception cref="InvalidOperationException">当历史缓冲区未启用,或当前已经没有可撤销的历史点时抛出。</exception>
|
||
public void Undo()
|
||
{
|
||
MoveToHistoryIndex(-1, isRelative: true, nameof(Undo), "No earlier history entry is available for undo.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将当前状态前进到下一个历史点。
|
||
/// </summary>
|
||
/// <exception cref="InvalidOperationException">当历史缓冲区未启用,或当前已经没有可重做的历史点时抛出。</exception>
|
||
public void Redo()
|
||
{
|
||
MoveToHistoryIndex(1, isRelative: true, nameof(Redo), "No later history entry is available for redo.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 跳转到指定索引的历史点。
|
||
/// </summary>
|
||
/// <param name="historyIndex">目标历史索引,从 0 开始。</param>
|
||
/// <exception cref="InvalidOperationException">当历史缓冲区未启用时抛出。</exception>
|
||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="historyIndex"/> 超出历史范围时抛出。</exception>
|
||
public void TimeTravelTo(int historyIndex)
|
||
{
|
||
MoveToHistoryIndex(historyIndex, isRelative: false, nameof(historyIndex), null);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空当前撤销/重做历史,并以当前状态作为新的历史锚点。
|
||
/// </summary>
|
||
public void ClearHistory()
|
||
{
|
||
lock (_dispatchGate)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
EnsureNotDispatching();
|
||
if (_historyCapacity == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ResetHistoryToCurrentState(DateTimeOffset.UtcNow);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将多个状态操作合并到一个批处理中执行。
|
||
/// 批处理内的状态变化会立即提交,但通知会在最外层批处理结束后折叠为一次最终回放。
|
||
/// </summary>
|
||
/// <param name="batchAction">批处理主体。</param>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="batchAction"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public void RunInBatch(Action batchAction)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(batchAction);
|
||
|
||
lock (_lock)
|
||
{
|
||
_batchDepth++;
|
||
}
|
||
|
||
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
|
||
TState notificationState = default!;
|
||
|
||
try
|
||
{
|
||
batchAction();
|
||
}
|
||
finally
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (_batchDepth == 0)
|
||
{
|
||
Debug.Fail("Batch depth is already zero during RunInBatch cleanup.");
|
||
}
|
||
else
|
||
{
|
||
_batchDepth--;
|
||
if (_batchDepth == 0 && _hasPendingBatchNotification)
|
||
{
|
||
notificationState = _pendingBatchState;
|
||
_pendingBatchState = default!;
|
||
_hasPendingBatchNotification = false;
|
||
listenersSnapshot = SnapshotListenersForNotification(notificationState);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (listenersSnapshot.Length > 0)
|
||
{
|
||
NotifyListeners(listenersSnapshot, notificationState);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 订阅状态变化通知。
|
||
/// </summary>
|
||
/// <param name="listener">状态变化时的监听器。</param>
|
||
/// <returns>用于取消订阅的句柄。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="listener"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public IUnRegister Subscribe(Action<TState> listener)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(listener);
|
||
|
||
var subscription = new ListenerSubscription(listener);
|
||
|
||
lock (_lock)
|
||
{
|
||
_listeners.Add(subscription);
|
||
}
|
||
|
||
return new DefaultUnRegister(() => UnSubscribe(subscription));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 订阅状态变化通知,并立即回放当前状态。
|
||
/// </summary>
|
||
/// <param name="listener">状态变化时的监听器。</param>
|
||
/// <returns>用于取消订阅的句柄。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="listener"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public IUnRegister SubscribeWithInitValue(Action<TState> listener)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(listener);
|
||
|
||
var subscription = new ListenerSubscription(listener)
|
||
{
|
||
IsActive = false
|
||
};
|
||
TState currentState;
|
||
TState? pendingState = default;
|
||
var hasPendingState = false;
|
||
|
||
lock (_lock)
|
||
{
|
||
currentState = _state;
|
||
_listeners.Add(subscription);
|
||
}
|
||
|
||
try
|
||
{
|
||
listener(currentState);
|
||
}
|
||
catch
|
||
{
|
||
UnSubscribe(subscription);
|
||
throw;
|
||
}
|
||
|
||
lock (_lock)
|
||
{
|
||
if (!subscription.IsSubscribed)
|
||
{
|
||
return new DefaultUnRegister(() => { });
|
||
}
|
||
|
||
subscription.IsActive = true;
|
||
if (subscription.HasPendingState)
|
||
{
|
||
pendingState = subscription.PendingState;
|
||
hasPendingState = true;
|
||
subscription.HasPendingState = false;
|
||
subscription.PendingState = default!;
|
||
}
|
||
}
|
||
|
||
if (hasPendingState)
|
||
{
|
||
listener(pendingState!);
|
||
}
|
||
|
||
return new DefaultUnRegister(() => UnSubscribe(subscription));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消订阅指定监听器。
|
||
/// </summary>
|
||
/// <param name="listener">需要移除的监听器。</param>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="listener"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public void UnSubscribe(Action<TState> listener)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(listener);
|
||
|
||
lock (_lock)
|
||
{
|
||
var index = _listeners.FindIndex(subscription => subscription.Listener == listener);
|
||
if (index < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_listeners[index].IsSubscribed = false;
|
||
_listeners.RemoveAt(index);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最近一次分发的 action 类型。
|
||
/// </summary>
|
||
public Type? LastActionType
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _lastActionType;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最近一次真正改变状态的时间戳。
|
||
/// </summary>
|
||
public DateTimeOffset? LastStateChangedAt
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _lastStateChangedAt;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前订阅者数量。
|
||
/// </summary>
|
||
public int SubscriberCount
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _listeners.Count;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最近一次分发记录。
|
||
/// </summary>
|
||
public StoreDispatchRecord<TState>? LastDispatchRecord
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _lastDispatchRecord;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前 Store 使用的 action 匹配策略。
|
||
/// </summary>
|
||
public StoreActionMatchingMode ActionMatchingMode => _actionMatchingMode;
|
||
|
||
/// <summary>
|
||
/// 获取历史缓冲区容量。
|
||
/// </summary>
|
||
public int HistoryCapacity => _historyCapacity;
|
||
|
||
/// <summary>
|
||
/// 获取当前可见历史记录数量。
|
||
/// </summary>
|
||
public int HistoryCount
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _history.Count;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前状态在历史缓冲区中的索引。
|
||
/// </summary>
|
||
public int HistoryIndex
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _historyIndex;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前是否处于批处理阶段。
|
||
/// </summary>
|
||
public bool IsBatching
|
||
{
|
||
get
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _batchDepth > 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前历史快照列表的只读快照。
|
||
/// 该方法以方法语义显式表达会分配并返回集合副本,避免把快照克隆隐藏在属性访问中。
|
||
/// </summary>
|
||
/// <returns>当前历史快照列表;若未启用历史记录或当前没有历史,则返回空数组。</returns>
|
||
public IReadOnlyList<StoreHistoryEntry<TState>> GetHistoryEntriesSnapshot()
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _history.Count == 0 ? Array.Empty<StoreHistoryEntry<TState>>() : _history.ToArray();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建一个用于当前状态类型的 Store 构建器。
|
||
/// </summary>
|
||
/// <returns>新的 Store 构建器实例。</returns>
|
||
public static StoreBuilder<TState> CreateBuilder()
|
||
{
|
||
return new StoreBuilder<TState>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册一个强类型 reducer。
|
||
/// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。
|
||
/// 该重载保留现有链式配置体验;若需要在运行时注销,请改用 <see cref="RegisterReducerHandle{TAction}(IReducer{TState, TAction})"/>。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||
/// <param name="reducer">要注册的 reducer 实例。</param>
|
||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public Store<TState> RegisterReducer<TAction>(IReducer<TState, TAction> reducer)
|
||
{
|
||
RegisterReducerHandle(reducer);
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用委托快速注册一个 reducer。
|
||
/// 该重载保留现有链式配置体验;若需要在运行时注销,请改用 <see cref="RegisterReducerHandle{TAction}(Func{TState, TAction, TState})"/>。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||
/// <param name="reducer">执行归约的委托。</param>
|
||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public Store<TState> RegisterReducer<TAction>(Func<TState, TAction, TState> reducer)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(reducer);
|
||
return RegisterReducer(new DelegateReducer<TAction>(reducer));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册一个强类型 reducer,并返回可用于注销该 reducer 的句柄。
|
||
/// 该句柄只会移除当前这次注册,不会影响同一 action 类型下的其他 reducer。
|
||
/// 若在 dispatch 进行中调用注销,当前这次 dispatch 仍会使用开始时抓取的 reducer 快照,
|
||
/// 注销仅影响之后的新 dispatch。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||
/// <param name="reducer">要注册的 reducer 实例。</param>
|
||
/// <returns>用于注销当前 reducer 注册的句柄。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public IUnRegister RegisterReducerHandle<TAction>(IReducer<TState, TAction> reducer)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(reducer);
|
||
|
||
var actionType = typeof(TAction);
|
||
ReducerRegistration registration;
|
||
|
||
lock (_lock)
|
||
{
|
||
registration = new ReducerRegistration(new ReducerAdapter<TAction>(reducer), _nextReducerSequence++);
|
||
|
||
if (!_reducers.TryGetValue(actionType, out var reducers))
|
||
{
|
||
reducers = [];
|
||
_reducers[actionType] = reducers;
|
||
}
|
||
|
||
reducers.Add(registration);
|
||
}
|
||
|
||
return new DefaultUnRegister(() => UnRegisterReducer(actionType, registration));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用委托快速注册一个 reducer,并返回可用于注销该 reducer 的句柄。
|
||
/// 适合测试代码或按场景临时挂载的状态逻辑。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||
/// <param name="reducer">执行归约的委托。</param>
|
||
/// <returns>用于注销当前 reducer 注册的句柄。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public IUnRegister RegisterReducerHandle<TAction>(Func<TState, TAction, TState> reducer)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(reducer);
|
||
return RegisterReducerHandle(new DelegateReducer<TAction>(reducer));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加一个 Store 中间件。
|
||
/// 中间件按添加顺序包裹 reducer 执行,可用于日志、审计或调试。
|
||
/// 该重载保留现有链式配置体验;若需要在运行时注销,请改用 <see cref="RegisterMiddleware"/>.
|
||
/// </summary>
|
||
/// <param name="middleware">要添加的中间件实例。</param>
|
||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="middleware"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public Store<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
|
||
{
|
||
RegisterMiddleware(middleware);
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册一个 Store 中间件,并返回可用于注销该中间件的句柄。
|
||
/// 中间件按注册顺序包裹 reducer 执行;注销只会移除当前这次注册。
|
||
/// 若在 dispatch 进行中调用注销,当前这次 dispatch 仍会使用开始时抓取的中间件快照,
|
||
/// 注销仅影响之后的新 dispatch。
|
||
/// </summary>
|
||
/// <param name="middleware">要注册的中间件实例。</param>
|
||
/// <returns>用于注销当前中间件注册的句柄。</returns>
|
||
/// <exception cref="ArgumentNullException">当 <paramref name="middleware"/> 为 <see langword="null"/> 时抛出。</exception>
|
||
public IUnRegister RegisterMiddleware(IStoreMiddleware<TState> middleware)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(middleware);
|
||
|
||
var registration = new MiddlewareRegistration(middleware);
|
||
|
||
lock (_lock)
|
||
{
|
||
_middlewares.Add(registration);
|
||
}
|
||
|
||
return new DefaultUnRegister(() => UnRegisterMiddleware(registration));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或创建一个带缓存的局部状态选择视图。
|
||
/// 对于会被频繁读取的 Model 只读属性,推荐使用该方法复用同一个选择实例。
|
||
/// </summary>
|
||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
||
/// <param name="key">缓存键,调用方应保证同一个键始终表示同一局部状态语义。</param>
|
||
/// <param name="selector">状态选择委托。</param>
|
||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
||
/// <returns>稳定复用的选择视图实例。</returns>
|
||
public StoreSelection<TState, TSelected> GetOrCreateSelection<TSelected>(
|
||
string key,
|
||
Func<TState, TSelected> selector,
|
||
IEqualityComparer<TSelected>? comparer = null)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(key);
|
||
ArgumentNullException.ThrowIfNull(selector);
|
||
|
||
lock (_lock)
|
||
{
|
||
if (_selectionCache.TryGetValue(key, out var existing))
|
||
{
|
||
if (existing is StoreSelection<TState, TSelected> cachedSelection)
|
||
{
|
||
return cachedSelection;
|
||
}
|
||
|
||
throw new InvalidOperationException(
|
||
$"A cached selection with key '{key}' already exists with a different selected type.");
|
||
}
|
||
|
||
var selection = new StoreSelection<TState, TSelected>(this, selector, comparer);
|
||
_selectionCache[key] = selection;
|
||
return selection;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或创建一个带缓存的只读 BindableProperty 风格视图。
|
||
/// </summary>
|
||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
||
/// <param name="key">缓存键,调用方应保证同一个键始终表示同一局部状态语义。</param>
|
||
/// <param name="selector">状态选择委托。</param>
|
||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
||
/// <returns>稳定复用的只读绑定视图。</returns>
|
||
public StoreSelection<TState, TSelected> GetOrCreateBindableProperty<TSelected>(
|
||
string key,
|
||
Func<TState, TSelected> selector,
|
||
IEqualityComparer<TSelected>? comparer = null)
|
||
{
|
||
return GetOrCreateSelection(key, selector, comparer);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行一次完整分发管线。
|
||
/// </summary>
|
||
/// <param name="context">当前分发上下文。</param>
|
||
/// <param name="middlewares">本次分发使用的中间件快照。</param>
|
||
/// <param name="reducers">本次分发使用的 reducer 快照。</param>
|
||
/// <param name="stateComparer">本次分发使用的状态比较器快照。</param>
|
||
private static void ExecuteDispatchPipeline(
|
||
StoreDispatchContext<TState> context,
|
||
IReadOnlyList<IStoreMiddleware<TState>> middlewares,
|
||
IReadOnlyList<IStoreReducerAdapter> reducers,
|
||
IEqualityComparer<TState> stateComparer)
|
||
{
|
||
Action pipeline = () => ApplyReducers(context, reducers, stateComparer);
|
||
|
||
for (var i = middlewares.Count - 1; i >= 0; i--)
|
||
{
|
||
var middleware = middlewares[i];
|
||
var next = pipeline;
|
||
pipeline = () => middleware.Invoke(context, next);
|
||
}
|
||
|
||
pipeline();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对当前 action 应用所有匹配的 reducer。
|
||
/// reducer 会按照预先计算好的稳定顺序执行,从而在多态匹配模式下仍保持确定性的状态演进。
|
||
/// </summary>
|
||
/// <param name="context">当前分发上下文。</param>
|
||
/// <param name="reducers">本次分发使用的 reducer 快照。</param>
|
||
/// <param name="stateComparer">本次分发使用的状态比较器快照。</param>
|
||
private static void ApplyReducers(
|
||
StoreDispatchContext<TState> context,
|
||
IReadOnlyList<IStoreReducerAdapter> reducers,
|
||
IEqualityComparer<TState> stateComparer)
|
||
{
|
||
if (reducers.Count == 0)
|
||
{
|
||
context.NextState = context.PreviousState;
|
||
context.HasStateChanged = false;
|
||
return;
|
||
}
|
||
|
||
var nextState = context.PreviousState;
|
||
|
||
// 多个 reducer 共享同一 action 类型时,后一个 reducer 以前一个 reducer 的输出作为输入,
|
||
// 从而支持按模块拆分归约逻辑,同时保持总体状态演进顺序明确。
|
||
foreach (var reducer in reducers)
|
||
{
|
||
nextState = reducer.Reduce(nextState, context.Action);
|
||
}
|
||
|
||
context.NextState = nextState;
|
||
context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
|
||
/// </summary>
|
||
/// <exception cref="InvalidOperationException">当检测到重入分发时抛出。</exception>
|
||
private void EnsureNotDispatching()
|
||
{
|
||
if (_isDispatching)
|
||
{
|
||
throw new InvalidOperationException("Nested dispatch on the same store is not allowed.");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将新的已提交状态应用到 Store。
|
||
/// 该方法负责统一更新当前状态、最后变更时间和历史缓冲区,避免 dispatch 与 time-travel 路径产生分叉语义。
|
||
/// </summary>
|
||
/// <param name="nextState">要提交的新状态。</param>
|
||
/// <param name="changedAt">状态生效时间。</param>
|
||
/// <param name="action">触发该状态的 action;若本次变化不是由 dispatch 触发,则为 <see langword="null"/>。</param>
|
||
private void ApplyCommittedStateChange(TState nextState, DateTimeOffset changedAt, object? action)
|
||
{
|
||
_state = nextState;
|
||
_lastStateChangedAt = changedAt;
|
||
RecordHistoryEntry(nextState, changedAt, action);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从当前订阅集合中提取需要立即通知的监听器快照,并为尚未激活的初始化订阅保存待补发状态。
|
||
/// </summary>
|
||
/// <param name="nextState">本次分发后的最新状态。</param>
|
||
/// <returns>需要在锁外立即调用的监听器快照。</returns>
|
||
private Action<TState>[] SnapshotListenersForNotification(TState nextState)
|
||
{
|
||
if (_listeners.Count == 0)
|
||
{
|
||
return Array.Empty<Action<TState>>();
|
||
}
|
||
|
||
var activeListeners = new List<Action<TState>>(_listeners.Count);
|
||
foreach (var subscription in _listeners)
|
||
{
|
||
if (!subscription.IsSubscribed)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (subscription.IsActive)
|
||
{
|
||
activeListeners.Add(subscription.Listener);
|
||
continue;
|
||
}
|
||
|
||
subscription.PendingState = nextState;
|
||
subscription.HasPendingState = true;
|
||
}
|
||
|
||
return activeListeners.Count > 0 ? activeListeners.ToArray() : Array.Empty<Action<TState>>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据当前批处理状态决定是立即提取监听器快照,还是把通知折叠到批处理尾部。
|
||
/// </summary>
|
||
/// <param name="nextState">最新状态快照。</param>
|
||
/// <param name="notificationState">若需要立即通知,则返回要回放给监听器的状态。</param>
|
||
/// <returns>需要立即通知的监听器快照;若处于批处理阶段则返回空数组。</returns>
|
||
private Action<TState>[] CaptureListenersOrDeferNotification(TState nextState, out TState notificationState)
|
||
{
|
||
if (_batchDepth > 0)
|
||
{
|
||
_pendingBatchState = nextState;
|
||
_hasPendingBatchNotification = true;
|
||
notificationState = default!;
|
||
return Array.Empty<Action<TState>>();
|
||
}
|
||
|
||
notificationState = nextState;
|
||
return SnapshotListenersForNotification(nextState);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为当前中间件链创建快照。
|
||
/// 调用该方法时必须已经持有状态锁,从而避免额外的锁重入和快照时序歧义。
|
||
/// </summary>
|
||
/// <returns>当前中间件链的快照;若未注册则返回空数组。</returns>
|
||
private IStoreMiddleware<TState>[] CreateMiddlewareSnapshotCore()
|
||
{
|
||
Debug.Assert(
|
||
Monitor.IsEntered(_lock),
|
||
"Caller must hold _lock before invoking CreateMiddlewareSnapshotCore to avoid concurrency bugs.");
|
||
|
||
if (_middlewares.Count == 0)
|
||
{
|
||
return Array.Empty<IStoreMiddleware<TState>>();
|
||
}
|
||
|
||
var snapshot = new IStoreMiddleware<TState>[_middlewares.Count];
|
||
for (var i = 0; i < _middlewares.Count; i++)
|
||
{
|
||
snapshot[i] = _middlewares[i].Middleware;
|
||
}
|
||
|
||
return snapshot;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为当前 action 类型创建 reducer 快照。
|
||
/// 在精确匹配模式下只读取一个注册桶;在多态模式下会按稳定排序规则合并可赋值的基类和接口注册。
|
||
/// </summary>
|
||
/// <param name="actionType">当前分发的 action 类型。</param>
|
||
/// <returns>对应 action 类型的 reducer 快照;若未注册则返回空数组。</returns>
|
||
private IStoreReducerAdapter[] CreateReducerSnapshotCore(Type actionType)
|
||
{
|
||
Debug.Assert(
|
||
Monitor.IsEntered(_lock),
|
||
"Caller must hold _lock before invoking CreateReducerSnapshotCore to avoid concurrency bugs.");
|
||
|
||
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
|
||
{
|
||
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
|
||
{
|
||
return Array.Empty<IStoreReducerAdapter>();
|
||
}
|
||
|
||
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
|
||
for (var i = 0; i < exactReducers.Count; i++)
|
||
{
|
||
exactSnapshot[i] = exactReducers[i].Adapter;
|
||
}
|
||
|
||
return exactSnapshot;
|
||
}
|
||
|
||
List<ReducerMatch>? matches = null;
|
||
|
||
foreach (var reducerBucket in _reducers)
|
||
{
|
||
if (!TryCreateReducerMatch(actionType, reducerBucket.Key, out var matchCategory,
|
||
out var inheritanceDistance))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
matches ??= new List<ReducerMatch>();
|
||
foreach (var registration in reducerBucket.Value)
|
||
{
|
||
matches.Add(new ReducerMatch(
|
||
registration.Adapter,
|
||
registration.Sequence,
|
||
matchCategory,
|
||
inheritanceDistance));
|
||
}
|
||
}
|
||
|
||
if (matches is null || matches.Count == 0)
|
||
{
|
||
return Array.Empty<IStoreReducerAdapter>();
|
||
}
|
||
|
||
matches.Sort(static (left, right) =>
|
||
{
|
||
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
|
||
if (categoryComparison != 0)
|
||
{
|
||
return categoryComparison;
|
||
}
|
||
|
||
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
|
||
if (distanceComparison != 0)
|
||
{
|
||
return distanceComparison;
|
||
}
|
||
|
||
return left.Sequence.CompareTo(right.Sequence);
|
||
});
|
||
|
||
var snapshot = new IStoreReducerAdapter[matches.Count];
|
||
for (var i = 0; i < matches.Count; i++)
|
||
{
|
||
snapshot[i] = matches[i].Adapter;
|
||
}
|
||
|
||
return snapshot;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断指定 reducer 注册类型是否能匹配当前 action 类型,并给出用于稳定排序的分类信息。
|
||
/// </summary>
|
||
/// <param name="actionType">当前 action 的运行时类型。</param>
|
||
/// <param name="registeredActionType">reducer 注册时声明的 action 类型。</param>
|
||
/// <param name="matchCategory">匹配分类:0 为精确类型,1 为基类,2 为接口。</param>
|
||
/// <param name="inheritanceDistance">继承距离,值越小表示越接近当前 action 类型。</param>
|
||
/// <returns>若可以匹配则返回 <see langword="true"/>。</returns>
|
||
private static bool TryCreateReducerMatch(
|
||
Type actionType,
|
||
Type registeredActionType,
|
||
out int matchCategory,
|
||
out int inheritanceDistance)
|
||
{
|
||
if (registeredActionType == actionType)
|
||
{
|
||
matchCategory = 0;
|
||
inheritanceDistance = 0;
|
||
return true;
|
||
}
|
||
|
||
if (!registeredActionType.IsAssignableFrom(actionType))
|
||
{
|
||
matchCategory = default;
|
||
inheritanceDistance = default;
|
||
return false;
|
||
}
|
||
|
||
if (registeredActionType.IsInterface)
|
||
{
|
||
matchCategory = 2;
|
||
inheritanceDistance = 0;
|
||
return true;
|
||
}
|
||
|
||
matchCategory = 1;
|
||
inheritanceDistance = GetInheritanceDistance(actionType, registeredActionType);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算当前 action 类型到目标基类的继承距离。
|
||
/// 距离越小表示基类越接近当前 action 类型,在多态匹配排序中优先级越高。
|
||
/// </summary>
|
||
/// <param name="actionType">当前 action 的运行时类型。</param>
|
||
/// <param name="registeredActionType">reducer 注册时声明的基类类型。</param>
|
||
/// <returns>从当前 action 到目标基类的继承层级数。</returns>
|
||
private static int GetInheritanceDistance(Type actionType, Type registeredActionType)
|
||
{
|
||
var distance = 0;
|
||
var currentType = actionType;
|
||
|
||
while (currentType != registeredActionType && currentType.BaseType is not null)
|
||
{
|
||
currentType = currentType.BaseType;
|
||
distance++;
|
||
}
|
||
|
||
return distance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 记录一条新的历史快照。
|
||
/// 当当前游标不在时间线末尾时,会先裁掉 redo 分支,再追加新的状态快照。
|
||
/// </summary>
|
||
/// <param name="state">要记录的状态快照。</param>
|
||
/// <param name="recordedAt">历史记录时间。</param>
|
||
/// <param name="action">触发该状态的 action;若为空则表示当前变化不应写入历史。</param>
|
||
private void RecordHistoryEntry(TState state, DateTimeOffset recordedAt, object? action)
|
||
{
|
||
if (_historyCapacity == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (action is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_historyIndex >= 0 && _historyIndex < _history.Count - 1)
|
||
{
|
||
_history.RemoveRange(_historyIndex + 1, _history.Count - _historyIndex - 1);
|
||
}
|
||
|
||
_history.Add(new StoreHistoryEntry<TState>(state, recordedAt, action));
|
||
_historyIndex = _history.Count - 1;
|
||
|
||
if (_history.Count <= _historyCapacity)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var overflow = _history.Count - _historyCapacity;
|
||
_history.RemoveRange(0, overflow);
|
||
_historyIndex = Math.Max(0, _historyIndex - overflow);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将当前状态重置为新的历史锚点。
|
||
/// 该操作用于 Store 初始化和显式清空历史后重新建立时间线起点。
|
||
/// </summary>
|
||
/// <param name="recordedAt">锚点记录时间。</param>
|
||
private void ResetHistoryToCurrentState(DateTimeOffset recordedAt)
|
||
{
|
||
_history.Clear();
|
||
_history.Add(new StoreHistoryEntry<TState>(_state, recordedAt));
|
||
_historyIndex = 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将当前状态移动到指定历史索引。
|
||
/// 该方法统一承载 Undo、Redo 和显式时间旅行路径,确保通知与批处理语义保持一致。
|
||
/// </summary>
|
||
/// <param name="historyIndexOrOffset">目标索引或相对偏移量。</param>
|
||
/// <param name="isRelative">是否按相对偏移量解释 <paramref name="historyIndexOrOffset"/>。</param>
|
||
/// <param name="argumentName">参数名,用于异常信息。</param>
|
||
/// <param name="emptyHistoryMessage">当相对跳转无可用历史时的错误信息;绝对跳转场景传 <see langword="null"/>。</param>
|
||
private void MoveToHistoryIndex(
|
||
int historyIndexOrOffset,
|
||
bool isRelative,
|
||
string argumentName,
|
||
string? emptyHistoryMessage)
|
||
{
|
||
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
|
||
TState notificationState = default!;
|
||
|
||
lock (_dispatchGate)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
EnsureNotDispatching();
|
||
EnsureHistoryEnabled();
|
||
|
||
var targetIndex = isRelative ? _historyIndex + historyIndexOrOffset : historyIndexOrOffset;
|
||
if (targetIndex < 0 || targetIndex >= _history.Count)
|
||
{
|
||
if (isRelative)
|
||
{
|
||
throw new InvalidOperationException(emptyHistoryMessage);
|
||
}
|
||
|
||
throw new ArgumentOutOfRangeException(argumentName, historyIndexOrOffset,
|
||
"History index is out of range.");
|
||
}
|
||
|
||
if (targetIndex == _historyIndex)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_historyIndex = targetIndex;
|
||
notificationState = _history[targetIndex].State;
|
||
_state = notificationState;
|
||
_lastStateChangedAt = DateTimeOffset.UtcNow;
|
||
listenersSnapshot = CaptureListenersOrDeferNotification(notificationState, out notificationState);
|
||
}
|
||
}
|
||
|
||
if (listenersSnapshot.Length > 0)
|
||
{
|
||
NotifyListeners(listenersSnapshot, notificationState);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保当前 Store 已启用历史缓冲区。
|
||
/// </summary>
|
||
/// <exception cref="InvalidOperationException">当历史记录未启用时抛出。</exception>
|
||
private void EnsureHistoryEnabled()
|
||
{
|
||
if (_historyCapacity == 0)
|
||
{
|
||
throw new InvalidOperationException("History is not enabled for this store.");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在锁外顺序通知监听器。
|
||
/// 始终在锁外通知可避免监听器内部读取 Store 或执行额外逻辑时产生死锁。
|
||
/// </summary>
|
||
/// <param name="listenersSnapshot">监听器快照。</param>
|
||
/// <param name="state">要回放给监听器的状态。</param>
|
||
private static void NotifyListeners(Action<TState>[] listenersSnapshot, TState state)
|
||
{
|
||
foreach (var listener in listenersSnapshot)
|
||
{
|
||
listener(state);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解绑一个精确的订阅对象。
|
||
/// </summary>
|
||
/// <param name="subscription">要解绑的订阅对象。</param>
|
||
private void UnSubscribe(ListenerSubscription subscription)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
subscription.IsSubscribed = false;
|
||
_listeners.Remove(subscription);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注销一个中间件注册条目。
|
||
/// 仅精确移除与当前句柄关联的条目,避免误删同一实例的其他重复注册。
|
||
/// </summary>
|
||
/// <param name="registration">要移除的中间件注册条目。</param>
|
||
private void UnRegisterMiddleware(MiddlewareRegistration registration)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_middlewares.Remove(registration);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注销一个 reducer 注册条目。
|
||
/// 若该 action 类型下已无其他 reducer,则同时清理空注册桶,保持注册表紧凑。
|
||
/// </summary>
|
||
/// <param name="actionType">reducer 对应的 action 类型。</param>
|
||
/// <param name="registration">要移除的 reducer 注册条目。</param>
|
||
private void UnRegisterReducer(Type actionType, ReducerRegistration registration)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (!_reducers.TryGetValue(actionType, out var reducers))
|
||
{
|
||
return;
|
||
}
|
||
|
||
reducers.Remove(registration);
|
||
if (reducers.Count == 0)
|
||
{
|
||
_reducers.Remove(actionType);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 适配不同 action 类型 reducer 的内部统一接口。
|
||
/// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。
|
||
/// </summary>
|
||
private interface IStoreReducerAdapter
|
||
{
|
||
/// <summary>
|
||
/// 使用当前 action 对状态进行一次归约。
|
||
/// </summary>
|
||
/// <param name="currentState">当前状态。</param>
|
||
/// <param name="action">分发中的 action。</param>
|
||
/// <returns>归约后的下一状态。</returns>
|
||
TState Reduce(TState currentState, object action);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 基于强类型 reducer 的适配器实现。
|
||
/// 该适配器仅负责安全地完成 object 到 action 类型的转换,然后委托给真实 reducer。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">当前适配器负责处理的 action 类型。</typeparam>
|
||
private sealed class ReducerAdapter<TAction>(IReducer<TState, TAction> reducer) : IStoreReducerAdapter
|
||
{
|
||
/// <summary>
|
||
/// 包装后的强类型 reducer 实例。
|
||
/// </summary>
|
||
private readonly IReducer<TState, TAction> _reducer =
|
||
reducer ?? throw new ArgumentNullException(nameof(reducer));
|
||
|
||
/// <summary>
|
||
/// 将运行时 action 转换为强类型 action 后执行归约。
|
||
/// </summary>
|
||
/// <param name="currentState">当前状态。</param>
|
||
/// <param name="action">运行时 action。</param>
|
||
/// <returns>归约后的下一状态。</returns>
|
||
public TState Reduce(TState currentState, object action)
|
||
{
|
||
return _reducer.Reduce(currentState, (TAction)action);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 表示一条 reducer 注册记录。
|
||
/// 该包装对象为运行时注销提供稳定身份,并携带全局序号以支撑多态匹配时的稳定排序。
|
||
/// </summary>
|
||
private sealed class ReducerRegistration(IStoreReducerAdapter adapter, long sequence)
|
||
{
|
||
/// <summary>
|
||
/// 获取真正执行归约的内部适配器。
|
||
/// </summary>
|
||
public IStoreReducerAdapter Adapter { get; } = adapter;
|
||
|
||
/// <summary>
|
||
/// 获取该 reducer 的全局注册序号。
|
||
/// </summary>
|
||
public long Sequence { get; } = sequence;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 表示一次多态 reducer 匹配结果。
|
||
/// 该结构在创建快照时缓存排序所需元数据,避免排序阶段重复计算类型关系。
|
||
/// </summary>
|
||
private sealed class ReducerMatch(
|
||
IStoreReducerAdapter adapter,
|
||
long sequence,
|
||
int matchCategory,
|
||
int inheritanceDistance)
|
||
{
|
||
/// <summary>
|
||
/// 获取匹配到的 reducer 适配器。
|
||
/// </summary>
|
||
public IStoreReducerAdapter Adapter { get; } = adapter;
|
||
|
||
/// <summary>
|
||
/// 获取 reducer 的全局注册序号。
|
||
/// </summary>
|
||
public long Sequence { get; } = sequence;
|
||
|
||
/// <summary>
|
||
/// 获取匹配分类:0 为精确类型,1 为基类,2 为接口。
|
||
/// </summary>
|
||
public int MatchCategory { get; } = matchCategory;
|
||
|
||
/// <summary>
|
||
/// 获取继承距离。
|
||
/// 该值越小表示注册类型越接近当前 action 类型。
|
||
/// </summary>
|
||
public int InheritanceDistance { get; } = inheritanceDistance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 基于委托的 reducer 适配器实现,便于快速在测试和应用代码中声明 reducer。
|
||
/// </summary>
|
||
/// <typeparam name="TAction">当前适配器负责处理的 action 类型。</typeparam>
|
||
private sealed class DelegateReducer<TAction>(Func<TState, TAction, TState> reducer) : IReducer<TState, TAction>
|
||
{
|
||
/// <summary>
|
||
/// 真正执行归约的委托。
|
||
/// </summary>
|
||
private readonly Func<TState, TAction, TState> _reducer =
|
||
reducer ?? throw new ArgumentNullException(nameof(reducer));
|
||
|
||
/// <summary>
|
||
/// 执行一次委托归约。
|
||
/// </summary>
|
||
/// <param name="currentState">当前状态。</param>
|
||
/// <param name="action">当前 action。</param>
|
||
/// <returns>归约后的下一状态。</returns>
|
||
public TState Reduce(TState currentState, TAction action)
|
||
{
|
||
return _reducer(currentState, action);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 表示一条中间件注册记录。
|
||
/// 通过显式注册对象而不是直接存储中间件实例,可在重复注册同一实例时保持精确注销。
|
||
/// </summary>
|
||
private sealed class MiddlewareRegistration(IStoreMiddleware<TState> middleware)
|
||
{
|
||
/// <summary>
|
||
/// 获取注册的中间件实例。
|
||
/// </summary>
|
||
public IStoreMiddleware<TState> Middleware { get; } = middleware;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 表示一个 Store 状态监听订阅。
|
||
/// 该对象用于支持初始化回放与正式订阅之间的原子衔接,避免 SubscribeWithInitValue 漏掉状态变化。
|
||
/// </summary>
|
||
private sealed class ListenerSubscription(Action<TState> listener)
|
||
{
|
||
/// <summary>
|
||
/// 获取订阅回调。
|
||
/// </summary>
|
||
public Action<TState> Listener { get; } = listener;
|
||
|
||
/// <summary>
|
||
/// 获取或设置订阅是否已激活。
|
||
/// 非激活状态表示正在执行初始化回放,此时新的状态变化会被暂存为待补发值。
|
||
/// </summary>
|
||
public bool IsActive { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// 获取或设置订阅是否仍然有效。
|
||
/// </summary>
|
||
public bool IsSubscribed { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// 获取或设置是否存在待补发的最新状态。
|
||
/// </summary>
|
||
public bool HasPendingState { get; set; }
|
||
|
||
/// <summary>
|
||
/// 获取或设置初始化阶段积累的最新状态。
|
||
/// </summary>
|
||
public TState PendingState { get; set; } = default!;
|
||
}
|
||
} |