Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
51 changes: 45 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ concurrency:
jobs:
deploy-abstraction:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' &&
Expand All @@ -38,14 +41,44 @@ jobs:

- name: Pack StatePulse.Net.Abstractions
run: dotnet pack src/StatePulse.Net.Abstractions/StatePulse.Net.Abstractions.csproj -c Release --no-build -o ./artifacts


- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}

- name: Push StatePulse.Net.Abstractions
run: dotnet nuget push ./artifacts/StatePulse.Net.Abstractions.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Wait for NuGet indexing
run: sleep 120
run: dotnet nuget push ./artifacts/StatePulse.Net.Abstractions.*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate

- name: Get package version
id: get_version
run: |
FILE=$(ls ./artifacts/*.nupkg | head -n1)
VERSION=$(basename "$FILE" | sed -E 's/MaksimShimshon.RestCountries\.([0-9]+\.[0-9]+\.[0-9]+)\.nupkg/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Wait for NuGet availability
uses: nick-invision/retry@v2
with:
timeout_minutes: 10
retry_wait_seconds: 10
max_attempts: 60
command: |
body=$(curl -s --compressed \
-H "Accept: application/json" \
"https://api.nuget.org/v3/registration5-gz-semver2/statepulse.net.abstractions/${{ steps.get_version.outputs.version }}.json" \
| tr -d '\000')

echo "$body" | head -c 5 | grep -q "<?xml" && exit 1
echo "$body" | grep -q '"@type"' || exit 1

deploy-core:
needs: deploy-abstraction
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' &&
Expand All @@ -72,6 +105,12 @@ jobs:

- name: Pack StatePulse.NET
run: dotnet pack src/StatePulse.NET/StatePulse.NET.csproj -c Release --no-build -o ./artifacts


- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}

- name: Push StatePulse.NET
run: dotnet nuget push ./artifacts/StatePulse.NET.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
run: dotnet nuget push ./artifacts/StatePulse.NET.*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate
177 changes: 146 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,43 @@

# StatePulse.NET
### [Official Documentation](https://statepulse.net/)
StatePulse.NET is a precision-tuned state and action management system that balances high-performance fire-and-forget operations with optional, internally controlled execution order when explicitly required.
It enables anti-duplication chaining for critical flows, preventing race conditions and ensuring consistent outcomes even under rapid user input or concurrent triggers.
Its internal tracking infrastructure provides near-zero overhead cancellation and dispatch control, drastically reducing inconsistency.
At the same time, it preserves the flexibility of traditional untracked state management, letting developers selectively enforce order and reliability without compromising overall responsiveness or introducing global locks.

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.


## 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.

## ✨ Features
- ⚡ **Fast Fire-and-Forget** — Executes actions immediately even tracked action are fire-and-forget.
- 🛡 **Anti-Duplicate Dispatching** — Prevents redundant or overlapping actions that can cause race condition state inconsistency.
- 🔍 **Effect Validator System** — Supports multiple effect validators for modular and reusable rule enforcement.
- 🧪 **Synchronous Debug Mode** — Optional lockstep mode for testing, diagnostics, and `Task.WhenAll` pipelines.
- 🧵 **DispatchTracker** — High-performance cancellation and deduplication logic via optimized concurrent tracking.

### 🚀 **State Management with Zero Boilerplate and Zero Compromises**

Expand All @@ -29,6 +54,19 @@ At the same time, it preserves the flexibility of traditional untracked state ma
- **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 | Median |
|----------------------------------------------- |-----------:|----------:|----------:|-----------:|
| StatePulse_Dispatch | 2.458 μs | 0.0344 μs | 0.0322 μs | 2.455 μs |
| StatePulse_BusrtDispatch | 321.243 μs | 4.6181 μs | 4.3198 μs | 322.030 μs |
| StatePulse_BusrtSafeDispatch | 350.282 μs | 4.4814 μs | 4.1919 μs | 351.182 μs |
| StatePulse_FireYieldDispatch | 3.193 μs | 0.0631 μs | 0.0675 μs | 3.193 μs |
| StatePulse_FireYield_SequentialEffectsDispatch | 3.326 μs | 0.0661 μs | 0.0969 μs | 3.303 μs |
| StatePulse_AwaitedDispatch | 4.420 μs | 0.6850 μs | 2.0196 μs | 3.165 μ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


Expand All @@ -39,11 +77,85 @@ 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.

```csharp
services.AddStatePulseServices(o =>
{
o.ScanAssemblies = new Type[] { typeof(Program) };
});
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).

```csharp
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.

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

## 🧭 How It Works
Expand All @@ -64,22 +176,6 @@ public record ProfileCardDefineAction : IAction

```

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

```csharp
/*
You are not required to create have an action validator but it is very useful when you have business logic that conditionally only contionally fires.
When validation fails it ignores the dispatch and move on.
*/
internal class ProfileCardDefineActionValidator : IActionValidator<ProfileCardDefineAction>
{
public void Validate(ProfileCardDefineAction action, ref ValidationResult result)
{
if (action.TestData == "Error")
result.AddError("ErrorName", "Name Cannot be Error");
}
}
```

### **Define Effect**:

Expand All @@ -104,6 +200,25 @@ internal class ProfileCardDefineEffect : IEffect<ProfileCardDefineAction>
}



### **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**:
Expand Down Expand Up @@ -162,7 +277,7 @@ await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)

### 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 flags.
- 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**:
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading