Skip to content

feat(state): 扩展状态管理功能支持历史记录撤销重做和批处理#134

Merged
GeWuYou merged 3 commits into
mainfrom
feat/state-history-undo-redo
Mar 24, 2026
Merged

feat(state): 扩展状态管理功能支持历史记录撤销重做和批处理#134
GeWuYou merged 3 commits into
mainfrom
feat/state-history-undo-redo

Conversation

@GeWuYou

@GeWuYou GeWuYou commented Mar 24, 2026

Copy link
Copy Markdown
Owner
  • 新增 RunInBatch() 方法支持批处理通知折叠
  • 添加 Undo()/Redo() 基于历史缓冲区的状态回退前进功能
  • 实现 TimeTravelTo() 跳转到指定历史索引的能力
  • 提供 ClearHistory() 以当前状态重置历史锚点的功能
  • 支持可选历史缓冲区、撤销/重做和时间旅行功能
  • 添加可选批处理通知折叠机制
  • 实现多态 action 匹配(基类/接口)支持
  • 在诊断信息中增加历史游标和批处理状态
  • StoreBuilder 新增 WithHistoryCapacity() 和 WithActionMatching() 配置方法
  • 优化 reducer 注册支持全局序号以实现稳定排序
  • 实现多态匹配时的类型继承距离计算
  • 添加批处理嵌套支持和状态通知延迟机制

Summary by Sourcery

使用可选的历史记录、批处理、多态动作匹配以及 EventBus 桥接能力扩展集中式状态存储(state store),并将这些能力接入到构建器(builder)、诊断(diagnostics)、文档和测试中。

新功能:

  • 为 Store 及其抽象添加可配置的历史记录缓冲,支持撤销(undo)、重做(redo)、时间旅行(time-travel)以及 ClearHistory。
  • 引入通过 RunInBatch 实现的批量执行,将多次状态变更折叠为一次最终通知,并支持嵌套调用。
  • 为 reducer 添加多态动作匹配能力,通过 StoreActionMatchingMode 进行配置,并通过 StoreBuilder 对外暴露。
  • 提供从 Store 到 EventBus 的桥接扩展,以及对应的 dispatched/state-changed 事件类型,以便与现有的 EventBus 使用方兼容。

增强项:

  • 扩展 reducer 注册机制以跟踪全局序列号,并在多态匹配场景中计算确定性的执行顺序。
  • 通过 Store 的诊断接口暴露与历史记录、批处理和动作匹配相关的诊断信息(如容量、数量、索引、条目、标志)。
  • 更新 StoreBuilder,以便在构建 store 时可配置历史容量和动作匹配模式。

文档:

  • 更新状态管理文档,描述历史记录/撤销-重做/时间旅行、批处理语义、多态动作匹配以及 StoreBuilder 的配置选项,并澄清推荐的使用模式。

测试:

  • 添加单元测试,覆盖批处理行为、历史操作(撤销、重做、时间旅行、分支重置)、多态匹配顺序、StoreBuilder 配置,以及 Store 到 EventBus 的桥接(包括拆卸/回收行为)。
Original summary in English

Summary by Sourcery

Extend the centralized state store with optional history, batching, polymorphic action matching, and EventBus bridging capabilities, and wire them into the builder, diagnostics, documentation, and tests.

New Features:

  • Add configurable history buffering with undo, redo, time-travel, and ClearHistory support on Store and its abstractions.
  • Introduce batch execution via RunInBatch that collapses multiple state changes into a single final notification, with nesting support.
  • Add polymorphic action matching for reducers, configurable via StoreActionMatchingMode and exposed through StoreBuilder.
  • Provide Store-to-EventBus bridge extensions and corresponding dispatched/state-changed event types for compatibility with existing EventBus consumers.

Enhancements:

  • Extend reducer registration to track a global sequence and compute deterministic ordering across polymorphic matches.
  • Expose history, batching, and action-matching-related diagnostics (capacity, count, index, entries, flags) from the Store diagnostics interface.
  • Update StoreBuilder to construct stores with configurable history capacity and action matching mode.

Documentation:

  • Update state management documentation to describe history/undo-redo/time-travel, batching semantics, polymorphic action matching, and StoreBuilder configuration options, and clarify recommended usage patterns.

Tests:

  • Add unit tests covering batching behavior, history operations (undo, redo, time travel, branch reset), polymorphic matching order, StoreBuilder configuration, and Store-to-EventBus bridging including teardown behavior.

- 新增 RunInBatch() 方法支持批处理通知折叠
- 添加 Undo()/Redo() 基于历史缓冲区的状态回退前进功能
- 实现 TimeTravelTo() 跳转到指定历史索引的能力
- 提供 ClearHistory() 以当前状态重置历史锚点的功能
- 支持可选历史缓冲区、撤销/重做和时间旅行功能
- 添加可选批处理通知折叠机制
- 实现多态 action 匹配(基类/接口)支持
- 在诊断信息中增加历史游标和批处理状态
- StoreBuilder 新增 WithHistoryCapacity() 和 WithActionMatching() 配置方法
- 优化 reducer 注册支持全局序号以实现稳定排序
- 实现多态匹配时的类型继承距离计算
- 添加批处理嵌套支持和状态通知延迟机制
@sourcery-ai

sourcery-ai Bot commented Mar 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

扩展 Store 状态容器,新增可选历史记录(撤销/重做/时间旅行)、批量通知、多态 Action 匹配、Store 到 EventBus 的桥接事件以及对应的 Builder 配置选项,并更新了测试和文档。

启用历史记录跟踪与批量通知时的 Dispatch 时序图

