Skip to content

StatePulse.NET enables fast, consistent state/action dispatch with optional tracking and anti-duplicate flow control. It supports ordered chaining when needed, while maintaining high-performance fire-and-yield behavior for general use cases.

License

Notifications You must be signed in to change notification settings

mshimshon/StatePulse.NET

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

121 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

License: MIT NuGet Version Build Deploy

StatePulse.NET

What is StatePulse.NET?

What is StatePulse.NET?

StatePulse.NET is a precision‑engineered state and action management system designed for high‑performance fire‑and‑yield workflows. It supports optional, internally controlled execution ordering when deterministic sequencing is explicitly required. Its multi‑layer anti‑duplication pipeline eliminates redundant dispatches, prevents race conditions, and maintains consistent outcomes even under rapid input or concurrent triggers. A lightweight internal tracking core provides near‑zero‑overhead cancellation and dispatch control, minimizing inconsistency without sacrificing throughput. Despite these guarantees, StatePulse.NET preserves the flexibility of traditional untracked state management, allowing developers to selectively enforce ordering and reliability without introducing global locks or compromising responsiveness.

Getting Started

What is StatePulse.NET?

Features

Fast Fire-and-Yield
Executes actions immediately, including tracked actions, while preserving fire-and-yield semantics.

Multi-Layer Anti-Duplicate Dispatching Layer 1: Cancels previously dispatched duplicates before effects, between effects, or after effects, ensuring no redundant action progresses through the pipeline. Layer 2: Uses a global state-change ticker enforcing a strict “latest action wins” rule so outdated or superseded actions cannot update state.

Effect Validator System
Supports multiple, composable validators for modular and reusable rule enforcement.

Synchronous Debug Mode
Provides an optional lockstep execution mode ideal for testing, diagnostics, and Task.WhenAll based pipelines.

DispatchTracker
Offers high-performance cancellation, deduplication, and concurrency control through an optimized tracking mechanism.

Short-Lived Middlewares Provides lightweight, disposable middleware hooks that run only during the lifetime of a single dispatch cycle.

Dispatch Middlewares Runs Before, After, and WhenDispatchFails. In asynchronous dispatch modes, failures are silently discarded unless handled internally, so DEC logic should manage its own errors; a logging middleware can also capture unhandled pipeline failures.

Effect Middlewares Runs Before, After, WhenValidationFails, and WhenValidationSucceed, allowing fine‑grained control and instrumentation around effect execution and validation flow.

Reducer Middlewares Runs Before and After the reducer, enabling patterns such as event dispatch on state changes, logging, instrumentation, or enforcing reducer‑level invariants.

🚀 State Management with Zero Boilerplate and Zero Compromises

  • Lazy State Access Model: Inject IStatePulse directly into your Blazor component and call StateOf<TState>(()=>this, TaskMethod) to get scoped state access.
  • Component-Scoped Event Listening: Automatically registers event listeners only for that component, ensuring StateHasChanged() is called exclusively on components subscribed to state changes.
  • No Base Classes or Global Event Listeners: Avoids global re-renders and boilerplate base class inheritance, giving you fine-grained control over component rendering and event subscription without forcing you into base classes.
  • Automatic Listener Disposal: Event listeners are automatically tracked and disposed with the component lifecycle, preventing memory leaks and dangling references.
  • Transient IStatePulse Service: Each component gets its own IStatePulse instance, isolating event subscriptions and making state updates scoped and efficient.

Benchmark

Method Mean Error StdDev
StatePulse_Dispatch 2.463 μs 0.0161 μs 0.0134 μs
StatePulse_SafeDispatch 3.838 μs 0.0712 μs 0.1501 μs
StatePulse_BusrtDispatch 332.804 μs 6.0721 μs 5.3828 μs
StatePulse_BusrtSafeDispatch 376.511 μs 7.4720 μs 14.3960 μs
StatePulse_FireYieldDispatch 3.353 μs 0.0669 μs 0.1702 μs
StatePulse_FireYield_SequentialEffectsDispatch 3.235 μs 0.0367 μs 0.0307 μs
StatePulse_AwaitedDispatch 3.292 μs 0.0470 μs 0.0417 μs

StatePulse delivers strong performance given its feature set, but it’s not designed for tight, high‑frequency loops. Long‑term performance improvements are planned, as there are several areas with optimization potential. For now, the priority remains system stability, configuration robustness, and feature completeness.