sequenceDiagram
    actor Client
    participant Store_TState_ as Store
    participant MiddlewareChain
    participant Reducers
    participant Subscribers

    rect rgb(235,235,255)
      Client->>Store_TState_: RunInBatch(batchAction)
      activate Store_TState_
      Store_TState_->>Store_TState_: increment _batchDepth
      Store_TState_->>Store_TState_: execute batchAction()

      note over Client,Store_TState_: 在 batchAction 内部会有多次 Dispatch 调用
    end

    loop each action in batch
      Client->>Store_TState_: Dispatch_TAction_(action)
      activate Store_TState_
      Store_TState_->>Store_TState_: EnsureNotDispatching()
      Store_TState_->>Store_TState_: CreateMiddlewareSnapshotCore()
      Store_TState_->>Store_TState_: CreateReducerSnapshotCore(action.GetType())
      Store_TState_->>MiddlewareChain: ExecuteDispatchPipeline(context, middlewares, reducers)
      activate MiddlewareChain
      MiddlewareChain->>Reducers: ApplyReducers(context)
      activate Reducers
      Reducers-->>MiddlewareChain: newState
      deactivate Reducers
      MiddlewareChain-->>Store_TState_: context.NextState, context.DispatchedAt
      deactivate MiddlewareChain

      Store_TState_->>Store_TState_: ApplyCommittedStateChange(nextState, changedAt, action)
      Store_TState_->>Store_TState_: CaptureListenersOrDeferNotification(nextState, out notificationState)
      alt batching active (_batchDepth > 0)
        Store_TState_->>Store_TState_: store pendingBatchState, hasPendingBatchNotification = true
        Store_TState_-->>Client: 返回但不通知
      else not batching
        Store_TState_->>Subscribers: NotifyListeners(listenersSnapshot, notificationState)
      end
      deactivate Store_TState_
    end

    rect rgb(235,255,235)
      Store_TState_->>Store_TState_: decrement _batchDepth
      alt leaving outermost batch and hasPendingBatchNotification
        Store_TState_->>Store_TState_: listenersSnapshot = SnapshotListenersForNotification(pendingBatchState)
        Store_TState_->>Store_TState_: clear pendingBatchState, hasPendingBatchNotification
        Store_TState_->>Subscribers: NotifyListeners(listenersSnapshot, pendingBatchState)
      else inner batch or no state change
        Store_TState_-->>Client: 无通知
      end
      deactivate Store_TState_
    end

    note over Store_TState_: 每次 ApplyCommittedStateChange 在启用历史记录时也会将 RecordHistoryEntry 写入历史缓冲区
Loading

Dispatch 与状态变更时 Store 到 EventBus 桥接的时序图

sequenceDiagram
    actor Client
    participant Store_TState_ as Store
    participant EventBus
    participant DispatchMiddleware as DispatchEventBusMiddleware_TState_
    participant StateBridge as StoreSubscriber
    participant DispatchConsumer
    participant StateConsumer

    rect rgb(235,235,255)
      Client->>Store_TState_: BridgeToEventBus_TState_(eventBus)
      activate Store_TState_
      Store_TState_->>Store_TState_: RegisterMiddleware(DispatchEventBusMiddleware_TState_)
      Store_TState_->>Store_TState_: Subscribe(state => Send StoreStateChangedEvent_TState_)
      Store_TState_-->>Client: 返回桥接的 IUnRegister
      deactivate Store_TState_

      EventBus->>DispatchConsumer: Register<StoreDispatchedEvent_TState_>()
      EventBus->>StateConsumer: Register<StoreStateChangedEvent_TState_>()
    end

    rect rgb(235,255,235)
      Client->>Store_TState_: Dispatch_TAction_(action)
      activate Store_TState_
      Store_TState_->>DispatchMiddleware: Invoke(context, next)
      activate DispatchMiddleware
      DispatchMiddleware->>Store_TState_: next() (继续 dispatch 流水线)
      Store_TState_->>Store_TState_: reducers 更新 State
      Store_TState_->>StateBridge: NotifyListeners(State)
      activate StateBridge
      StateBridge->>EventBus: Send(StoreStateChangedEvent_TState_)
      deactivate StateBridge

      DispatchMiddleware->>EventBus: Send(StoreDispatchedEvent_TState_)
      deactivate DispatchMiddleware
      deactivate Store_TState_
    end

    EventBus-->>DispatchConsumer: StoreDispatchedEvent_TState_
    EventBus-->>StateConsumer: StoreStateChangedEvent_TState_

    note over Store_TState_,EventBus: 在批量 dispatch 中,StoreStateChangedEvent_TState_ 只会在最终状态时发送一次
Loading

带历史记录、批量操作、多态匹配与 EventBus 桥接的 Store 状态管理类图(更新版)

classDiagram
    direction LR

    class IStore_TState_ {
      <<interface>>
      +TState State
      +bool CanUndo
      +bool CanRedo
      +void Dispatch_TAction_(TAction action)
      +void RunInBatch(Action batchAction)
      +void Undo()
      +void Redo()
      +void TimeTravelTo(int historyIndex)
      +void ClearHistory()
    }

    class IReadonlyStore_TState_ {
      <<interface>>
      +TState State
      +IUnRegister Subscribe(Action_TState_ listener)
    }

    class IStoreDiagnostics_TState_ {
      <<interface>>
      +StoreDispatchRecord_TState_ LastDispatchRecord
      +StoreActionMatchingMode ActionMatchingMode
      +int HistoryCapacity
      +int HistoryCount
      +int HistoryIndex
      +IReadOnlyList_StoreHistoryEntry_TState__ HistoryEntries
      +bool IsBatching
    }

    class IStoreBuilder_TState_ {
      <<interface>>
      +IStoreBuilder_TState_ UseMiddleware(IStoreMiddleware_TState_ middleware)
      +IStoreBuilder_TState_ WithComparer(IEqualityComparer_TState_ comparer)
      +IStoreBuilder_TState_ WithHistoryCapacity(int historyCapacity)
      +IStoreBuilder_TState_ WithActionMatching(StoreActionMatchingMode actionMatchingMode)
      +IStoreBuilder_TState_ AddReducer_TAction_(IReducer_TState__TAction_ reducer)
      +IStoreBuilder_TState_ AddReducer_TAction_(Func_TState__TAction__TState_ reducer)
      +IStore_TState_ Build(TState initialState)
    }

    class Store_TState_ {
      +Store_TState_(TState initialState, IEqualityComparer_TState_ comparer, int historyCapacity, StoreActionMatchingMode actionMatchingMode)
      +TState State
      +bool CanUndo
      +bool CanRedo
      +bool IsBatching
      +StoreActionMatchingMode ActionMatchingMode
      +int HistoryCapacity
      +int HistoryCount
      +int HistoryIndex
      +IReadOnlyList_StoreHistoryEntry_TState__ HistoryEntries
      +void Dispatch_TAction_(TAction action)
      +void RunInBatch(Action batchAction)
      +void Undo()
      +void Redo()
      +void TimeTravelTo(int historyIndex)
      +void ClearHistory()
      +IUnRegister RegisterReducer_TAction_(IReducer_TState__TAction_ reducer)
      +IUnRegister RegisterMiddleware(IStoreMiddleware_TState_ middleware)
      -void ApplyCommittedStateChange(TState nextState, DateTimeOffset changedAt, object action)
      -Action_TState_[] CaptureListenersOrDeferNotification(TState nextState, out TState notificationState)
      -IStoreReducerAdapter[] CreateReducerSnapshotCore(Type actionType)
      -void RecordHistoryEntry(TState state, DateTimeOffset recordedAt, object action)
      -void ResetHistoryToCurrentState(DateTimeOffset recordedAt)
      -void MoveToHistoryIndex(int historyIndexOrOffset, bool isRelative, string argumentName, string emptyHistoryMessage)
      -void EnsureHistoryEnabled()
      -static bool TryCreateReducerMatch(Type actionType, Type registeredActionType, out int matchCategory, out int inheritanceDistance)
      -static int GetInheritanceDistance(Type actionType, Type registeredActionType)
      -static void NotifyListeners(Action_TState_[] listenersSnapshot, TState state)
    }

    class StoreBuilder_TState_ {
      +StoreBuilder_TState_()
      -List_Action_Store_TState___ configurators
      -IEqualityComparer_TState_ comparer
      -int historyCapacity
      -StoreActionMatchingMode actionMatchingMode
      +IStoreBuilder_TState_ UseMiddleware(IStoreMiddleware_TState_ middleware)
      +IStoreBuilder_TState_ WithComparer(IEqualityComparer_TState_ comparer)
      +IStoreBuilder_TState_ WithHistoryCapacity(int historyCapacity)
      +IStoreBuilder_TState_ WithActionMatching(StoreActionMatchingMode actionMatchingMode)
      +IStoreBuilder_TState_ AddReducer_TAction_(Func_TState__TAction__TState_ reducer)
      +IStoreBuilder_TState_ AddReducer_TAction_(IReducer_TState__TAction_ reducer)
      +IStore_TState_ Build(TState initialState)
    }

    class StoreHistoryEntry_TState_ {
      +StoreHistoryEntry_TState_(TState state, DateTimeOffset recordedAt, object action)
      +TState State
      +DateTimeOffset RecordedAt
      +object Action
      +Type ActionType
    }

    class StoreActionMatchingMode {
      <<enumeration>>
      ExactTypeOnly
      IncludeAssignableTypes
    }

    class StoreDispatchedEvent_TState_ {
      +StoreDispatchedEvent_TState_(StoreDispatchRecord_TState_ dispatchRecord)
      +StoreDispatchRecord_TState_ DispatchRecord
    }

    class StoreStateChangedEvent_TState_ {
      +StoreStateChangedEvent_TState_(TState state, DateTimeOffset changedAt)
      +TState State
      +DateTimeOffset ChangedAt
    }

    class StoreEventBusExtensions {
      <<static>>
      +IUnRegister BridgeToEventBus_TState_(Store_TState_ store, IEventBus eventBus, bool publishDispatches, bool publishStateChanges)
      +IUnRegister BridgeDispatchesToEventBus_TState_(Store_TState_ store, IEventBus eventBus)
      +IUnRegister BridgeStateChangesToEventBus_TState_(IReadonlyStore_TState_ store, IEventBus eventBus)
    }

    class DispatchEventBusMiddleware_TState_ {
      +DispatchEventBusMiddleware_TState_(IEventBus eventBus)
      +void Invoke(StoreDispatchContext_TState_ context, Action next)
    }

    class ReducerRegistration {
      -ReducerRegistration(IStoreReducerAdapter adapter, long sequence)
      +IStoreReducerAdapter Adapter
      +long Sequence
    }

    class ReducerMatch {
      -ReducerMatch(IStoreReducerAdapter adapter, long sequence, int matchCategory, int inheritanceDistance)
      +IStoreReducerAdapter Adapter
      +long Sequence
      +int MatchCategory
      +int InheritanceDistance
    }

    Store_TState_ ..|> IStore_TState_
    Store_TState_ ..|> IReadonlyStore_TState_
    Store_TState_ ..|> IStoreDiagnostics_TState_

    StoreBuilder_TState_ ..|> IStoreBuilder_TState_

    Store_TState_ --> StoreHistoryEntry_TState_ : uses
    Store_TState_ --> ReducerRegistration : contains
    Store_TState_ --> ReducerMatch : uses for matching
    Store_TState_ --> StoreActionMatchingMode : config

    IStoreDiagnostics_TState_ --> StoreHistoryEntry_TState_ : exposes

    StoreDispatchedEvent_TState_ --> StoreDispatchRecord_TState_ : wraps
    StoreStateChangedEvent_TState_ --> Store_TState_ : state snapshot

    StoreEventBusExtensions --> Store_TState_ : extension
    StoreEventBusExtensions --> IReadonlyStore_TState_ : extension
    StoreEventBusExtensions --> IEventBus : bridge
    DispatchEventBusMiddleware_TState_ ..|> IStoreMiddleware_TState_
    DispatchEventBusMiddleware_TState_ --> IEventBus : publishes

    StoreBuilder_TState_ --> Store_TState_ : builds
    StoreBuilder_TState_ --> StoreActionMatchingMode : config
    StoreBuilder_TState_ --> IStoreMiddleware_TState_ : config
    StoreBuilder_TState_ --> IReducer_TState__TAction_ : config