📦 Installation & Setup

Install-Package StatePulse.Net

dotnet add package StatePulse.Net

3 Ways to Register Services

Method 1
The most deterministic and explicit registration approach. This method avoids “magic” and one‑liners by requiring you to manually add all Reducers, Effects, Middlewares, Validators, and Actions. It provides full clarity and control over what the system loads.

    ServiceCollection.AddStatePulseServices(o =>
    {
        o.AutoRegisterTypes = [
                typeof(MainMenuLoaderStartAction),
                typeof(MainMenuLoaderStopAction),
                typeof(MainMenuLoadNavigationItemsAction),
                typeof(MainMenuLoadNavigationItemsResultAction),
                typeof(MainMenuOpenAction),
                typeof(ProfileCardDefineAction),
                typeof(ProfileCardDefineResultAction),
                typeof(ProfileCardLoaderStartAction),
                typeof(ProfileCardLoaderStopAction),
                typeof(UpdateCounterAction),
                typeof(ProfileCardDefineEffect),
                typeof(ProfileCardDefineResultAction),
                typeof(MainMenuLoadNavigationItemsEffect),
                typeof(MainMenuOpenEffect),
                typeof(MainMenuOpenEffectValidation),
                typeof(ProfileCardDefineActionValidator),
                typeof(MainMenuLoaderStartReducer),
                typeof(MainMenuLoaderStopReducer),
                typeof(MainMenuLoadNavigationItemsResultReducer),
                typeof(MainMenuOpenReducer),
                typeof(ProfileCardDefineResultReducer),
                typeof(UpdateCounterReducer),
                typeof(ProfileCardState),
                typeof(MainMenuState),
                typeof(CounterState),
            ];
    });

Method 2
This is also very explicit since v2+ we have a single entry AddStatePulseService for all statepulse types (Reducers, Effects, Middlewares, Validators, and Actions).

    ServiceCollection.AddStatePulseServices();
    ServiceCollection.AddStatePulseService<MainMenuLoaderStartAction>();
    ServiceCollection.AddStatePulseService<MainMenuLoaderStopAction>();
    ServiceCollection.AddStatePulseService<MainMenuLoadNavigationItemsAction>();
    ServiceCollection.AddStatePulseService<MainMenuLoadNavigationItemsResultAction>();
    ServiceCollection.AddStatePulseService<MainMenuOpenAction>();
    ServiceCollection.AddStatePulseService<ProfileCardDefineAction>();
    ServiceCollection.AddStatePulseService<ProfileCardDefineResultAction>();
    ServiceCollection.AddStatePulseService<ProfileCardLoaderStartAction>();
    ServiceCollection.AddStatePulseService<ProfileCardLoaderStopAction>();
    ServiceCollection.AddStatePulseService<UpdateCounterAction>();
    ServiceCollection.AddStatePulseService<ProfileCardDefineEffect>();
    ServiceCollection.AddStatePulseService<MainMenuLoadNavigationItemsEffect>();
    ServiceCollection.AddStatePulseService<MainMenuOpenEffect>();

    ServiceCollection.AddStatePulseService<MainMenuOpenEffectValidation>();
    ServiceCollection.AddStatePulseService<ProfileCardDefineActionValidator>();

    ServiceCollection.AddStatePulseService<MainMenuLoaderStartReducer>();
    ServiceCollection.AddStatePulseService<MainMenuLoaderStopReducer>();
    ServiceCollection.AddStatePulseService<MainMenuLoadNavigationItemsResultReducer>();
    ServiceCollection.AddStatePulseService<MainMenuOpenReducer>();
    ServiceCollection.AddStatePulseService<ProfileCardDefineResultReducer>();
    ServiceCollection.AddStatePulseService<UpdateCounterReducer>();
    ServiceCollection.AddStatePulseService<ProfileCardState>();
    ServiceCollection.AddStatePulseService<MainMenuState>();

    ServiceCollection.AddStatePulseService<CounterState>();

Method 3 The assembly-scan approach. Convenient but not recommended for most scenarios. While useful for rapid setup, it can introduce problems as system grows.

    ServiceCollection.AddStatePulseServices(o => {
        o.ScanAssemblies = [typeof(TestBase).Assembly];
    });

🧭 How It Works

Define Actions:

// IAction { }
// ISafeAction { } // Cannot be dispatched unsafely