Loading

文件级改动

Change Details Files
添加带有撤销/重做/时间旅行 API 和诊断接口的可选历史记录缓冲区。
  • 扩展 Store<TState>,新增历史列表、容量与索引跟踪,并通过构造函数参数启用历史记录
  • IStoreIStoreDiagnostics 上暴露 CanUndo/CanRedo 以及历史相关的诊断信息(容量、数量、索引、条目)
  • 实现 ApplyCommittedStateChangeRecordHistoryEntryResetHistoryToCurrentStateMoveToHistoryIndexEnsureHistoryEnabled,统一处理历史更新和游标移动
  • 添加 Undo/Redo/TimeTravelTo/ClearHistory 公共 API,并与 dispatch 闸门和监听器通知逻辑协同
GFramework.Core/StateManagement/Store.cs
GFramework.Core.Abstractions/StateManagement/IStore.cs
GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs
GFramework.Core.Abstractions/StateManagement/StoreHistoryEntry.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
docs/zh-CN/core/state-management.md
引入可嵌套的批量状态通知,并将其集成到 dispatch 和 EventBus 桥接逻辑中。
  • Store<TState> 中添加批量深度、待发布状态和标志位,并通过 IsBatching 暴露诊断信息
  • 实现 RunInBatch(Action),在进入/退出批量调用时增加/减少批量深度,并在最外层退出时将多次通知折叠为一次最终通知
  • 重构监听器快照捕获逻辑到 CaptureListenersOrDeferNotificationNotifyListeners,以正确处理批量语义
  • 确保 EventBus 状态变更桥接通过 Store 的订阅机制工作,从而继承状态通知折叠语义
GFramework.Core/StateManagement/Store.cs
GFramework.Core.Abstractions/StateManagement/IStore.cs
GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs
GFramework.Core/Extensions/StoreEventBusExtensions.cs
GFramework.Core/StateManagement/StoreStateChangedEvent.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
GFramework.Core.Tests/StateManagement/StoreEventBusExtensionsTests.cs
docs/zh-CN/core/state-management.md
支持可配置的多态 Action 匹配,具有确定性的 Reducer 顺序,并可通过 Builder 配置。
  • 添加 StoreActionMatchingMode 枚举和 Store 级字段,用于控制匹配行为
  • 重构 reducer 注册,记录全局递增的序列号,以在不同桶之间提供稳定的执行顺序
  • 将中间件/Reducer 快照创建拆分为 CreateMiddlewareSnapshotCore/CreateReducerSnapshotCore,假设调用方已持有锁
  • 实现 TryCreateReducerMatch/GetInheritanceDistance 以及 ReducerMatch 帮助类型,用于在精确类型、基类和接口类型之间计算并排序匹配结果
  • 通过诊断暴露 ActionMatchingMode,并添加默认/文档化行为以及针对精确匹配与多态匹配模式的测试
GFramework.Core.Abstractions/StateManagement/StoreActionMatchingMode.cs
GFramework.Core/StateManagement/Store.cs
GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
docs/zh-CN/core/state-management.md
扩展 StoreBuilder 以配置历史容量和 Action 匹配方式,并相应地连接到构造函数。
  • StoreBuilder<TState> 中添加 _historyCapacity_actionMatchingMode 字段及默认值
  • StoreBuilder.Build 接线为向 Store 构造函数传递 comparer、历史容量和 Action 匹配模式
  • IStoreBuilderStoreBuilder 上暴露 WithHistoryCapacity(int)WithActionMatching(StoreActionMatchingMode),并添加参数校验
  • 重新排序/新增 AddReducer 重载,以保持流式配置 API 的一致性
  • 更新文档,展示如何通过 Builder 配置历史记录和多态匹配
GFramework.Core/StateManagement/StoreBuilder.cs
GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
docs/zh-CN/core/state-management.md
添加 EventBus 桥接扩展与事件,使遗留的 EventBus 使用方可以观察 Store 的活动。
  • 引入 StoreEventBusExtensions,提供 BridgeToEventBusBridgeDispatchesToEventBusBridgeStateChangesToEventBus 辅助方法
  • 实现 DispatchEventBusMiddleware<TState>,在每次 dispatch 时发布 StoreDispatchedEvent<TState>
  • 添加 StoreDispatchedEvent<TState>StoreStateChangedEvent<TState> 作为 EventBus 的封装事件类型
  • 添加测试,验证 dispatch 事件与折叠后的状态变更事件,以及在取消注册桥接后不再发布新事件
  • 在文档中说明 EventBus 桥接行为及推荐用法,作为迁移层
GFramework.Core/Extensions/StoreEventBusExtensions.cs
GFramework.Core/StateManagement/StoreDispatchedEvent.cs
GFramework.Core/StateManagement/StoreStateChangedEvent.cs
GFramework.Core.Tests/StateManagement/StoreEventBusExtensionsTests.cs
docs/zh-CN/core/state-management.md

提示与命令

与 Sourcery 交互

  • 触发新评审: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub Issue: 在某条评审评论下回复请求 Sourcery 从该评论创建 issue。你也可以直接回复 @sourcery-ai issue,从该评论创建 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 描述正文的任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。也可以在 Pull Request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成 Reviewer's Guide: 在 Pull Request 中评论 @sourcery-ai guide,即可随时(重新)生成评审指南。
  • 一次性解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。适用于你已经处理完所有评论并且不希望再看到它们时。
  • 一次性关闭所有 Sourcery 评审: 在 Pull Request 中评论 @sourcery-ai dismiss,即可关闭所有现有的 Sourcery 评审。尤其适用于你希望以一次全新的评审开始——别忘了再评论 @sourcery-ai review 以触发新的评审!

自定义你的体验

访问你的 控制面板 以:

  • 启用或禁用评审功能,例如 Sourcery 自动生成的 Pull Request 摘要、评审指南等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审指令。
  • 调整其他评审设置。

获取帮助

Original review guide in English

Reviewer's Guide

Extends the Store state container with optional history (undo/redo/time-travel), batched notifications, polymorphic action matching, EventBus bridge events, and corresponding builder options, plus tests and documentation updates.

Sequence diagram for dispatch with history tracking and batched notifications

sequenceDiagram
    actor Client
    participant Store_TState_ as Store
    participant MiddlewareChain
    participant Reducers
    participant Subscribers

    rect rgb(235,235,255)
      Client->>Store_TState_: RunInBatch(batchAction)
      activate Store_TState_
      Store_TState_->>Store_TState_: increment _batchDepth
      Store_TState_->>Store_TState_: execute batchAction()

      note over Client,Store_TState_: Inside batchAction, multiple Dispatch calls
    end

    loop each action in batch
      Client->>Store_TState_: Dispatch_TAction_(action)
      activate Store_TState_
      Store_TState_->>Store_TState_: EnsureNotDispatching()
      Store_TState_->>Store_TState_: CreateMiddlewareSnapshotCore()
      Store_TState_->>Store_TState_: CreateReducerSnapshotCore(action.GetType())
      Store_TState_->>MiddlewareChain: ExecuteDispatchPipeline(context, middlewares, reducers)
      activate MiddlewareChain
      MiddlewareChain->>Reducers: ApplyReducers(context)
      activate Reducers
      Reducers-->>MiddlewareChain: newState
      deactivate Reducers
      MiddlewareChain-->>Store_TState_: context.NextState, context.DispatchedAt
      deactivate MiddlewareChain

      Store_TState_->>Store_TState_: ApplyCommittedStateChange(nextState, changedAt, action)
      Store_TState_->>Store_TState_: CaptureListenersOrDeferNotification(nextState, out notificationState)
      alt batching active (_batchDepth > 0)
        Store_TState_->>Store_TState_: store pendingBatchState, hasPendingBatchNotification = true
        Store_TState_-->>Client: return without notifying
      else not batching
        Store_TState_->>Subscribers: NotifyListeners(listenersSnapshot, notificationState)
      end
      deactivate Store_TState_
    end

    rect rgb(235,255,235)
      Store_TState_->>Store_TState_: decrement _batchDepth
      alt leaving outermost batch and hasPendingBatchNotification
        Store_TState_->>Store_TState_: listenersSnapshot = SnapshotListenersForNotification(pendingBatchState)
        Store_TState_->>Store_TState_: clear pendingBatchState, hasPendingBatchNotification
        Store_TState_->>Subscribers: NotifyListeners(listenersSnapshot, pendingBatchState)
      else inner batch or no state change
        Store_TState_-->>Client: no notification
      end
      deactivate Store_TState_
    end

    note over Store_TState_: Each ApplyCommittedStateChange also RecordHistoryEntry to history buffer when enabled
Loading

Sequence diagram for Store to EventBus bridge on dispatch and state change

sequenceDiagram
    actor Client
    participant Store_TState_ as Store
    participant EventBus
    participant DispatchMiddleware as DispatchEventBusMiddleware_TState_
    participant StateBridge as StoreSubscriber
    participant DispatchConsumer
    participant StateConsumer

    rect rgb(235,235,255)
      Client->>Store_TState_: BridgeToEventBus_TState_(eventBus)
      activate Store_TState_
      Store_TState_->>Store_TState_: RegisterMiddleware(DispatchEventBusMiddleware_TState_)
      Store_TState_->>Store_TState_: Subscribe(state => Send StoreStateChangedEvent_TState_)
      Store_TState_-->>Client: bridge IUnRegister
      deactivate Store_TState_

      EventBus->>DispatchConsumer: Register<StoreDispatchedEvent_TState_>()
      EventBus->>StateConsumer: Register<StoreStateChangedEvent_TState_>()
    end

    rect rgb(235,255,235)
      Client->>Store_TState_: Dispatch_TAction_(action)
      activate Store_TState_
      Store_TState_->>DispatchMiddleware: Invoke(context, next)
      activate DispatchMiddleware
      DispatchMiddleware->>Store_TState_: next() (continue dispatch pipeline)
      Store_TState_->>Store_TState_: reducers update State
      Store_TState_->>StateBridge: NotifyListeners(State)
      activate StateBridge
      StateBridge->>EventBus: Send(StoreStateChangedEvent_TState_)
      deactivate StateBridge

      DispatchMiddleware->>EventBus: Send(StoreDispatchedEvent_TState_)
      deactivate DispatchMiddleware
      deactivate Store_TState_
    end

    EventBus-->>DispatchConsumer: StoreDispatchedEvent_TState_
    EventBus-->>StateConsumer: StoreStateChangedEvent_TState_

    note over Store_TState_,EventBus: In batched dispatch, StoreStateChangedEvent_TState_ is only sent once with final state