public record ProfileCardDefineAction : IAction
{
    public string? TestData { get; set; }
}

Define Effect:

internal class ProfileCardDefineEffect : IEffect<ProfileCardDefineAction>
{

    public ProfileCardDefineEffect()
    {
    }
    public async Task EffectAsync(ProfileCardDefineAction action, IDispatcher dispatcher)
    {
        var random = new Random();
        int value = random.Next(100, 1001); // Upper bound is exclusive, so use 1001
        await Task.Delay(value);
        var myProfile = new UserResponse();
        await dispatcher.Prepare(() => new ProfileCardDefineResultAction(action.TestData ?? myProfile.Name, myProfile.Picture, myProfile.Id))
            .DispatchAsync();
    }

}



### **Define Effect Validator** (Optional):

```csharp
/*
* This is the best way to define clean conditional effects, it either run or not... this is not meant for triggering errors.
* This is meant for optional/condition effects to either run or not base on the action settings...
*/
internal class ProfileCardDefineActionValidator : IEffectValidator<ProfileCardDefineAction, ProfileCardDefineEffect>
{
    public Task<bool> Validate(ProfileCardDefineAction action)
    {
        if (action.TestData == "Error")
            return Task.FromResult(false);
        return Task.FromResult(true);
    }
}

### **Define Reducer**:

```csharp
internal class ProfileCardDefineResultReducer : IReducer<ProfileCardState, ProfileCardDefineResultAction>
{
    public Task<ProfileCardState> ReduceAsync(ProfileCardState state, ProfileCardDefineResultAction action)
        => Task.FromResult(state with
        {
            LastUpdate = DateTime.UtcNow,
            ProfileId = action.Id,
            ProfileName = action.Name,
            ProfilePicture = action.Picture
        });
}

Define StateFeature:

public sealed record ProfileCardState : IStateFeature
{
    public string? ProfileName { get; set; }
    public string? ProfilePicture { get; set; }
    public string? ProfileId { get; set; }
    public DateTime LastUpdate { get; set; } = DateTime.UtcNow;
}

Trigger Dispatch:

var dispatcher = ServiceProvider.GetRequiredService<IDispatcher>();
var stateAccessor = ServiceProvider.GetRequiredService<IStateAccessor<ProfileCardState>>();
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
    .DispatchAsync();

// You can Capture the validation in case of failure, only call if validators exist.
ValidationResult? validation = default;
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
    .HandleActionValidation(p => validation = p)
    .DispatchAsync();

// You can trigger synchronously... this will await the whole pipeline, otherwise you just await until action is send to dispatch pool.
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
    .Sync()
    .DispatchAsync();

// if the action is implementing ISafeState, the dispatch will always run asSafe=true but an action not implementing ISafeAction will
// have the option to run asSafe or not...
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
    .DispatchAsync(true);

Important Notes

  • Rule of thumb is always await dispatch calls, avoiding to do so can cause inconsistency for safe dispatch mode..
  • ISafeAction implementations are always dispatched safely, ignoring unsafe flag.
  • synchronous is an anti-pattern of statemanement use it sparingly; it is primarily for debugging or specific scenarios requiring full completion before continuation.

Access State:

var stateAccessor = ServiceProvider.GetRequiredService<IStateAccessor<ProfileCardState>>();

Blazor Example Usage

using StatePulse.Net;

public partial class CounterView : ComponentBase
{

    // METHOD 1:
    [Inject] public IStatePulse PulseState { get; set; } = default!; // Handles State Accessor

    // This is for convienience always use this method or directly PulseState.StateOf<CounterState>(this).Value
    // Never assign State Instance variable as it will not update... 
    // Never use lambda it will throw exception as WeakREference is fundamatally flawed and disposes of lambda even when its object is alive.
    private CounterState state => PulseState.StateOf<CounterState>(()=>this, OnUpdate);
    
    private async Task OnUpdate() => await InvokeAsync(StateHadChanged);

    // METHOD 2: 
    // Inject direct state but injecting the state directly requires you to handle onchanged events by sub/unsub in lifecycle
    // Or to create a basecomponent system similar to other state management systems.
    [Inject] public IStateAccessor<CounterState> State { get; set; } = default!; 

    
}

About

StatePulse.NET enables fast, consistent state/action dispatch with optional tracking and anti-duplicate flow control. It supports ordered chaining when needed, while maintaining high-performance fire-and-yield behavior for general use cases.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published