Loading

Updated class diagram for Store state management with history, batching, polymorphic matching, and EventBus bridge

classDiagram
    direction LR

    class IStore_TState_ {
      <<interface>>
      +TState State
      +bool CanUndo
      +bool CanRedo
      +void Dispatch_TAction_(TAction action)
      +void RunInBatch(Action batchAction)
      +void Undo()
      +void Redo()
      +void TimeTravelTo(int historyIndex)
      +void ClearHistory()
    }

    class IReadonlyStore_TState_ {
      <<interface>>
      +TState State
      +IUnRegister Subscribe(Action_TState_ listener)
    }

    class IStoreDiagnostics_TState_ {
      <<interface>>
      +StoreDispatchRecord_TState_ LastDispatchRecord
      +StoreActionMatchingMode ActionMatchingMode
      +int HistoryCapacity
      +int HistoryCount
      +int HistoryIndex
      +IReadOnlyList_StoreHistoryEntry_TState__ HistoryEntries
      +bool IsBatching
    }

    class IStoreBuilder_TState_ {
      <<interface>>
      +IStoreBuilder_TState_ UseMiddleware(IStoreMiddleware_TState_ middleware)
      +IStoreBuilder_TState_ WithComparer(IEqualityComparer_TState_ comparer)
      +IStoreBuilder_TState_ WithHistoryCapacity(int historyCapacity)
      +IStoreBuilder_TState_ WithActionMatching(StoreActionMatchingMode actionMatchingMode)
      +IStoreBuilder_TState_ AddReducer_TAction_(IReducer_TState__TAction_ reducer)
      +IStoreBuilder_TState_ AddReducer_TAction_(Func_TState__TAction__TState_ reducer)
      +IStore_TState_ Build(TState initialState)
    }

    class Store_TState_ {
      +Store_TState_(TState initialState, IEqualityComparer_TState_ comparer, int historyCapacity, StoreActionMatchingMode actionMatchingMode)
      +TState State
      +bool CanUndo
      +bool CanRedo
      +bool IsBatching
      +StoreActionMatchingMode ActionMatchingMode
      +int HistoryCapacity
      +int HistoryCount
      +int HistoryIndex
      +IReadOnlyList_StoreHistoryEntry_TState__ HistoryEntries
      +void Dispatch_TAction_(TAction action)
      +void RunInBatch(Action batchAction)
      +void Undo()
      +void Redo()
      +void TimeTravelTo(int historyIndex)
      +void ClearHistory()
      +IUnRegister RegisterReducer_TAction_(IReducer_TState__TAction_ reducer)
      +IUnRegister RegisterMiddleware(IStoreMiddleware_TState_ middleware)
      -void ApplyCommittedStateChange(TState nextState, DateTimeOffset changedAt, object action)
      -Action_TState_[] CaptureListenersOrDeferNotification(TState nextState, out TState notificationState)
      -IStoreReducerAdapter[] CreateReducerSnapshotCore(Type actionType)
      -void RecordHistoryEntry(TState state, DateTimeOffset recordedAt, object action)
      -void ResetHistoryToCurrentState(DateTimeOffset recordedAt)
      -void MoveToHistoryIndex(int historyIndexOrOffset, bool isRelative, string argumentName, string emptyHistoryMessage)
      -void EnsureHistoryEnabled()
      -static bool TryCreateReducerMatch(Type actionType, Type registeredActionType, out int matchCategory, out int inheritanceDistance)
      -static int GetInheritanceDistance(Type actionType, Type registeredActionType)
      -static void NotifyListeners(Action_TState_[] listenersSnapshot, TState state)
    }

    class StoreBuilder_TState_ {
      +StoreBuilder_TState_()
      -List_Action_Store_TState___ configurators
      -IEqualityComparer_TState_ comparer
      -int historyCapacity
      -StoreActionMatchingMode actionMatchingMode
      +IStoreBuilder_TState_ UseMiddleware(IStoreMiddleware_TState_ middleware)
      +IStoreBuilder_TState_ WithComparer(IEqualityComparer_TState_ comparer)
      +IStoreBuilder_TState_ WithHistoryCapacity(int historyCapacity)
      +IStoreBuilder_TState_ WithActionMatching(StoreActionMatchingMode actionMatchingMode)
      +IStoreBuilder_TState_ AddReducer_TAction_(Func_TState__TAction__TState_ reducer)
      +IStoreBuilder_TState_ AddReducer_TAction_(IReducer_TState__TAction_ reducer)
      +IStore_TState_ Build(TState initialState)
    }

    class StoreHistoryEntry_TState_ {
      +StoreHistoryEntry_TState_(TState state, DateTimeOffset recordedAt, object action)
      +TState State
      +DateTimeOffset RecordedAt
      +object Action
      +Type ActionType
    }

    class StoreActionMatchingMode {
      <<enumeration>>
      ExactTypeOnly
      IncludeAssignableTypes
    }

    class StoreDispatchedEvent_TState_ {
      +StoreDispatchedEvent_TState_(StoreDispatchRecord_TState_ dispatchRecord)
      +StoreDispatchRecord_TState_ DispatchRecord
    }

    class StoreStateChangedEvent_TState_ {
      +StoreStateChangedEvent_TState_(TState state, DateTimeOffset changedAt)
      +TState State
      +DateTimeOffset ChangedAt
    }

    class StoreEventBusExtensions {
      <<static>>
      +IUnRegister BridgeToEventBus_TState_(Store_TState_ store, IEventBus eventBus, bool publishDispatches, bool publishStateChanges)
      +IUnRegister BridgeDispatchesToEventBus_TState_(Store_TState_ store, IEventBus eventBus)
      +IUnRegister BridgeStateChangesToEventBus_TState_(IReadonlyStore_TState_ store, IEventBus eventBus)
    }

    class DispatchEventBusMiddleware_TState_ {
      +DispatchEventBusMiddleware_TState_(IEventBus eventBus)
      +void Invoke(StoreDispatchContext_TState_ context, Action next)
    }

    class ReducerRegistration {
      -ReducerRegistration(IStoreReducerAdapter adapter, long sequence)
      +IStoreReducerAdapter Adapter
      +long Sequence
    }

    class ReducerMatch {
      -ReducerMatch(IStoreReducerAdapter adapter, long sequence, int matchCategory, int inheritanceDistance)
      +IStoreReducerAdapter Adapter
      +long Sequence
      +int MatchCategory
      +int InheritanceDistance
    }

    Store_TState_ ..|> IStore_TState_
    Store_TState_ ..|> IReadonlyStore_TState_
    Store_TState_ ..|> IStoreDiagnostics_TState_

    StoreBuilder_TState_ ..|> IStoreBuilder_TState_

    Store_TState_ --> StoreHistoryEntry_TState_ : uses
    Store_TState_ --> ReducerRegistration : contains
    Store_TState_ --> ReducerMatch : uses for matching
    Store_TState_ --> StoreActionMatchingMode : config

    IStoreDiagnostics_TState_ --> StoreHistoryEntry_TState_ : exposes

    StoreDispatchedEvent_TState_ --> StoreDispatchRecord_TState_ : wraps
    StoreStateChangedEvent_TState_ --> Store_TState_ : state snapshot

    StoreEventBusExtensions --> Store_TState_ : extension
    StoreEventBusExtensions --> IReadonlyStore_TState_ : extension
    StoreEventBusExtensions --> IEventBus : bridge
    DispatchEventBusMiddleware_TState_ ..|> IStoreMiddleware_TState_
    DispatchEventBusMiddleware_TState_ --> IEventBus : publishes

    StoreBuilder_TState_ --> Store_TState_ : builds
    StoreBuilder_TState_ --> StoreActionMatchingMode : config
    StoreBuilder_TState_ --> IStoreMiddleware_TState_ : config
    StoreBuilder_TState_ --> IReducer_TState__TAction_ : config
Loading

File-Level Changes

Change Details Files
Add optional history buffer with undo/redo/time-travel APIs and diagnostics surface.
  • Extend Store with history list, capacity and index tracking, and constructor parameter to enable history
  • Expose CanUndo/CanRedo and history-related diagnostics (capacity, count, index, entries) on IStore and IStoreDiagnostics
  • Implement ApplyCommittedStateChange, RecordHistoryEntry, ResetHistoryToCurrentState, MoveToHistoryIndex and EnsureHistoryEnabled to centralize history updates and cursor movement
  • Add Undo/Redo/TimeTravelTo/ClearHistory public APIs that coordinate with dispatch gate and listener notification logic
GFramework.Core/StateManagement/Store.cs
GFramework.Core.Abstractions/StateManagement/IStore.cs
GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs
GFramework.Core.Abstractions/StateManagement/StoreHistoryEntry.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
docs/zh-CN/core/state-management.md
Introduce batched state notification with nesting support and integrate it into dispatch and EventBus bridge.
  • Add batch depth, pending state and flags to Store, plus IsBatching diagnostics
  • Implement RunInBatch(Action) that increments batch depth and collapses notifications to a single final publish at outermost exit
  • Refactor listener snapshot capture into CaptureListenersOrDeferNotification and NotifyListeners to respect batching
  • Ensure EventBus state-change bridge uses Store subscriptions so it inherits notification collapsing semantics
GFramework.Core/StateManagement/Store.cs
GFramework.Core.Abstractions/StateManagement/IStore.cs
GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs
GFramework.Core/Extensions/StoreEventBusExtensions.cs
GFramework.Core/StateManagement/StoreStateChangedEvent.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
GFramework.Core.Tests/StateManagement/StoreEventBusExtensionsTests.cs
docs/zh-CN/core/state-management.md
Support configurable polymorphic action matching with deterministic reducer ordering and builder configuration.
  • Add StoreActionMatchingMode enum and store-level field to control matching behavior
  • Refactor reducer registration to record a global sequence number for stable ordering across buckets
  • Split middleware/reducer snapshot creation into CreateMiddlewareSnapshotCore/CreateReducerSnapshotCore that assume caller holds the lock
  • Implement TryCreateReducerMatch/GetInheritanceDistance and ReducerMatch helper to compute and sort matches across exact, base, and interface types
  • Expose ActionMatchingMode via diagnostics and add default/documented behavior plus tests for exact vs polymorphic modes
GFramework.Core.Abstractions/StateManagement/StoreActionMatchingMode.cs
GFramework.Core/StateManagement/Store.cs
GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
docs/zh-CN/core/state-management.md
Extend StoreBuilder to configure history capacity and action matching, and wire constructor accordingly.
  • Add _historyCapacity and _actionMatchingMode fields with defaults on StoreBuilder
  • Wire StoreBuilder.Build to pass comparer, history capacity and action matching mode into Store constructor
  • Expose WithHistoryCapacity(int) and WithActionMatching(StoreActionMatchingMode) on IStoreBuilder and StoreBuilder with validation
  • Reorder/Add AddReducer overloads to keep fluent configuration consistent
  • Update docs to show using builder to configure history and polymorphic matching
GFramework.Core/StateManagement/StoreBuilder.cs
GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs
GFramework.Core.Tests/StateManagement/StoreTests.cs
docs/zh-CN/core/state-management.md
Add EventBus bridge extensions and events so legacy EventBus consumers can observe Store activity.
  • Introduce StoreEventBusExtensions with BridgeToEventBus, BridgeDispatchesToEventBus and BridgeStateChangesToEventBus helpers
  • Implement DispatchEventBusMiddleware to publish StoreDispatchedEvent for each dispatch
  • Add StoreDispatchedEvent and StoreStateChangedEvent envelope types for EventBus
  • Add tests verifying dispatch events and collapsed state-change events, and that un-registering the bridge stops future publications
  • Document EventBus bridge behavior and recommended usage as migration layer
GFramework.Core/Extensions/StoreEventBusExtensions.cs
GFramework.Core/StateManagement/StoreDispatchedEvent.cs
GFramework.Core/StateManagement/StoreStateChangedEvent.cs
GFramework.Core.Tests/StateManagement/StoreEventBusExtensionsTests.cs
docs/zh-CN/core/state-management.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepsource-io

deepsource-io Bot commented Mar 24, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in b912e6a...e5da5aa on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
C# Mar 24, 2026 11:46a.m. Review ↗
Secrets Mar 24, 2026 11:46a.m. Review ↗

Comment on lines +303 to +321
finally
{
lock (_lock)
{
if (_batchDepth == 0)
{
throw new InvalidOperationException("Batch depth is already zero.");
}

_batchDepth--;
if (_batchDepth == 0 && _hasPendingBatchNotification)
{
notificationState = _pendingBatchState;
_pendingBatchState = default!;
_hasPendingBatchNotification = false;
listenersSnapshot = SnapshotListenersForNotification(notificationState);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception being thrown from `finally` block


The finally block is used to clean up resources that may have been allocated in the try block. Throwing any exceptions from the finally block may prevent it from executing fully and also affect the stack trace. It is therefore recommended that you switch to a more suitable alternative.

Comment thread GFramework.Core/StateManagement/Store.cs
Comment thread GFramework.Core/StateManagement/Store.cs

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="GFramework.Core/StateManagement/Store.cs" line_range="916-925" />
<code_context>
+    private IStoreMiddleware<TState>[] CreateMiddlewareSnapshotCore()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 请考虑在运行时强制检查 “必须持有 _lock” 这一前置条件,以避免对快照辅助方法的误用。

这些 `private` 辅助方法假定调用方已经持有 `_lock`,但这一约定目前只在 XML 中被文档化。建议添加一个防御性的运行时检查(例如 `Debug.Assert(Monitor.IsEntered(_lock))`)来强制该前置条件,并在未来有调用方在未持有锁的情况下调用这些方法时尽早暴露问题,从而避免重构后出现隐蔽的并发 Bug。

建议实现:

```csharp
    /// <returns>当前中间件链的快照;若未注册则返回空数组。</returns>
    private IStoreMiddleware<TState>[] CreateMiddlewareSnapshotCore()
    {
        System.Diagnostics.Debug.Assert(
            System.Threading.Monitor.IsEntered(_lock),
            "Caller must hold _lock before invoking CreateMiddlewareSnapshotCore to avoid concurrency bugs."
        );

        if (_middlewares.Count == 0)
        {

```

如果你的代码库更倾向在命名空间级别使用 `using` 指令来引入 `System.Diagnostics` 和 `System.Threading`,也可以在文件顶部添加:
- `using System.Diagnostics;`
- `using System.Threading;`
然后将断言简化为 `Debug.Assert(Monitor.IsEntered(_lock));`。
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,欢迎分享 ✨
请帮我变得更有用!欢迎在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="GFramework.Core/StateManagement/Store.cs" line_range="916-925" />
<code_context>
+    private IStoreMiddleware<TState>[] CreateMiddlewareSnapshotCore()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider enforcing the "must hold _lock" precondition at runtime to avoid accidental misuse of the snapshot helpers.

These `private` helpers assume the caller already holds `_lock`, but that contract is only documented in XML. Consider adding a defensive runtime check (e.g. `Debug.Assert(Monitor.IsEntered(_lock))`) to enforce the precondition and surface any future call sites that invoke them without the lock, avoiding subtle concurrency bugs after refactors.

Suggested implementation:

```csharp
    /// <returns>当前中间件链的快照;若未注册则返回空数组。</returns>
    private IStoreMiddleware<TState>[] CreateMiddlewareSnapshotCore()
    {
        System.Diagnostics.Debug.Assert(
            System.Threading.Monitor.IsEntered(_lock),
            "Caller must hold _lock before invoking CreateMiddlewareSnapshotCore to avoid concurrency bugs."
        );

        if (_middlewares.Count == 0)
        {

```

If your codebase prefers namespace-level `using` directives for `System.Diagnostics` and `System.Threading`, you can instead add:
- `using System.Diagnostics;`
- `using System.Threading;`
at the top of the file and then simplify the assertion to `Debug.Assert(Monitor.IsEntered(_lock));`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread GFramework.Core/StateManagement/Store.cs
- 在 CreateMiddlewareSnapshotCore 方法中添加锁持有检查
- 在 CreateReducerSnapshotCore 方法中添加锁持有检查
- 确保并发安全性避免竞态条件
- 导入 System.Diagnostics 命名空间支持断言功能
Comment thread GFramework.Core/StateManagement/Store.cs Outdated
Comment thread GFramework.Core/StateManagement/Store.cs
- 将 HistoryEntries 属性替换为 GetHistoryEntriesSnapshot() 方法,明确表达会返回集合副本
- 在批处理清理逻辑中添加 else 条件避免无效操作
- 更新测试代码使用新的快照获取方法
- 添加 System.Diagnostics 命名空间引用
- 修复批处理深度为零时的异常处理逻辑
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant