diff --git a/README.md b/README.md index bdca94e..7ac4080 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,12 @@ Runs Before and After the reducer, enabling patterns such as event dispatch on s ## 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 | +| Method | Mean | Error | StdDev | Median | +|----------------------------- |-----------:|----------:|-----------:|-----------:| +| StatePulse_Dispatch | 2.240 μs | 0.0405 μs | 0.0720 μs | 2.208 μs | +| StatePulse_SafeDispatch | 2.802 μs | 0.0556 μs | 0.1512 μs | 2.744 μs | +| StatePulse_BusrtDispatch | 232.678 μs | 5.4732 μs | 15.8789 μs | 227.091 μs | +| StatePulse_BusrtSafeDispatch | 284.785 μs | 8.3677 μs | 24.1427 μs | 276.808 μ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. diff --git a/doc/build/404.html b/doc/build/404.html index 21386bc..3be14b4 100644 --- a/doc/build/404.html +++ b/doc/build/404.html @@ -4,7 +4,7 @@ StatePulse.NET - + diff --git a/doc/build/assets/js/22dd74f7.f784a3d1.js b/doc/build/assets/js/22dd74f7.9f52074d.js similarity index 97% rename from doc/build/assets/js/22dd74f7.f784a3d1.js rename to doc/build/assets/js/22dd74f7.9f52074d.js index d4b92bc..32d7fd5 100644 --- a/doc/build/assets/js/22dd74f7.f784a3d1.js +++ b/doc/build/assets/js/22dd74f7.9f52074d.js @@ -1 +1 @@ -"use strict";(self.webpackChunkstatepulse_doc=self.webpackChunkstatepulse_doc||[]).push([[1567],{5226:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"Updates","href":"/versions","docId":"Versions","unlisted":false},{"type":"link","label":"Get Started","href":"/","docId":"Getting Started","unlisted":false},{"type":"link","label":"Setup Blazor Project","href":"/setup-blazor-project","docId":"Setup Blazor Project","unlisted":false},{"type":"link","label":"The Actions","href":"/gs-the-action","docId":"Creating Actions","unlisted":false},{"type":"link","label":"The States","href":"/gs-state","docId":"Create State","unlisted":false},{"type":"link","label":"The Effects","href":"/gs-the-effect","docId":"4 Create Effects","unlisted":false},{"type":"link","label":"The Reducers","href":"/gs-the-reducer","docId":"Create Reducer","unlisted":false},{"type":"link","label":"The Dispatcher","href":"/gs-the-dispatcher","docId":"The Dispatcher","unlisted":false},{"type":"link","label":"The Middlewares","href":"/gs-the-middlewares","docId":"Middlewares","unlisted":false}]},"docs":{"4 Create Effects":{"id":"4 Create Effects","title":"The Effects","description":"What are Effects","sidebar":"tutorialSidebar"},"Create Reducer":{"id":"Create Reducer","title":"The Reducers","description":"Reducers \u2013 Pure State Updates","sidebar":"tutorialSidebar"},"Create State":{"id":"Create State","title":"The States","description":"Defining a State","sidebar":"tutorialSidebar"},"Creating Actions":{"id":"Creating Actions","title":"The Actions","description":"Type of Actions","sidebar":"tutorialSidebar"},"Getting Started":{"id":"Getting Started","title":"Get Started","description":"License: MIT","sidebar":"tutorialSidebar"},"Middlewares":{"id":"Middlewares","title":"The Middlewares","description":"\u2699\ufe0f What are Middlewares?","sidebar":"tutorialSidebar"},"Setup Blazor Project":{"id":"Setup Blazor Project","title":"Setup Blazor Project","description":"\ud83d\udce6 Installation & Setup","sidebar":"tutorialSidebar"},"The Dispatcher":{"id":"The Dispatcher","title":"The Dispatcher","description":"Dispatcher \u2013 Executing Actions in StatePulse","sidebar":"tutorialSidebar"},"Versions":{"id":"Versions","title":"Updates","description":"v2.2.0","sidebar":"tutorialSidebar"}}}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkstatepulse_doc=self.webpackChunkstatepulse_doc||[]).push([[1567],{5226:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"Updates","href":"/versions","docId":"Versions","unlisted":false},{"type":"link","label":"Get Started","href":"/","docId":"Getting Started","unlisted":false},{"type":"link","label":"Setup Blazor Project","href":"/setup-blazor-project","docId":"Setup Blazor Project","unlisted":false},{"type":"link","label":"The Actions","href":"/gs-the-action","docId":"Creating Actions","unlisted":false},{"type":"link","label":"The States","href":"/gs-state","docId":"Create State","unlisted":false},{"type":"link","label":"The Effects","href":"/gs-the-effect","docId":"4 Create Effects","unlisted":false},{"type":"link","label":"The Reducers","href":"/gs-the-reducer","docId":"Create Reducer","unlisted":false},{"type":"link","label":"The Dispatcher","href":"/gs-the-dispatcher","docId":"The Dispatcher","unlisted":false},{"type":"link","label":"The Middlewares","href":"/gs-the-middlewares","docId":"Middlewares","unlisted":false}]},"docs":{"4 Create Effects":{"id":"4 Create Effects","title":"The Effects","description":"What are Effects","sidebar":"tutorialSidebar"},"Create Reducer":{"id":"Create Reducer","title":"The Reducers","description":"Reducers \u2013 Pure State Updates","sidebar":"tutorialSidebar"},"Create State":{"id":"Create State","title":"The States","description":"Defining a State","sidebar":"tutorialSidebar"},"Creating Actions":{"id":"Creating Actions","title":"The Actions","description":"Type of Actions","sidebar":"tutorialSidebar"},"Getting Started":{"id":"Getting Started","title":"Get Started","description":"License: MIT","sidebar":"tutorialSidebar"},"Middlewares":{"id":"Middlewares","title":"The Middlewares","description":"\u2699\ufe0f What are Middlewares?","sidebar":"tutorialSidebar"},"Setup Blazor Project":{"id":"Setup Blazor Project","title":"Setup Blazor Project","description":"\ud83d\udce6 Installation & Setup","sidebar":"tutorialSidebar"},"The Dispatcher":{"id":"The Dispatcher","title":"The Dispatcher","description":"Dispatcher \u2013 Executing Actions in StatePulse","sidebar":"tutorialSidebar"},"Versions":{"id":"Versions","title":"Updates","description":"v3.0.0","sidebar":"tutorialSidebar"}}}}')}}]); \ No newline at end of file diff --git a/doc/build/assets/js/94cf9043.cdbd9e88.js b/doc/build/assets/js/94cf9043.cdbd9e88.js deleted file mode 100644 index 816f467..0000000 --- a/doc/build/assets/js/94cf9043.cdbd9e88.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkstatepulse_doc=self.webpackChunkstatepulse_doc||[]).push([[6683],{8453:(e,n,i)=>{i.d(n,{R:()=>l,x:()=>c});var s=i(6540);const r={},t=s.createContext(r);function l(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:l(e.components),s.createElement(t.Provider,{value:n},e.children)}},8886:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>h,frontMatter:()=>l,metadata:()=>s,toc:()=>a});const s=JSON.parse('{"id":"Versions","title":"Updates","description":"v2.2.0","source":"@site/docs/0.Versions.md","sourceDirName":".","slug":"/versions","permalink":"/versions","draft":false,"unlisted":false,"editUrl":"https://github.com/mshimshon/StatePulse.NET/docs/0.Versions.md","tags":[],"version":"current","sidebarPosition":0,"frontMatter":{"slug":"versions","title":"Updates","sidebar_position":0},"sidebar":"tutorialSidebar","next":{"title":"Get Started","permalink":"/"}}');var r=i(4848),t=i(8453);const l={slug:"versions",title:"Updates",sidebar_position:0},c=void 0,d={},a=[{value:"v2.2.0",id:"v220",level:2},{value:"Fix",id:"fix",level:3},{value:"v2.1.0",id:"v210",level:2},{value:"Performance",id:"performance",level:3},{value:"Fix",id:"fix-1",level:3},{value:"v2.0.1",id:"v201",level:2},{value:"Fixes",id:"fixes",level:3},{value:"v2.0.0",id:"v200",level:2},{value:"BREAKING CHANGES",id:"breaking-changes",level:3},{value:"New Features",id:"new-features",level:3},{value:"Security",id:"security",level:3},{value:"Fixes",id:"fixes-1",level:3},{value:"v1.1.0",id:"v110",level:2},{value:"Minor Change",id:"minor-change",level:3},{value:"v1.0.2",id:"v102",level:2},{value:"Fixes",id:"fixes-2",level:3},{value:"v1.0.1",id:"v101",level:2},{value:"Minor Change",id:"minor-change-1",level:3},{value:"v1.0.0",id:"v100",level:2},{value:"New Features",id:"new-features-1",level:3},{value:"\ud83d\udca5 Breaking Changes",id:"-breaking-changes",level:3},{value:"\ud83d\ude80 Performance Improvements",id:"-performance-improvements",level:3},{value:"\ud83e\uddfc Clean Code Improvements",id:"-clean-code-improvements",level:3},{value:"\ud83d\udc1e Fixes",id:"-fixes",level:3},{value:"v0.9.41",id:"v0941",level:2},{value:"v0.9.4",id:"v094",level:2},{value:"v0.9.21",id:"v0921",level:2},{value:"v0.9.2 (Blazor Packages)",id:"v092-blazor-packages",level:2}];function o(e){const n={br:"br",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h2,{id:"v220",children:"v2.2.0"}),"\n",(0,r.jsx)(n.h3,{id:"fix",children:"Fix"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Major fix of Middleware not able to be registered multiple times therefore prevent multiple middleware to work for same interface."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v210",children:"v2.1.0"}),"\n",(0,r.jsx)(n.h3,{id:"performance",children:"Performance"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The Global Tracker now uses an event\u2011based mechanism to invoke Self Disposal Check directly on each ",(0,r.jsx)(n.code,{children:"IStatePulse"})," instance, removing the need to iterate the internal registry. The registry itself is scheduled for removal in version 3.0 and is now marked with the appropriate ",(0,r.jsx)(n.code,{children:"Obsolete"})," attribute. Cleanup is no longer driven by a continuous loop; instead, the tracker schedules a self\u2011check only when activity occurs in the registry and goes idle when the application becomes inactive. This preserves memory\u2011leak protection without a permanent cycle. A minimum delay of 10 seconds is enforced between checks, and when burst activity occurs, only the final activity triggers a single final cleanup run rather than multiple redundant executions."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"fix-1",children:"Fix"}),"\n",(0,r.jsxs)(n.p,{children:["This is one of those \u201cbug or feature?\u201d situations. By design, ",(0,r.jsx)(n.code,{children:"StateOf()"})," is meant to notify the component when its state changes. However, earlier Blazor assumptions overlooked scenarios where one state might render through one route while another state uses a different route."]}),"\n",(0,r.jsxs)(n.p,{children:["Before version 2.0.11, StatePulse always treated the latest route as the default route for all states registered by a component, this is was ",(0,r.jsx)(n.strong,{children:"BUG"}),". Starting with 2.0.11, each state that a component subscribes to StatePulse now respects those route differences if any."]}),"\n",(0,r.jsx)(n.h2,{id:"v201",children:"v2.0.1"}),"\n",(0,r.jsx)(n.h3,{id:"fixes",children:"Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Moved Fluent API Extension Methods into Abstraction Package."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v200",children:"v2.0.0"}),"\n",(0,r.jsx)(n.h3,{id:"breaking-changes",children:"BREAKING CHANGES"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Interface signatures have changed."}),(0,r.jsx)(n.br,{}),"\n","Core interface updates may break plugin\u2011based systems if the core updates while plugins do not.",(0,r.jsx)(n.br,{}),"\n","This is only safe when proper plugin isolation is respected."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Reducers now run before effects."}),(0,r.jsx)(n.br,{}),"\n","The default dispatch order has changed.",(0,r.jsx)(n.br,{}),"\n","Pipelines that rely on reducers running after effects must update their configuration to restore the previous behavior."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Middleware between individual effects has been removed."}),(0,r.jsx)(n.br,{}),"\n","Per\u2011effect middleware proved unreliable and is no longer supported.",(0,r.jsx)(n.br,{}),"\n","All effects now run as a batch, followed by a single AfterEffect phase.",(0,r.jsx)(n.br,{}),"\n","Middleware can still be awaited before the BeforeEffect phase and after the AfterEffect phase.\r\nMiddlewares for effects if not awaited they are still ganrantueed to be running sequentially BeforeEffect Parallel with Effects then AfterEffects."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Effects, Reducers, Middleware, EffectValidators no long transient"}),(0,r.jsx)(n.br,{}),"\n","Transient lifetime for those is unecessary as none of them should ever hold state of their own therefore they act like static method filled with logic alone... let's eliminate transient overhead and put scoped as default and singleton with the singleton interfaces."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Configuration Scan Assembly Type Changed"}),(0,r.jsx)(n.br,{}),"\n","The property ",(0,r.jsx)(n.code,{children:"ConfigureOptions.ScanAssemblies"})," is now taking a ",(0,r.jsx)(n.code,{children:"Assembly[]"})," instead of ",(0,r.jsx)(n.code,{children:"Type[]"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"IPulseGlobalTracker registration changed"}),(0,r.jsx)(n.br,{}),"\n","The IPulseGlobalTracker is now registered as Scoped instead of singleton... issues were occuring with some blazor server projects."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Removed ",(0,r.jsx)(n.code,{children:".AddStatePulse"})]}),(0,r.jsx)(n.br,{}),"\n","the extension method were removed from public access... now you use ",(0,r.jsx)(n.code,{children:".AddStatePulseService()"})," which will auto detect from action to effect to reducers to middlewares.\r\nReason: Better dev experience less confusing and annoying when having to register stuff manually instead of by scanning.\r\nAlternative: You can also add types into ",(0,r.jsx)(n.code,{children:".AddStatePulseServices(o=>{ o.AutoRegisterTypes = [typeof(AAA)];});"})," which will perform manual registration automatically without scanning assemblies."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Added to IDispatchMiddleware"}),"\r\n",(0,r.jsx)(n.code,{children:"OnDispatchFailure(Exception exception, object action)"})," is now available to middleware hit is receive on any dispatch failure..."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Removed Throw for Dispatch Failure"}),"\r\nRe-Throw only occurs when using Synchronous ",(0,r.jsx)(n.code,{children:"Await()"})," otherwise dispatch will swallow any uncaught exception thrown at it."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Reducers are no longer Tasks"}),"\r\nWith the full introduction of middlewares it is no longer justifiable to have reducers as Task... nothing should be in the reducers to the exception of generating a new state."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"ReducerExt is removed"}),"\r\nReducerExt is no longer available as it was used as helper for Task return."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"IDispatcherPrepper Prepare(Func createInstance)"})," deprecated"]}),"\r\nUse Prepared(instance).DispatchAsync() instead;"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"new-features",children:"New Features"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Enhanced Configuration Options"})," - New global configuration properties:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"DispatchOrderBehavior"})," - Set default ordering (EffectsFirst/ReducersFirst)"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Configurable Dispatch Ordering"})," - Choose between ",(0,r.jsx)(n.code,{children:"EffectsFirst"})," (default) or ",(0,r.jsx)(n.code,{children:"ReducersFirst"})," execution order globally or per-dispatch"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Configurable Dispatch Ordering"})," - The Middleware BeforeReducers will run before the AfterReducer as garantueed..."]}),"\n",(0,r.jsx)(n.li,{children:"The reducer will however run in parallel with BeforeReducing if settings is to not await for middlewares."}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Per-Dispatch Execution Control"})," - Override global settings per dispatch with fluent API:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".EffectsFirst()"})," - Run effects before reducers"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".ReducersFirst()"})," - Run reducers before effects"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".SequentialEffects()"})," - Force effects to run in sequence"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".ParallelEffects()"})," - Force effects to run in parallel"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Invalid Configuration Detection"})," - New ",(0,r.jsx)(n.code,{children:"InvalidDispatchCombinationException"})," thrown at dispatch time for incompatible configuration combinations (Plan to generate compiler errors later)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Recursive Dispatch Support"})," - Fire-and-forget mode enables safe recursive dispatch patterns without deadlocks"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"PulseTrackingModel"})," - Clear option for the tracking model! Either Thread-Safe (",(0,r.jsx)(n.code,{children:"Default"}),") or Single Threaded Fast Application... WASM/Blazor Server."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"GlobalTrackerLifetime"})," - Define clearly lifetime scope for the Global Tracker singleton or scoped. ",(0,r.jsx)(n.code,{children:"BlazorServer"}),", ",(0,r.jsx)(n.code,{children:"WASM"}),", ",(0,r.jsx)(n.code,{children:"Scoped"})," (Default) or ",(0,r.jsx)(n.code,{children:"Singleton"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IStateFeatureSingleton"})," - Define a singleton state across your app... not useful in WASM but very useful in Blazor Server... where one can share the state across client each client run their own action but the state update spread across all circuits."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"StateVersioning"})," long global ticker used to calculate version of a state... this will help prevent further race conditions related to multi-threading (blazor server singleton)."]}),"\n",(0,r.jsxs)(n.li,{children:["State update will automatically discard stale states. you can also acces the information using ",(0,r.jsx)(n.code,{children:"IStateAccessor"})]}),"\n",(0,r.jsxs)(n.li,{children:["Roslyn Analyzer will now trigger error ",(0,r.jsx)(n.code,{children:".Prepare(entity)"})," if constructor object does not match the underlying object's constructor type and it supports overload."]}),"\n",(0,r.jsxs)(n.li,{children:["Roslyn Analyzer will now trigger error ",(0,r.jsx)(n.code,{children:".Prepare().With(p => p.PROP, data)"})," when PROP is ",(0,r.jsx)(n.code,{children:"init;"})," or ",(0,r.jsx)(n.code,{children:"get;"})," only... this will eleminate runtime issues."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"security",children:"Security"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Roslyn will now enforce ",(0,r.jsx)(n.code,{children:"_statePulse.StateOf(() => this, OnUpdate);"})," on the two arguments this will avoid runtime errors and potential memory leaks."]}),"\n",(0,r.jsx)(n.li,{children:"Fixed All Configure Internal..."}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"fixes-1",children:"Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Fixed Various Warnings"}),"\n",(0,r.jsx)(n.li,{children:"Fixed All Configure Internal..."}),"\n",(0,r.jsx)(n.li,{children:"Fixed Middleware are now added via Scanned Assemblies."}),"\n",(0,r.jsxs)(n.li,{children:["Fixed some issues where ",(0,r.jsx)(n.code,{children:"Await()"})," was not respected."]}),"\n",(0,r.jsx)(n.li,{children:"Fixed major issue where all race condition elements where cancelled leading to full chain cancellation including the last action."}),"\n",(0,r.jsx)(n.li,{children:"Fixed inconsistence with race conditions."}),"\n",(0,r.jsxs)(n.li,{children:["Fixed\t",(0,r.jsx)(n.code,{children:"StateOf()"})," throwing inability to cast to ",(0,r.jsx)(n.code,{children:"IStateAccessor"}),"."]}),"\n",(0,r.jsx)(n.li,{children:"Fixed AddStatePulseServices() configuration not optional."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v110",children:"v1.1.0"}),"\n",(0,r.jsx)(n.h3,{id:"minor-change",children:"Minor Change"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Upgraded to .NET 10"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v102",children:"v1.0.2"}),"\n",(0,r.jsx)(n.h3,{id:"fixes-2",children:"Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Added StatePulse.Net.Abstractions package reference instead of project reference to fix IL trimming issues."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v101",children:"v1.0.1"}),"\n",(0,r.jsx)(n.h3,{id:"minor-change-1",children:"Minor Change"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Splited Abstractions into StatePulse.Net.Abstractions (Will not break anything Namespace is the same)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v100",children:"v1.0.0"}),"\n",(0,r.jsx)(n.h3,{id:"new-features-1",children:"New Features"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Action Effect Validator"}),": Allows effects to run conditionally by validating them before execution."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Middleware Support"}),":","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"IEffectMiddleware"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"IReducerMiddleware"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"IDispatchMiddleware"})}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Behavior Configuration"}),": You can configure execution behaviors via:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"DispatchEffectBehavior"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"MiddlewareEffectBehavior"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"MiddlewareTaskBehavior"})}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Strict Manual Registration"}),": Manual service registration ",(0,r.jsx)(n.strong,{children:"must use"})," extension methods:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulse()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseEffect<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseAction<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseReducer<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseStateFeature<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseEffectValidator<>()"})}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-breaking-changes",children:"\ud83d\udca5 Breaking Changes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Removed ",(0,r.jsx)(n.strong,{children:"Action Validator"})," \u2013 validating action data is not the responsibility of the state management layer."]}),"\n",(0,r.jsxs)(n.li,{children:["Renamed:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IStateAccessor<>.StateChanged"})," \u2192 ",(0,r.jsx)(n.code,{children:"OnStateChanged"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"UsingSynchronousMode"})," \u2192 ",(0,r.jsx)(n.strong,{children:"Removed"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"Sync()"})," \u2192 ",(0,r.jsx)(n.code,{children:"Await()"})," for clarity and accuracy"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-performance-improvements",children:"\ud83d\ude80 Performance Improvements"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Improved ",(0,r.jsx)(n.strong,{children:"dispatcher caching"})]}),"\n",(0,r.jsxs)(n.li,{children:["Enhanced ",(0,r.jsx)(n.strong,{children:"type cache"})," in ",(0,r.jsx)(n.code,{children:"StatePulseRegistry"})]}),"\n",(0,r.jsxs)(n.li,{children:["Replaced reflection with ",(0,r.jsx)(n.strong,{children:"dynamic method caching"})," for faster dispatching"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-clean-code-improvements",children:"\ud83e\uddfc Clean Code Improvements"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Refactored ",(0,r.jsx)(n.code,{children:"DispatchPrepper"})," for cleaner and lighter internal logic"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-fixes",children:"\ud83d\udc1e Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Resolved several ",(0,r.jsx)(n.strong,{children:"null reference warnings"})]}),"\n",(0,r.jsxs)(n.li,{children:["Removed leftover ",(0,r.jsx)(n.strong,{children:"internal artifacts"})]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v0941",children:"v0.9.41"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Fix: Added Anti-Service duplication to avoid double triggers."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v094",children:"v0.9.4"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Breaking Change, StateOf no longer accept lambda will throw exception you must define a Task directly... this was necessary due to Garbage Collector and tracking behavior."}),"\n",(0,r.jsx)(n.li,{children:"Deprecated UsingSynchronousMode() instead use Sync()."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v0921",children:"v0.9.21"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Implement the Blazor Package and removed dependencies to Blazor ComponentBase which is no longer required..."}),"\n",(0,r.jsx)(n.li,{children:"Any objects within .NET can now use IStatePulse and benefit from state management without extra implementations."}),"\n",(0,r.jsx)(n.li,{children:"Renamed IPulse to IStatePulse"}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"using IStatePulse.StateOf(()=>this, () => InvokeAsync(StateHasChanged));"})}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v092-blazor-packages",children:"v0.9.2 (Blazor Packages)"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Deprecated now part of StatePulse regular since we have removed the dependencies to blazor component.\r\n... that was quick!"}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(o,{...e})}):o(e)}}}]); \ No newline at end of file diff --git a/doc/build/assets/js/94cf9043.e3d963d9.js b/doc/build/assets/js/94cf9043.e3d963d9.js new file mode 100644 index 0000000..dd47fb3 --- /dev/null +++ b/doc/build/assets/js/94cf9043.e3d963d9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkstatepulse_doc=self.webpackChunkstatepulse_doc||[]).push([[6683],{8453:(e,n,i)=>{i.d(n,{R:()=>l,x:()=>c});var s=i(6540);const r={},t=s.createContext(r);function l(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:l(e.components),s.createElement(t.Provider,{value:n},e.children)}},8886:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>c,default:()=>h,frontMatter:()=>l,metadata:()=>s,toc:()=>d});const s=JSON.parse('{"id":"Versions","title":"Updates","description":"v3.0.0","source":"@site/docs/0.Versions.md","sourceDirName":".","slug":"/versions","permalink":"/versions","draft":false,"unlisted":false,"editUrl":"https://github.com/mshimshon/StatePulse.NET/docs/0.Versions.md","tags":[],"version":"current","sidebarPosition":0,"frontMatter":{"slug":"versions","title":"Updates","sidebar_position":0},"sidebar":"tutorialSidebar","next":{"title":"Get Started","permalink":"/"}}');var r=i(4848),t=i(8453);const l={slug:"versions",title:"Updates",sidebar_position:0},c=void 0,a={},d=[{value:"v3.0.0",id:"v300",level:2},{value:"BREAKING CHANGES",id:"breaking-changes",level:3},{value:"WARNING CHANGES",id:"warning-changes",level:3},{value:"New Features",id:"new-features",level:3},{value:"Performance",id:"performance",level:3},{value:"Major Fixes",id:"major-fixes",level:3},{value:"v2.2.0",id:"v220",level:2},{value:"Fix",id:"fix",level:3},{value:"v2.1.0",id:"v210",level:2},{value:"Performance",id:"performance-1",level:3},{value:"Fix",id:"fix-1",level:3},{value:"v2.0.1",id:"v201",level:2},{value:"Fixes",id:"fixes",level:3},{value:"v2.0.0",id:"v200",level:2},{value:"BREAKING CHANGES",id:"breaking-changes-1",level:3},{value:"New Features",id:"new-features-1",level:3},{value:"Security",id:"security",level:3},{value:"Fixes",id:"fixes-1",level:3},{value:"v1.1.0",id:"v110",level:2},{value:"Minor Change",id:"minor-change",level:3},{value:"v1.0.2",id:"v102",level:2},{value:"Fixes",id:"fixes-2",level:3},{value:"v1.0.1",id:"v101",level:2},{value:"Minor Change",id:"minor-change-1",level:3},{value:"v1.0.0",id:"v100",level:2},{value:"New Features",id:"new-features-2",level:3},{value:"\ud83d\udca5 Breaking Changes",id:"-breaking-changes",level:3},{value:"\ud83d\ude80 Performance Improvements",id:"-performance-improvements",level:3},{value:"\ud83e\uddfc Clean Code Improvements",id:"-clean-code-improvements",level:3},{value:"\ud83d\udc1e Fixes",id:"-fixes",level:3},{value:"v0.9.41",id:"v0941",level:2},{value:"v0.9.4",id:"v094",level:2},{value:"v0.9.21",id:"v0921",level:2},{value:"v0.9.2 (Blazor Packages)",id:"v092-blazor-packages",level:2}];function o(e){const n={br:"br",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h2,{id:"v300",children:"v3.0.0"}),"\n",(0,r.jsx)(n.h3,{id:"breaking-changes",children:"BREAKING CHANGES"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IDispatchTracker"})," is not longer taking a TAction and the service is no longer register per action but instead as a scope service ",(0,r.jsx)(n.code,{children:"IDispatchTracker"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IDispatcher.Prepare().DispatchAsync(CancellationToken ct = default)"})," no longer returns ",(0,r.jsx)(n.code,{children:"Guid"})," as it was unused artifacts of a idea of a system for cancellations."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IDispatcher.Prepare().DispatchAsync(bool asSafe = false, CancellationToken ct = default)"})," no longer takes ",(0,r.jsx)(n.code,{children:"bool"})," as first param instead use fluent api ",(0,r.jsx)(n.code,{children:".AsSafe()"})," to achieve the same result."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"warning-changes",children:"WARNING CHANGES"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IDispatcherMiddleware.OnDispatchFailure(Exception exception, object action);"})," renamed arguments to ",(0,r.jsx)(n.code,{children:"IDispatcherMiddleware.OnDispatchFailure(Exception ex, object action);"})," which could trigger compiler warnings."]}),"\n",(0,r.jsx)(n.li,{}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"new-features",children:"New Features"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Dispatch now supports Cancellation tokens."}),"\n",(0,r.jsxs)(n.li,{children:["Token are passed down to all subsequent chain until you define a different one ",(0,r.jsx)(n.code,{children:"DispatchAsync(false,new CT);"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".DoNotAwait()"})," is now available, design for effect sub-dispatches when specific dispatch should not be awaited in case the origin call awaits the full pipeline... some tasks should not be awaited? that's your method."]}),"\n",(0,r.jsxs)(n.li,{children:["Access Token from effects using ",(0,r.jsx)(n.code,{children:"dispatcher.CancelToken"})," which is default or correspond to upper chain origin."]}),"\n",(0,r.jsxs)(n.li,{children:["You can check if you shouldd stop execution of effect using ",(0,r.jsx)(n.code,{children:"dispatcher.IsCancellationRequested"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"dispatcher.IsCancellationRequested"})," include now if the anti-duplication chain that was cancelled but slipped through the cracks if any you can now catch it in the effects."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"dispatcer.AsSafe()"})," replaces the first param ",(0,r.jsx)(n.code,{children:"IDispatcher.Prepare().DispatchAsync(bool asSafe = false, CancellationToken ct = default)"})," to execute the context with race condition resistance."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"dispatcer.AsUnSafe()"})," turn off the the rc resistance, it is useful to disconnect a usually safe flow to prevent cancellation."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"performance",children:"Performance"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Improved Dispatcher Performance up to 24% by removing class creation via reflection at every dispatch."}),"\n",(0,r.jsx)(n.li,{children:"Improved Dispatch Performance up to 20% by only performing allocation for the middleware class when middlewares are actually present. (Note: the second you start adding middleware slight performance drop is expected)."}),"\n",(0,r.jsx)(n.li,{children:"Improved Reducer Performance, we initially tested every single state registered to compare if the running action had reducers... now we get reducers by Action or by State directly."}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"major-fixes",children:"Major Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Await feature did not await the full pipeline, subsequent dispatches were set as normal fire-and-yield unless specified. Now the await feature passes down the await flag thus fully awaiting as the intended feature was design for."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v220",children:"v2.2.0"}),"\n",(0,r.jsx)(n.h3,{id:"fix",children:"Fix"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Major fix of Middleware not able to be registered multiple times therefore prevent multiple middleware to work for same interface."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v210",children:"v2.1.0"}),"\n",(0,r.jsx)(n.h3,{id:"performance-1",children:"Performance"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The Global Tracker now uses an event\u2011based mechanism to invoke Self Disposal Check directly on each ",(0,r.jsx)(n.code,{children:"IStatePulse"})," instance, removing the need to iterate the internal registry. The registry itself is scheduled for removal in version 3.0 and is now marked with the appropriate ",(0,r.jsx)(n.code,{children:"Obsolete"})," attribute. Cleanup is no longer driven by a continuous loop; instead, the tracker schedules a self\u2011check only when activity occurs in the registry and goes idle when the application becomes inactive. This preserves memory\u2011leak protection without a permanent cycle. A minimum delay of 10 seconds is enforced between checks, and when burst activity occurs, only the final activity triggers a single final cleanup run rather than multiple redundant executions."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"fix-1",children:"Fix"}),"\n",(0,r.jsxs)(n.p,{children:["This is one of those \u201cbug or feature?\u201d situations. By design, ",(0,r.jsx)(n.code,{children:"StateOf()"})," is meant to notify the component when its state changes. However, earlier Blazor assumptions overlooked scenarios where one state might render through one route while another state uses a different route."]}),"\n",(0,r.jsxs)(n.p,{children:["Before version 2.1.0, StatePulse always treated the latest route as the default route for all states registered by a component, this is was ",(0,r.jsx)(n.strong,{children:"BUG"}),". Starting with 2.1.0, each state that a component subscribes to StatePulse now respects those route differences if any."]}),"\n",(0,r.jsx)(n.h2,{id:"v201",children:"v2.0.1"}),"\n",(0,r.jsx)(n.h3,{id:"fixes",children:"Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Moved Fluent API Extension Methods into Abstraction Package."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v200",children:"v2.0.0"}),"\n",(0,r.jsx)(n.h3,{id:"breaking-changes-1",children:"BREAKING CHANGES"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Interface signatures have changed."}),(0,r.jsx)(n.br,{}),"\n","Core interface updates may break plugin\u2011based systems if the core updates while plugins do not.",(0,r.jsx)(n.br,{}),"\n","This is only safe when proper plugin isolation is respected."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Reducers now run before effects."}),(0,r.jsx)(n.br,{}),"\n","The default dispatch order has changed.",(0,r.jsx)(n.br,{}),"\n","Pipelines that rely on reducers running after effects must update their configuration to restore the previous behavior."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Middleware between individual effects has been removed."}),(0,r.jsx)(n.br,{}),"\n","Per\u2011effect middleware proved unreliable and is no longer supported.",(0,r.jsx)(n.br,{}),"\n","All effects now run as a batch, followed by a single AfterEffect phase.",(0,r.jsx)(n.br,{}),"\n","Middleware can still be awaited before the BeforeEffect phase and after the AfterEffect phase.\r\nMiddlewares for effects if not awaited they are still ganrantueed to be running sequentially BeforeEffect Parallel with Effects then AfterEffects."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Effects, Reducers, Middleware, EffectValidators no long transient"}),(0,r.jsx)(n.br,{}),"\n","Transient lifetime for those is unecessary as none of them should ever hold state of their own therefore they act like static method filled with logic alone... let's eliminate transient overhead and put scoped as default and singleton with the singleton interfaces."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Configuration Scan Assembly Type Changed"}),(0,r.jsx)(n.br,{}),"\n","The property ",(0,r.jsx)(n.code,{children:"ConfigureOptions.ScanAssemblies"})," is now taking a ",(0,r.jsx)(n.code,{children:"Assembly[]"})," instead of ",(0,r.jsx)(n.code,{children:"Type[]"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"IPulseGlobalTracker registration changed"}),(0,r.jsx)(n.br,{}),"\n","The IPulseGlobalTracker is now registered as Scoped instead of singleton... issues were occuring with some blazor server projects."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Removed ",(0,r.jsx)(n.code,{children:".AddStatePulse"})]}),(0,r.jsx)(n.br,{}),"\n","the extension method were removed from public access... now you use ",(0,r.jsx)(n.code,{children:".AddStatePulseService()"})," which will auto detect from action to effect to reducers to middlewares.\r\nReason: Better dev experience less confusing and annoying when having to register stuff manually instead of by scanning.\r\nAlternative: You can also add types into ",(0,r.jsx)(n.code,{children:".AddStatePulseServices(o=>{ o.AutoRegisterTypes = [typeof(AAA)];});"})," which will perform manual registration automatically without scanning assemblies."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Added to IDispatchMiddleware"}),"\r\n",(0,r.jsx)(n.code,{children:"OnDispatchFailure(Exception exception, object action)"})," is now available to middleware hit is receive on any dispatch failure..."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Removed Throw for Dispatch Failure"}),"\r\nRe-Throw only occurs when using Synchronous ",(0,r.jsx)(n.code,{children:"Await()"})," otherwise dispatch will swallow any uncaught exception thrown at it."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Reducers are no longer Tasks"}),"\r\nWith the full introduction of middlewares it is no longer justifiable to have reducers as Task... nothing should be in the reducers to the exception of generating a new state."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"ReducerExt is removed"}),"\r\nReducerExt is no longer available as it was used as helper for Task return."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"IDispatcherPrepper Prepare(Func createInstance)"})," deprecated"]}),"\r\nUse Prepared(instance).DispatchAsync() instead;"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"new-features-1",children:"New Features"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Enhanced Configuration Options"})," - New global configuration properties:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"DispatchOrderBehavior"})," - Set default ordering (EffectsFirst/ReducersFirst)"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Configurable Dispatch Ordering"})," - Choose between ",(0,r.jsx)(n.code,{children:"EffectsFirst"})," (default) or ",(0,r.jsx)(n.code,{children:"ReducersFirst"})," execution order globally or per-dispatch"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Configurable Dispatch Ordering"})," - The Middleware BeforeReducers will run before the AfterReducer as garantueed..."]}),"\n",(0,r.jsx)(n.li,{children:"The reducer will however run in parallel with BeforeReducing if settings is to not await for middlewares."}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Per-Dispatch Execution Control"})," - Override global settings per dispatch with fluent API:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".EffectsFirst()"})," - Run effects before reducers"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".ReducersFirst()"})," - Run reducers before effects"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".SequentialEffects()"})," - Force effects to run in sequence"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:".ParallelEffects()"})," - Force effects to run in parallel"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Invalid Configuration Detection"})," - New ",(0,r.jsx)(n.code,{children:"InvalidDispatchCombinationException"})," thrown at dispatch time for incompatible configuration combinations (Plan to generate compiler errors later)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Recursive Dispatch Support"})," - Fire-and-forget mode enables safe recursive dispatch patterns without deadlocks"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"PulseTrackingModel"})," - Clear option for the tracking model! Either Thread-Safe (",(0,r.jsx)(n.code,{children:"Default"}),") or Single Threaded Fast Application... WASM/Blazor Server."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"GlobalTrackerLifetime"})," - Define clearly lifetime scope for the Global Tracker singleton or scoped. ",(0,r.jsx)(n.code,{children:"BlazorServer"}),", ",(0,r.jsx)(n.code,{children:"WASM"}),", ",(0,r.jsx)(n.code,{children:"Scoped"})," (Default) or ",(0,r.jsx)(n.code,{children:"Singleton"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IStateFeatureSingleton"})," - Define a singleton state across your app... not useful in WASM but very useful in Blazor Server... where one can share the state across client each client run their own action but the state update spread across all circuits."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"StateVersioning"})," long global ticker used to calculate version of a state... this will help prevent further race conditions related to multi-threading (blazor server singleton)."]}),"\n",(0,r.jsxs)(n.li,{children:["State update will automatically discard stale states. you can also acces the information using ",(0,r.jsx)(n.code,{children:"IStateAccessor"})]}),"\n",(0,r.jsxs)(n.li,{children:["Roslyn Analyzer will now trigger error ",(0,r.jsx)(n.code,{children:".Prepare(entity)"})," if constructor object does not match the underlying object's constructor type and it supports overload."]}),"\n",(0,r.jsxs)(n.li,{children:["Roslyn Analyzer will now trigger error ",(0,r.jsx)(n.code,{children:".Prepare().With(p => p.PROP, data)"})," when PROP is ",(0,r.jsx)(n.code,{children:"init;"})," or ",(0,r.jsx)(n.code,{children:"get;"})," only... this will eleminate runtime issues."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"security",children:"Security"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Roslyn will now enforce ",(0,r.jsx)(n.code,{children:"_statePulse.StateOf(() => this, OnUpdate);"})," on the two arguments this will avoid runtime errors and potential memory leaks."]}),"\n",(0,r.jsx)(n.li,{children:"Fixed All Configure Internal..."}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"fixes-1",children:"Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Fixed Various Warnings"}),"\n",(0,r.jsx)(n.li,{children:"Fixed All Configure Internal..."}),"\n",(0,r.jsx)(n.li,{children:"Fixed Middleware are now added via Scanned Assemblies."}),"\n",(0,r.jsxs)(n.li,{children:["Fixed some issues where ",(0,r.jsx)(n.code,{children:"Await()"})," was not respected."]}),"\n",(0,r.jsx)(n.li,{children:"Fixed major issue where all race condition elements where cancelled leading to full chain cancellation including the last action."}),"\n",(0,r.jsx)(n.li,{children:"Fixed inconsistence with race conditions."}),"\n",(0,r.jsxs)(n.li,{children:["Fixed\t",(0,r.jsx)(n.code,{children:"StateOf()"})," throwing inability to cast to ",(0,r.jsx)(n.code,{children:"IStateAccessor"}),"."]}),"\n",(0,r.jsx)(n.li,{children:"Fixed AddStatePulseServices() configuration not optional."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v110",children:"v1.1.0"}),"\n",(0,r.jsx)(n.h3,{id:"minor-change",children:"Minor Change"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Upgraded to .NET 10"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v102",children:"v1.0.2"}),"\n",(0,r.jsx)(n.h3,{id:"fixes-2",children:"Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Added StatePulse.Net.Abstractions package reference instead of project reference to fix IL trimming issues."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v101",children:"v1.0.1"}),"\n",(0,r.jsx)(n.h3,{id:"minor-change-1",children:"Minor Change"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Splited Abstractions into StatePulse.Net.Abstractions (Will not break anything Namespace is the same)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v100",children:"v1.0.0"}),"\n",(0,r.jsx)(n.h3,{id:"new-features-2",children:"New Features"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Action Effect Validator"}),": Allows effects to run conditionally by validating them before execution."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Middleware Support"}),":","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"IEffectMiddleware"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"IReducerMiddleware"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"IDispatchMiddleware"})}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Behavior Configuration"}),": You can configure execution behaviors via:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"DispatchEffectBehavior"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"MiddlewareEffectBehavior"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"MiddlewareTaskBehavior"})}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Strict Manual Registration"}),": Manual service registration ",(0,r.jsx)(n.strong,{children:"must use"})," extension methods:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulse()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseEffect<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseAction<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseReducer<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseStateFeature<>()"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"AddStatePulseEffectValidator<>()"})}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-breaking-changes",children:"\ud83d\udca5 Breaking Changes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Removed ",(0,r.jsx)(n.strong,{children:"Action Validator"})," \u2013 validating action data is not the responsibility of the state management layer."]}),"\n",(0,r.jsxs)(n.li,{children:["Renamed:","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IStateAccessor<>.StateChanged"})," \u2192 ",(0,r.jsx)(n.code,{children:"OnStateChanged"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"UsingSynchronousMode"})," \u2192 ",(0,r.jsx)(n.strong,{children:"Removed"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"Sync()"})," \u2192 ",(0,r.jsx)(n.code,{children:"Await()"})," for clarity and accuracy"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-performance-improvements",children:"\ud83d\ude80 Performance Improvements"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Improved ",(0,r.jsx)(n.strong,{children:"dispatcher caching"})]}),"\n",(0,r.jsxs)(n.li,{children:["Enhanced ",(0,r.jsx)(n.strong,{children:"type cache"})," in ",(0,r.jsx)(n.code,{children:"StatePulseRegistry"})]}),"\n",(0,r.jsxs)(n.li,{children:["Replaced reflection with ",(0,r.jsx)(n.strong,{children:"dynamic method caching"})," for faster dispatching"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-clean-code-improvements",children:"\ud83e\uddfc Clean Code Improvements"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Refactored ",(0,r.jsx)(n.code,{children:"DispatchPrepper"})," for cleaner and lighter internal logic"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"-fixes",children:"\ud83d\udc1e Fixes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Resolved several ",(0,r.jsx)(n.strong,{children:"null reference warnings"})]}),"\n",(0,r.jsxs)(n.li,{children:["Removed leftover ",(0,r.jsx)(n.strong,{children:"internal artifacts"})]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v0941",children:"v0.9.41"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Fix: Added Anti-Service duplication to avoid double triggers."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v094",children:"v0.9.4"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Breaking Change, StateOf no longer accept lambda will throw exception you must define a Task directly... this was necessary due to Garbage Collector and tracking behavior."}),"\n",(0,r.jsx)(n.li,{children:"Deprecated UsingSynchronousMode() instead use Sync()."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v0921",children:"v0.9.21"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Implement the Blazor Package and removed dependencies to Blazor ComponentBase which is no longer required..."}),"\n",(0,r.jsx)(n.li,{children:"Any objects within .NET can now use IStatePulse and benefit from state management without extra implementations."}),"\n",(0,r.jsx)(n.li,{children:"Renamed IPulse to IStatePulse"}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"using IStatePulse.StateOf(()=>this, () => InvokeAsync(StateHasChanged));"})}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"v092-blazor-packages",children:"v0.9.2 (Blazor Packages)"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Deprecated now part of StatePulse regular since we have removed the dependencies to blazor component.\r\n... that was quick!"}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(o,{...e})}):o(e)}}}]); \ No newline at end of file diff --git a/doc/build/assets/js/runtime~main.4fb4416e.js b/doc/build/assets/js/runtime~main.6e246e26.js similarity index 97% rename from doc/build/assets/js/runtime~main.4fb4416e.js rename to doc/build/assets/js/runtime~main.6e246e26.js index 326b736..359a8dd 100644 --- a/doc/build/assets/js/runtime~main.4fb4416e.js +++ b/doc/build/assets/js/runtime~main.6e246e26.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,t,c,f,r={},d={};function b(e){var a=d[e];if(void 0!==a)return a.exports;var t=d[e]={exports:{}};return r[e].call(t.exports,t,t.exports,b),t.exports}b.m=r,e=[],b.O=(a,t,c,f)=>{if(!t){var r=1/0;for(i=0;i=f)&&Object.keys(b.O).every(e=>b.O[e](t[o]))?t.splice(o--,1):(d=!1,f0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[t,c,f]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var f=Object.create(null);b.r(f);var r={};a=a||[null,t({}),t([]),t(t)];for(var d=2&c&&e;("object"==typeof d||"function"==typeof d)&&!~a.indexOf(d);d=t(d))Object.getOwnPropertyNames(d).forEach(a=>r[a]=()=>e[a]);return r.default=()=>e,b.d(f,r),f},b.d=(e,a)=>{for(var t in a)b.o(a,t)&&!b.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce((a,t)=>(b.f[t](e,a),a),[])),b.u=e=>"assets/js/"+({177:"7691d1c3",688:"f2ace840",841:"8c3a5864",855:"8edd9378",867:"33fc5bb8",908:"d5b5cc03",972:"02105171",1235:"a7456010",1567:"22dd74f7",1653:"f552f37d",1903:"acecf23e",2220:"0e30184d",2520:"e925e243",2787:"7954e82b",3318:"742328d7",3328:"cc87a84e",3478:"cd69296b",3944:"0d8668ed",4134:"393be207",4157:"f179eaed",4212:"621db11d",4279:"df203c0f",4787:"3720c009",4850:"79199b91",5742:"aba21aa0",5899:"c9c672c1",6061:"1f391b9e",6403:"c5fd082d",6527:"e48d0160",6683:"94cf9043",6980:"936b9f1e",6996:"ce33d687",7098:"a7bd4aaa",7235:"4ebf985a",7472:"814f3328",7687:"90c0aa62",7698:"547f2087",7713:"dbf990ff",7778:"e4b9287f",7853:"9cc31a46",8151:"0bc25be7",8401:"17896441",8672:"2e6f86e1",8947:"ef8b811a",9048:"a94703ab",9174:"59af61a6",9647:"5e95c892",9858:"36994c47",9963:"68ee3bbe",9977:"f6f0caca"}[e]||e)+"."+{177:"41ff88ef",688:"b60b551f",841:"65d8a29a",855:"383d25f9",867:"d2f2b0c1",908:"a0e4c5cf",972:"051f2e89",1235:"336f95fe",1567:"f784a3d1",1653:"f6b5254e",1903:"b595c8e0",2220:"03ddad7e",2237:"2847df75",2520:"eaa2c69c",2787:"724d28ea",3318:"9cf46a79",3328:"05b44a8f",3478:"c353b6fa",3944:"cdb36547",4134:"2823742b",4157:"6ac92fc3",4212:"28d58a98",4279:"c82ea539",4787:"cf7bc292",4850:"c3dab34b",5742:"5a986477",5899:"e6df7d83",6061:"47fc6bcb",6120:"b521cc9a",6403:"5c866409",6527:"acea7c7a",6683:"cdbd9e88",6980:"d8a5a951",6996:"3df1d0b1",7098:"f2d2fe61",7235:"a97ef297",7312:"49620988",7472:"fbffe806",7687:"5eb5689f",7698:"6c1b068d",7713:"f1c7dfd1",7778:"61c20a69",7853:"968cd617",8151:"ad260ac3",8401:"145b706a",8672:"647dd398",8947:"7573ae26",9048:"abc249b9",9174:"c252ee6f",9647:"9334a2c8",9858:"39393642",9963:"20abb41a",9977:"6f80119e"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),c={},f="statepulse-doc:",b.l=(e,a,t,r)=>{if(c[e])c[e].push(a);else{var d,o;if(void 0!==t)for(var n=document.getElementsByTagName("script"),i=0;i{d.onerror=d.onload=null,clearTimeout(s);var f=c[e];if(delete c[e],d.parentNode&&d.parentNode.removeChild(d),f&&f.forEach(e=>e(t)),a)return a(t)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),o&&document.head.appendChild(d)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"8401","7691d1c3":"177",f2ace840:"688","8c3a5864":"841","8edd9378":"855","33fc5bb8":"867",d5b5cc03:"908","02105171":"972",a7456010:"1235","22dd74f7":"1567",f552f37d:"1653",acecf23e:"1903","0e30184d":"2220",e925e243:"2520","7954e82b":"2787","742328d7":"3318",cc87a84e:"3328",cd69296b:"3478","0d8668ed":"3944","393be207":"4134",f179eaed:"4157","621db11d":"4212",df203c0f:"4279","3720c009":"4787","79199b91":"4850",aba21aa0:"5742",c9c672c1:"5899","1f391b9e":"6061",c5fd082d:"6403",e48d0160:"6527","94cf9043":"6683","936b9f1e":"6980",ce33d687:"6996",a7bd4aaa:"7098","4ebf985a":"7235","814f3328":"7472","90c0aa62":"7687","547f2087":"7698",dbf990ff:"7713",e4b9287f:"7778","9cc31a46":"7853","0bc25be7":"8151","2e6f86e1":"8672",ef8b811a:"8947",a94703ab:"9048","59af61a6":"9174","5e95c892":"9647","36994c47":"9858","68ee3bbe":"9963",f6f0caca:"9977"}[e]||e,b.p+b.u(e)},(()=>{var e={5354:0,1869:0};b.f.j=(a,t)=>{var c=b.o(e,a)?e[a]:void 0;if(0!==c)if(c)t.push(c[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var f=new Promise((t,f)=>c=e[a]=[t,f]);t.push(c[2]=f);var r=b.p+b.u(a),d=new Error;b.l(r,t=>{if(b.o(e,a)&&(0!==(c=e[a])&&(e[a]=void 0),c)){var f=t&&("load"===t.type?"missing":t.type),r=t&&t.target&&t.target.src;d.message="Loading chunk "+a+" failed.\n("+f+": "+r+")",d.name="ChunkLoadError",d.type=f,d.request=r,c[1](d)}},"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,t)=>{var c,f,r=t[0],d=t[1],o=t[2],n=0;if(r.some(a=>0!==e[a])){for(c in d)b.o(d,c)&&(b.m[c]=d[c]);if(o)var i=o(b)}for(a&&a(t);n{"use strict";var e,a,t,c,f,r={},d={};function b(e){var a=d[e];if(void 0!==a)return a.exports;var t=d[e]={exports:{}};return r[e].call(t.exports,t,t.exports,b),t.exports}b.m=r,e=[],b.O=(a,t,c,f)=>{if(!t){var r=1/0;for(i=0;i=f)&&Object.keys(b.O).every(e=>b.O[e](t[o]))?t.splice(o--,1):(d=!1,f0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[t,c,f]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var f=Object.create(null);b.r(f);var r={};a=a||[null,t({}),t([]),t(t)];for(var d=2&c&&e;("object"==typeof d||"function"==typeof d)&&!~a.indexOf(d);d=t(d))Object.getOwnPropertyNames(d).forEach(a=>r[a]=()=>e[a]);return r.default=()=>e,b.d(f,r),f},b.d=(e,a)=>{for(var t in a)b.o(a,t)&&!b.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce((a,t)=>(b.f[t](e,a),a),[])),b.u=e=>"assets/js/"+({177:"7691d1c3",688:"f2ace840",841:"8c3a5864",855:"8edd9378",867:"33fc5bb8",908:"d5b5cc03",972:"02105171",1235:"a7456010",1567:"22dd74f7",1653:"f552f37d",1903:"acecf23e",2220:"0e30184d",2520:"e925e243",2787:"7954e82b",3318:"742328d7",3328:"cc87a84e",3478:"cd69296b",3944:"0d8668ed",4134:"393be207",4157:"f179eaed",4212:"621db11d",4279:"df203c0f",4787:"3720c009",4850:"79199b91",5742:"aba21aa0",5899:"c9c672c1",6061:"1f391b9e",6403:"c5fd082d",6527:"e48d0160",6683:"94cf9043",6980:"936b9f1e",6996:"ce33d687",7098:"a7bd4aaa",7235:"4ebf985a",7472:"814f3328",7687:"90c0aa62",7698:"547f2087",7713:"dbf990ff",7778:"e4b9287f",7853:"9cc31a46",8151:"0bc25be7",8401:"17896441",8672:"2e6f86e1",8947:"ef8b811a",9048:"a94703ab",9174:"59af61a6",9647:"5e95c892",9858:"36994c47",9963:"68ee3bbe",9977:"f6f0caca"}[e]||e)+"."+{177:"41ff88ef",688:"b60b551f",841:"65d8a29a",855:"383d25f9",867:"d2f2b0c1",908:"a0e4c5cf",972:"051f2e89",1235:"336f95fe",1567:"9f52074d",1653:"f6b5254e",1903:"b595c8e0",2220:"03ddad7e",2237:"2847df75",2520:"eaa2c69c",2787:"724d28ea",3318:"9cf46a79",3328:"05b44a8f",3478:"c353b6fa",3944:"cdb36547",4134:"2823742b",4157:"6ac92fc3",4212:"28d58a98",4279:"c82ea539",4787:"cf7bc292",4850:"c3dab34b",5742:"5a986477",5899:"e6df7d83",6061:"47fc6bcb",6120:"b521cc9a",6403:"5c866409",6527:"acea7c7a",6683:"e3d963d9",6980:"d8a5a951",6996:"3df1d0b1",7098:"f2d2fe61",7235:"a97ef297",7312:"49620988",7472:"fbffe806",7687:"5eb5689f",7698:"6c1b068d",7713:"f1c7dfd1",7778:"61c20a69",7853:"968cd617",8151:"ad260ac3",8401:"145b706a",8672:"647dd398",8947:"7573ae26",9048:"abc249b9",9174:"c252ee6f",9647:"9334a2c8",9858:"39393642",9963:"20abb41a",9977:"6f80119e"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),c={},f="statepulse-doc:",b.l=(e,a,t,r)=>{if(c[e])c[e].push(a);else{var d,o;if(void 0!==t)for(var n=document.getElementsByTagName("script"),i=0;i{d.onerror=d.onload=null,clearTimeout(s);var f=c[e];if(delete c[e],d.parentNode&&d.parentNode.removeChild(d),f&&f.forEach(e=>e(t)),a)return a(t)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=l.bind(null,d.onerror),d.onload=l.bind(null,d.onload),o&&document.head.appendChild(d)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"8401","7691d1c3":"177",f2ace840:"688","8c3a5864":"841","8edd9378":"855","33fc5bb8":"867",d5b5cc03:"908","02105171":"972",a7456010:"1235","22dd74f7":"1567",f552f37d:"1653",acecf23e:"1903","0e30184d":"2220",e925e243:"2520","7954e82b":"2787","742328d7":"3318",cc87a84e:"3328",cd69296b:"3478","0d8668ed":"3944","393be207":"4134",f179eaed:"4157","621db11d":"4212",df203c0f:"4279","3720c009":"4787","79199b91":"4850",aba21aa0:"5742",c9c672c1:"5899","1f391b9e":"6061",c5fd082d:"6403",e48d0160:"6527","94cf9043":"6683","936b9f1e":"6980",ce33d687:"6996",a7bd4aaa:"7098","4ebf985a":"7235","814f3328":"7472","90c0aa62":"7687","547f2087":"7698",dbf990ff:"7713",e4b9287f:"7778","9cc31a46":"7853","0bc25be7":"8151","2e6f86e1":"8672",ef8b811a:"8947",a94703ab:"9048","59af61a6":"9174","5e95c892":"9647","36994c47":"9858","68ee3bbe":"9963",f6f0caca:"9977"}[e]||e,b.p+b.u(e)},(()=>{var e={5354:0,1869:0};b.f.j=(a,t)=>{var c=b.o(e,a)?e[a]:void 0;if(0!==c)if(c)t.push(c[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var f=new Promise((t,f)=>c=e[a]=[t,f]);t.push(c[2]=f);var r=b.p+b.u(a),d=new Error;b.l(r,t=>{if(b.o(e,a)&&(0!==(c=e[a])&&(e[a]=void 0),c)){var f=t&&("load"===t.type?"missing":t.type),r=t&&t.target&&t.target.src;d.message="Loading chunk "+a+" failed.\n("+f+": "+r+")",d.name="ChunkLoadError",d.type=f,d.request=r,c[1](d)}},"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,t)=>{var c,f,r=t[0],d=t[1],o=t[2],n=0;if(r.some(a=>0!==e[a])){for(c in d)b.o(d,c)&&(b.m[c]=d[c]);if(o)var i=o(b)}for(a&&a(t);n Authors | StatePulse.NET - + diff --git a/doc/build/blog/authors/mshimshon/index.html b/doc/build/blog/authors/mshimshon/index.html index c1eaa1f..936f3f1 100644 --- a/doc/build/blog/authors/mshimshon/index.html +++ b/doc/build/blog/authors/mshimshon/index.html @@ -4,7 +4,7 @@ Maksim Shimshon - 0 posts | StatePulse.NET - + diff --git a/doc/build/gs-state/index.html b/doc/build/gs-state/index.html index 5bbb3fc..7926904 100644 --- a/doc/build/gs-state/index.html +++ b/doc/build/gs-state/index.html @@ -4,7 +4,7 @@ The States | StatePulse.NET - + diff --git a/doc/build/gs-the-action/index.html b/doc/build/gs-the-action/index.html index 0cb9200..aa8125d 100644 --- a/doc/build/gs-the-action/index.html +++ b/doc/build/gs-the-action/index.html @@ -4,7 +4,7 @@ The Actions | StatePulse.NET - + diff --git a/doc/build/gs-the-dispatcher/index.html b/doc/build/gs-the-dispatcher/index.html index 4dc29f0..c77845c 100644 --- a/doc/build/gs-the-dispatcher/index.html +++ b/doc/build/gs-the-dispatcher/index.html @@ -4,7 +4,7 @@ The Dispatcher | StatePulse.NET - + diff --git a/doc/build/gs-the-effect/index.html b/doc/build/gs-the-effect/index.html index acf23a7..d001cfa 100644 --- a/doc/build/gs-the-effect/index.html +++ b/doc/build/gs-the-effect/index.html @@ -4,7 +4,7 @@ The Effects | StatePulse.NET - + diff --git a/doc/build/gs-the-middlewares/index.html b/doc/build/gs-the-middlewares/index.html index 342d229..c0c7eac 100644 --- a/doc/build/gs-the-middlewares/index.html +++ b/doc/build/gs-the-middlewares/index.html @@ -4,7 +4,7 @@ The Middlewares | StatePulse.NET - + diff --git a/doc/build/gs-the-reducer/index.html b/doc/build/gs-the-reducer/index.html index 015eb06..cef84af 100644 --- a/doc/build/gs-the-reducer/index.html +++ b/doc/build/gs-the-reducer/index.html @@ -4,7 +4,7 @@ The Reducers | StatePulse.NET - + diff --git a/doc/build/index.html b/doc/build/index.html index 4d42d28..9c7c008 100644 --- a/doc/build/index.html +++ b/doc/build/index.html @@ -4,7 +4,7 @@ Get Started | StatePulse.NET - + diff --git a/doc/build/markdown-page/index.html b/doc/build/markdown-page/index.html index f007143..fa3b70c 100644 --- a/doc/build/markdown-page/index.html +++ b/doc/build/markdown-page/index.html @@ -4,7 +4,7 @@ Markdown page example | StatePulse.NET - + diff --git a/doc/build/setup-blazor-project/index.html b/doc/build/setup-blazor-project/index.html index 2b7511f..1a25727 100644 --- a/doc/build/setup-blazor-project/index.html +++ b/doc/build/setup-blazor-project/index.html @@ -4,7 +4,7 @@ Setup Blazor Project | StatePulse.NET - + diff --git a/doc/build/tags/actions/index.html b/doc/build/tags/actions/index.html index f77aa71..b2e83d1 100644 --- a/doc/build/tags/actions/index.html +++ b/doc/build/tags/actions/index.html @@ -4,7 +4,7 @@ 2 docs tagged with "actions" | StatePulse.NET - + diff --git a/doc/build/tags/async/index.html b/doc/build/tags/async/index.html index b4d1960..28a2f83 100644 --- a/doc/build/tags/async/index.html +++ b/doc/build/tags/async/index.html @@ -4,7 +4,7 @@ 4 docs tagged with "async" | StatePulse.NET - + diff --git a/doc/build/tags/await/index.html b/doc/build/tags/await/index.html index 3267951..08f8450 100644 --- a/doc/build/tags/await/index.html +++ b/doc/build/tags/await/index.html @@ -4,7 +4,7 @@ One doc tagged with "await" | StatePulse.NET - + diff --git a/doc/build/tags/blazor/index.html b/doc/build/tags/blazor/index.html index 5269487..8b3fce7 100644 --- a/doc/build/tags/blazor/index.html +++ b/doc/build/tags/blazor/index.html @@ -4,7 +4,7 @@ 7 docs tagged with "blazor" | StatePulse.NET - + diff --git a/doc/build/tags/csharp/index.html b/doc/build/tags/csharp/index.html index 10e47be..18f398b 100644 --- a/doc/build/tags/csharp/index.html +++ b/doc/build/tags/csharp/index.html @@ -4,7 +4,7 @@ 6 docs tagged with "csharp" | StatePulse.NET - + diff --git a/doc/build/tags/dependency-injection/index.html b/doc/build/tags/dependency-injection/index.html index 19000cb..0badf35 100644 --- a/doc/build/tags/dependency-injection/index.html +++ b/doc/build/tags/dependency-injection/index.html @@ -4,7 +4,7 @@ One doc tagged with "dependency-injection" | StatePulse.NET - + diff --git a/doc/build/tags/dispatcher/index.html b/doc/build/tags/dispatcher/index.html index 6d59c07..0e11391 100644 --- a/doc/build/tags/dispatcher/index.html +++ b/doc/build/tags/dispatcher/index.html @@ -4,7 +4,7 @@ One doc tagged with "dispatcher" | StatePulse.NET - + diff --git a/doc/build/tags/effects/index.html b/doc/build/tags/effects/index.html index ed282dd..353533d 100644 --- a/doc/build/tags/effects/index.html +++ b/doc/build/tags/effects/index.html @@ -4,7 +4,7 @@ One doc tagged with "effects" | StatePulse.NET - + diff --git a/doc/build/tags/immutable/index.html b/doc/build/tags/immutable/index.html index 8eaaef1..75ebfe5 100644 --- a/doc/build/tags/immutable/index.html +++ b/doc/build/tags/immutable/index.html @@ -4,7 +4,7 @@ One doc tagged with "immutable" | StatePulse.NET - + diff --git a/doc/build/tags/index.html b/doc/build/tags/index.html index d613fdc..f129830 100644 --- a/doc/build/tags/index.html +++ b/doc/build/tags/index.html @@ -4,7 +4,7 @@ Tags | StatePulse.NET - + diff --git a/doc/build/tags/installation/index.html b/doc/build/tags/installation/index.html index 77cd151..094d5a6 100644 --- a/doc/build/tags/installation/index.html +++ b/doc/build/tags/installation/index.html @@ -4,7 +4,7 @@ One doc tagged with "installation" | StatePulse.NET - + diff --git a/doc/build/tags/isafeaction/index.html b/doc/build/tags/isafeaction/index.html index 575dce7..ced5736 100644 --- a/doc/build/tags/isafeaction/index.html +++ b/doc/build/tags/isafeaction/index.html @@ -4,7 +4,7 @@ One doc tagged with "isafeaction" | StatePulse.NET - + diff --git a/doc/build/tags/net/index.html b/doc/build/tags/net/index.html index e4542c5..66a2691 100644 --- a/doc/build/tags/net/index.html +++ b/doc/build/tags/net/index.html @@ -4,7 +4,7 @@ 6 docs tagged with ".net" | StatePulse.NET - + diff --git a/doc/build/tags/performance/index.html b/doc/build/tags/performance/index.html index 4c1903b..457918d 100644 --- a/doc/build/tags/performance/index.html +++ b/doc/build/tags/performance/index.html @@ -4,7 +4,7 @@ 2 docs tagged with "performance" | StatePulse.NET - + diff --git a/doc/build/tags/pure-functions/index.html b/doc/build/tags/pure-functions/index.html index bed8426..4fb7244 100644 --- a/doc/build/tags/pure-functions/index.html +++ b/doc/build/tags/pure-functions/index.html @@ -4,7 +4,7 @@ 2 docs tagged with "pure-functions" | StatePulse.NET - + diff --git a/doc/build/tags/reducer/index.html b/doc/build/tags/reducer/index.html index 0d825ba..19eaef7 100644 --- a/doc/build/tags/reducer/index.html +++ b/doc/build/tags/reducer/index.html @@ -4,7 +4,7 @@ 2 docs tagged with "reducer" | StatePulse.NET - + diff --git a/doc/build/tags/redux/index.html b/doc/build/tags/redux/index.html index 40d96ae..1966c69 100644 --- a/doc/build/tags/redux/index.html +++ b/doc/build/tags/redux/index.html @@ -4,7 +4,7 @@ One doc tagged with "redux" | StatePulse.NET - + diff --git a/doc/build/tags/safedispatch/index.html b/doc/build/tags/safedispatch/index.html index f8f1073..7fa4c43 100644 --- a/doc/build/tags/safedispatch/index.html +++ b/doc/build/tags/safedispatch/index.html @@ -4,7 +4,7 @@ One doc tagged with "safedispatch" | StatePulse.NET - + diff --git a/doc/build/tags/setup/index.html b/doc/build/tags/setup/index.html index 8b6cc09..2268e8d 100644 --- a/doc/build/tags/setup/index.html +++ b/doc/build/tags/setup/index.html @@ -4,7 +4,7 @@ One doc tagged with "setup" | StatePulse.NET - + diff --git a/doc/build/tags/side-effects/index.html b/doc/build/tags/side-effects/index.html index 92cd79b..12f67e5 100644 --- a/doc/build/tags/side-effects/index.html +++ b/doc/build/tags/side-effects/index.html @@ -4,7 +4,7 @@ One doc tagged with "side-effects" | StatePulse.NET - + diff --git a/doc/build/tags/state-management/index.html b/doc/build/tags/state-management/index.html index c737809..3583aac 100644 --- a/doc/build/tags/state-management/index.html +++ b/doc/build/tags/state-management/index.html @@ -4,7 +4,7 @@ 6 docs tagged with "state-management" | StatePulse.NET - + diff --git a/doc/build/tags/state/index.html b/doc/build/tags/state/index.html index ce02c2a..fef8d5f 100644 --- a/doc/build/tags/state/index.html +++ b/doc/build/tags/state/index.html @@ -4,7 +4,7 @@ One doc tagged with "state" | StatePulse.NET - + diff --git a/doc/build/tags/statepulse/index.html b/doc/build/tags/statepulse/index.html index 707f33d..7f761dc 100644 --- a/doc/build/tags/statepulse/index.html +++ b/doc/build/tags/statepulse/index.html @@ -4,7 +4,7 @@ 7 docs tagged with "statepulse" | StatePulse.NET - + diff --git a/doc/build/versions/index.html b/doc/build/versions/index.html index 7a44baf..b86ce3f 100644 --- a/doc/build/versions/index.html +++ b/doc/build/versions/index.html @@ -3,34 +3,67 @@ -Updates | StatePulse.NET - +Updates | StatePulse.NET + -

Updates

v2.2.0

+

Updates

v3.0.0

+

BREAKING CHANGES

+
    +
  • IDispatchTracker<TAction> is not longer taking a TAction and the service is no longer register per action but instead as a scope service IDispatchTracker.
  • +
  • IDispatcher.Prepare<TAction>().DispatchAsync(CancellationToken ct = default) no longer returns Guid as it was unused artifacts of a idea of a system for cancellations.
  • +
  • IDispatcher.Prepare<TAction>().DispatchAsync(bool asSafe = false, CancellationToken ct = default) no longer takes bool as first param instead use fluent api .AsSafe() to achieve the same result.
  • +
+

WARNING CHANGES

+
    +
  • IDispatcherMiddleware.OnDispatchFailure(Exception exception, object action); renamed arguments to IDispatcherMiddleware.OnDispatchFailure(Exception ex, object action); which could trigger compiler warnings.
  • +
  • +
+

New Features

+
    +
  • Dispatch now supports Cancellation tokens.
  • +
  • Token are passed down to all subsequent chain until you define a different one DispatchAsync(false,new CT);.
  • +
  • .DoNotAwait() is now available, design for effect sub-dispatches when specific dispatch should not be awaited in case the origin call awaits the full pipeline... some tasks should not be awaited? that's your method.
  • +
  • Access Token from effects using dispatcher.CancelToken which is default or correspond to upper chain origin.
  • +
  • You can check if you shouldd stop execution of effect using dispatcher.IsCancellationRequested.
  • +
  • dispatcher.IsCancellationRequested include now if the anti-duplication chain that was cancelled but slipped through the cracks if any you can now catch it in the effects.
  • +
  • dispatcer.AsSafe() replaces the first param IDispatcher.Prepare<TAction>().DispatchAsync(bool asSafe = false, CancellationToken ct = default) to execute the context with race condition resistance.
  • +
  • dispatcer.AsUnSafe() turn off the the rc resistance, it is useful to disconnect a usually safe flow to prevent cancellation.
  • +
+

Performance

+
    +
  • Improved Dispatcher Performance up to 24% by removing class creation via reflection at every dispatch.
  • +
  • Improved Dispatch Performance up to 20% by only performing allocation for the middleware class when middlewares are actually present. (Note: the second you start adding middleware slight performance drop is expected).
  • +
  • Improved Reducer Performance, we initially tested every single state registered to compare if the running action had reducers... now we get reducers by Action or by State directly.
  • +
+

Major Fixes

+
    +
  • Await feature did not await the full pipeline, subsequent dispatches were set as normal fire-and-yield unless specified. Now the await feature passes down the await flag thus fully awaiting as the intended feature was design for.
  • +
+

v2.2.0

Fix

  • Major fix of Middleware not able to be registered multiple times therefore prevent multiple middleware to work for same interface.

v2.1.0

-

Performance

+

Performance

  • The Global Tracker now uses an event‑based mechanism to invoke Self Disposal Check directly on each IStatePulse instance, removing the need to iterate the internal registry. The registry itself is scheduled for removal in version 3.0 and is now marked with the appropriate Obsolete attribute. Cleanup is no longer driven by a continuous loop; instead, the tracker schedules a self‑check only when activity occurs in the registry and goes idle when the application becomes inactive. This preserves memory‑leak protection without a permanent cycle. A minimum delay of 10 seconds is enforced between checks, and when burst activity occurs, only the final activity triggers a single final cleanup run rather than multiple redundant executions.

Fix

This is one of those “bug or feature?” situations. By design, StateOf<TState>() is meant to notify the component when its state changes. However, earlier Blazor assumptions overlooked scenarios where one state might render through one route while another state uses a different route.

-

Before version 2.0.11, StatePulse always treated the latest route as the default route for all states registered by a component, this is was BUG. Starting with 2.0.11, each state that a component subscribes to StatePulse now respects those route differences if any.

+

Before version 2.1.0, StatePulse always treated the latest route as the default route for all states registered by a component, this is was BUG. Starting with 2.1.0, each state that a component subscribes to StatePulse now respects those route differences if any.

v2.0.1

Fixes

  • Moved Fluent API Extension Methods into Abstraction Package.

v2.0.0

-

BREAKING CHANGES

+

BREAKING CHANGES

  • Interface signatures have changed.
    @@ -88,7 +121,7 @@

    BREAKING CH Use Prepared(instance).DispatchAsync() instead;

-

New Features

+

New Features

  • Enhanced Configuration Options - New global configuration properties:
      @@ -148,7 +181,7 @@

      Minor Change<
    • Splited Abstractions into StatePulse.Net.Abstractions (Will not break anything Namespace is the same)

    v1.0.0

    -

    New Features

    +

    New Features

    • Action Effect Validator: Allows effects to run conditionally by validating them before execution.
    • Middleware Support: @@ -222,6 +255,6 @@

      v0.9.2
      • Deprecated now part of StatePulse regular since we have removed the dependencies to blazor component. ... that was quick!
      • -

+
\ No newline at end of file diff --git a/doc/docs/0.Versions.md b/doc/docs/0.Versions.md index b9a1b46..6b11357 100644 --- a/doc/docs/0.Versions.md +++ b/doc/docs/0.Versions.md @@ -3,10 +3,39 @@ slug: versions title: Updates sidebar_position: 0 --- +## v3.0.0 +### BREAKING CHANGES +- `IDispatchTracker` is not longer taking a TAction and the service is no longer register per action but instead as a scope service `IDispatchTracker`. +- `IDispatcher.Prepare().DispatchAsync(CancellationToken ct = default)` no longer returns `Guid` as it was unused artifacts of a idea of a system for cancellations. +- `IDispatcher.Prepare().DispatchAsync(bool asSafe = false, CancellationToken ct = default)` no longer takes `bool` as first param instead use fluent api `.AsSafe()` to achieve the same result. + +### WARNING CHANGES +- `IDispatcherMiddleware.OnDispatchFailure(Exception exception, object action);` renamed arguments to `IDispatcherMiddleware.OnDispatchFailure(Exception ex, object action);` which could trigger compiler warnings. +- +### New Features +- Dispatch now supports Cancellation tokens. +- Token are passed down to all subsequent chain until you define a different one `DispatchAsync(false,new CT);`. +- `.DoNotAwait()` is now available, design for effect sub-dispatches when specific dispatch should not be awaited in case the origin call awaits the full pipeline... some tasks should not be awaited? that's your method. +- Access Token from effects using `dispatcher.CancelToken` which is default or correspond to upper chain origin. +- You can check if you shouldd stop execution of effect using `dispatcher.IsCancellationRequested`. +- `dispatcher.IsCancellationRequested` include now if the anti-duplication chain that was cancelled but slipped through the cracks if any you can now catch it in the effects. +- `dispatcer.AsSafe()` replaces the first param `IDispatcher.Prepare().DispatchAsync(bool asSafe = false, CancellationToken ct = default)` to execute the context with race condition resistance. +- `dispatcer.AsUnSafe()` turn off the the rc resistance, it is useful to disconnect a usually safe flow to prevent cancellation. + +### Performance +- Improved Dispatcher Performance up to 24% by removing class creation via reflection at every dispatch. +- Improved Dispatch Performance up to 20% by only performing allocation for the middleware class when middlewares are actually present. (Note: the second you start adding middleware slight performance drop is expected). +- Improved Reducer Performance, we initially tested every single state registered to compare if the running action had reducers... now we get reducers by Action or by State directly. + +### Major Fixes +- Await feature did not await the full pipeline, subsequent dispatches were set as normal fire-and-yield unless specified. Now the await feature passes down the await flag thus fully awaiting as the intended feature was design for. + + ## v2.2.0 ### Fix - Major fix of Middleware not able to be registered multiple times therefore prevent multiple middleware to work for same interface. + ## v2.1.0 ### Performance - The Global Tracker now uses an event‑based mechanism to invoke Self Disposal Check directly on each `IStatePulse` instance, removing the need to iterate the internal registry. The registry itself is scheduled for removal in version 3.0 and is now marked with the appropriate `Obsolete` attribute. Cleanup is no longer driven by a continuous loop; instead, the tracker schedules a self‑check only when activity occurs in the registry and goes idle when the application becomes inactive. This preserves memory‑leak protection without a permanent cycle. A minimum delay of 10 seconds is enforced between checks, and when burst activity occurs, only the final activity triggers a single final cleanup run rather than multiple redundant executions. @@ -14,7 +43,7 @@ sidebar_position: 0 ### Fix This is one of those “bug or feature?” situations. By design, `StateOf()` is meant to notify the component when its state changes. However, earlier Blazor assumptions overlooked scenarios where one state might render through one route while another state uses a different route. -Before version 2.0.11, StatePulse always treated the latest route as the default route for all states registered by a component, this is was **BUG**. Starting with 2.0.11, each state that a component subscribes to StatePulse now respects those route differences if any. +Before version 2.1.0, StatePulse always treated the latest route as the default route for all states registered by a component, this is was **BUG**. Starting with 2.1.0, each state that a component subscribes to StatePulse now respects those route differences if any. ## v2.0.1 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 7a4786c..e683fa5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,6 +1,6 @@ - 2.2.0 + 3.0.0 Maksim Shimshon © 2026 diff --git a/src/StatePulse.NET/Configuration/ConfigureOptions.cs b/src/StatePulse.NET/Configuration/ConfigureOptions.cs index 212087c..4c11c45 100644 --- a/src/StatePulse.NET/Configuration/ConfigureOptions.cs +++ b/src/StatePulse.NET/Configuration/ConfigureOptions.cs @@ -16,14 +16,10 @@ public class ConfigureOptions public Type[] AutoRegisterTypes { get; set; } = new Type[] { }; private long _versionTicker; - private readonly static Object _lock = new(); public long GetNextVersion() { - lock (_lock) - { - var next = Interlocked.Increment(ref _versionTicker); - return next; - } + var next = Interlocked.Increment(ref _versionTicker); + return next; } } diff --git a/src/StatePulse.NET/Engine/DispatchEntry.cs b/src/StatePulse.NET/Engine/DispatchEntry.cs index fa53707..149d840 100644 --- a/src/StatePulse.NET/Engine/DispatchEntry.cs +++ b/src/StatePulse.NET/Engine/DispatchEntry.cs @@ -1,19 +1,22 @@ namespace StatePulse.Net.Engine; - -using StatePulse.Net; -public class DispatchEntry : IDispatchEntry where TAction : IAction +//public class DispatchEntry : IDispatchEntry where TAction : IAction +public class DispatchEntry : IDispatchEntry { - private readonly IDispatcherPrepper _action; + //private readonly IDispatcherPrepper _action; private readonly Guid _guid; + private readonly Type _actionType; + public Guid Id => _guid; - public IDispatcherPrepper Action => _action; + //public IDispatcherPrepper Action => _action; private readonly CancellationTokenSource _tokenSource = new(); public DateTime Execution { get; } private int _disposed; - public DispatchEntry(Guid id, IDispatcherPrepper action) + //public DispatchEntry(Guid id, IDispatcherPrepper action) + public DispatchEntry(Guid id, Type actionType) { _guid = id; - _action = action; + _actionType = actionType; + //_action = action; Execution = DateTime.UtcNow; } @@ -29,6 +32,16 @@ public void Cancel() _tokenSource.Dispose(); } } + // Equality based on Id + public bool Equals(DispatchEntry? other) + { + if (other is null) return false; + return Id == other.Id; + } + + public override bool Equals(object? obj) => Equals(obj as DispatchEntry); + + public override int GetHashCode() => Id.GetHashCode(); public bool IsCancelled => Volatile.Read(ref _disposed) == 1 || diff --git a/src/StatePulse.NET/Engine/IDispatchEntry.cs b/src/StatePulse.NET/Engine/IDispatchEntry.cs index 1a53e30..4aaabdc 100644 --- a/src/StatePulse.NET/Engine/IDispatchEntry.cs +++ b/src/StatePulse.NET/Engine/IDispatchEntry.cs @@ -3,4 +3,5 @@ public interface IDispatchEntry { bool IsCancelled { get; } + void Cancel(); } diff --git a/src/StatePulse.NET/Engine/IDispatchHandler.cs b/src/StatePulse.NET/Engine/IDispatchHandler.cs index ebd3812..d9e41ef 100644 --- a/src/StatePulse.NET/Engine/IDispatchHandler.cs +++ b/src/StatePulse.NET/Engine/IDispatchHandler.cs @@ -1,7 +1,11 @@ using StatePulse.Net.Engine.Implementations; namespace StatePulse.Net.Engine; + public interface IDispatchHandler { - Task MaintainChainKey(DispatchTrackingIdentity chainKey); + void MaintainChainKey(DispatchTrackingIdentity chainKey); + void AssignToken(CancellationToken ct); + void NextAwaited(); + void NextSafe(); } diff --git a/src/StatePulse.NET/Engine/IDispatchTracker.cs b/src/StatePulse.NET/Engine/IDispatchTracker.cs index d04cb82..d40a0ce 100644 --- a/src/StatePulse.NET/Engine/IDispatchTracker.cs +++ b/src/StatePulse.NET/Engine/IDispatchTracker.cs @@ -5,9 +5,6 @@ public interface IDispatchTracker public bool IsCancelled(Guid id, long version); public long CurrentVersion { get; } public bool CreateExecutingAction(Guid id, object action, long version); -} -public interface IDispatchTracker : IDispatchTracker where TAction : IAction -{ IDispatchEntry CreateEntryPoint(Guid id, object action); void DeleteEntryPoint(Guid id); void CancelDispatchFor(Guid id); diff --git a/src/StatePulse.NET/Engine/Implementations/DispatchPipeline.cs b/src/StatePulse.NET/Engine/Implementations/DispatchPipeline.cs new file mode 100644 index 0000000..70c5347 --- /dev/null +++ b/src/StatePulse.NET/Engine/Implementations/DispatchPipeline.cs @@ -0,0 +1,12 @@ +namespace StatePulse.Net.Engine.Implementations; + +internal class DispatchPipeline : IDispatchPipeline +{ + + public DispatchPipeline(Guid id) + { + Id = id; + } + + public Guid Id { get; } +} diff --git a/src/StatePulse.NET/Engine/Implementations/DispatchTracker.cs b/src/StatePulse.NET/Engine/Implementations/DispatchTracker.cs index 7e27377..9b2795f 100644 --- a/src/StatePulse.NET/Engine/Implementations/DispatchTracker.cs +++ b/src/StatePulse.NET/Engine/Implementations/DispatchTracker.cs @@ -1,13 +1,13 @@ using System.Collections.Concurrent; namespace StatePulse.Net.Engine.Implementations; -internal class DispatchTracker : IDispatchTracker where TAction : IAction +internal class DispatchTracker : IDispatchTracker { - private readonly ConcurrentDictionary> _cancelTracker = new(); - public EventHandler>? OnCancel { get; set; } - public EventHandler>? OnEntry { get; set; } + private readonly ConcurrentDictionary _cancelTracker = new(); + public EventHandler? OnCancel { get; set; } + public EventHandler? OnEntry { get; set; } - public ConcurrentDictionary> CancellationTracker => _cancelTracker; + public ConcurrentDictionary CancellationTracker => _cancelTracker; public long CurrentVersion => _currentVersion; @@ -35,7 +35,7 @@ public bool CreateExecutingAction(Guid id, object action, long version) public IDispatchEntry CreateEntryPoint(Guid id, object action) { CancelAll(); - var item = new DispatchEntry(id, (IDispatcherPrepper)action); + var item = new DispatchEntry(id, action.GetType()); _cancelTracker.TryAdd(id, item); OnEntry?.Invoke(this, item); diff --git a/src/StatePulse.NET/Engine/Implementations/DispatchTrackingIdentity.cs b/src/StatePulse.NET/Engine/Implementations/DispatchTrackingIdentity.cs index f9eaef4..29c5e8b 100644 --- a/src/StatePulse.NET/Engine/Implementations/DispatchTrackingIdentity.cs +++ b/src/StatePulse.NET/Engine/Implementations/DispatchTrackingIdentity.cs @@ -2,7 +2,7 @@ public record DispatchTrackingIdentity { - public Guid Id { get; init; } + public IDispatchPipeline Pipeline { get; init; } = default!; public Type EntryType { get; init; } = default!; public IDispatchEntry TrackedEntry { get; set; } = default!; public long Version { get; init; } diff --git a/src/StatePulse.NET/Engine/Implementations/Dispatcher.cs b/src/StatePulse.NET/Engine/Implementations/Dispatcher.cs index bc1f558..4946438 100644 --- a/src/StatePulse.NET/Engine/Implementations/Dispatcher.cs +++ b/src/StatePulse.NET/Engine/Implementations/Dispatcher.cs @@ -4,18 +4,38 @@ internal class Dispatcher : IDispatcher, IDispatchHandler { private readonly IServiceProvider _serviceProvider; private DispatchTrackingIdentity? _chainKey; + private CancellationToken _cancelToken = default; + private bool _forcedSync = false; + private bool _safe = false; + public CancellationToken CancelToken => _cancelToken; + public bool IsCancellationRequested => IsChainKeyCancelled() || CancelToken.IsCancellationRequested; + private bool IsChainKeyCancelled() + { + if (_chainKey == default) return false; + return _chainKey.Tracker().IsCancelled(_chainKey.Pipeline.Id, _chainKey.Version); + } public Dispatcher(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } - public Task MaintainChainKey(DispatchTrackingIdentity chainKey) + public void MaintainChainKey(DispatchTrackingIdentity chainKey) { _chainKey = chainKey; - return Task.CompletedTask; } - + public void AssignToken(CancellationToken ct) + { + _cancelToken = ct; + } + public void NextAwaited() + { + _forcedSync = true; + } + public void NextSafe() + { + _safe = true; + } public IDispatcherPrepper Prepare(params object[] constructor) where TAction : IAction { var instanceAction = Activator.CreateInstance(typeof(TAction), constructor) @@ -34,10 +54,11 @@ public IDispatcherPrepper Prepared(TAction instance) where TAc private IDispatcherPrepper CreatePrepper(TAction Instance) where TAction : IAction { var passKeyChain = _chainKey?.EntryType ?? typeof(TAction); - var dispatcherPrepperType = typeof(DispatcherPrepper<,>).MakeGenericType(typeof(TAction), passKeyChain); - var instance = Activator.CreateInstance(dispatcherPrepperType, Instance, _serviceProvider, _chainKey) - ?? throw new InvalidOperationException($"Cannot create instance of {typeof(TAction).Name} with given constructor parameters."); - return (instance as IDispatcherPrepper)!; + var instance = new DispatcherPrepper(Instance, _serviceProvider, _chainKey, _cancelToken, _forcedSync, _safe); + //var dispatcherPrepperType = typeof(DispatcherPrepper<,>).MakeGenericType(typeof(TAction), passKeyChain); + //var instance = Activator.CreateInstance(dispatcherPrepperType, Instance, _serviceProvider, _chainKey, _cancelToken, forcedSync) + //?? throw new InvalidOperationException($"Cannot create instance of {typeof(TAction).Name} with given constructor parameters."); + return instance; } } diff --git a/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper.cs b/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper.cs index cd905c0..5d9a571 100644 --- a/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper.cs +++ b/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper.cs @@ -4,30 +4,38 @@ using System.Reflection; namespace StatePulse.Net.Engine.Implementations; -internal partial class DispatcherPrepper : IDispatcherPrepper +internal partial class DispatcherPrepper : IDispatcherPrepper where TAction : IAction - where TActionChain : IAction { private readonly TAction _action; private readonly IServiceProvider _serviceProvider; private readonly IStatePulseRegistry _statePulseRegistry; private readonly DispatchTrackingIdentity? _chainKey; - private readonly IDispatchTracker _tracker; + private readonly IDispatchTracker _tracker; private long _currentVersion = -1; - public DispatcherPrepper(TAction action, IServiceProvider serviceProvider, DispatchTrackingIdentity? chainKey) + private CancellationToken _cancelToken; + private bool _safe; + private static object _reducerLock = new(); + + public DispatcherPrepper(TAction action, IServiceProvider serviceProvider, DispatchTrackingIdentity? chainKey, CancellationToken ct = default, bool sync = false, bool? safe = null) { + _forceSyncronous = sync; + _safe = safe ?? (_action is ISafeAction); + _cancelToken = ct; _action = action!; _serviceProvider = serviceProvider; _statePulseRegistry = _serviceProvider.GetRequiredService(); _chainKey = chainKey; - var trackerTypeAction = typeof(IDispatchTracker<>).MakeGenericType(typeof(TActionChain)); - _tracker = (IDispatchTracker)_serviceProvider.GetRequiredService(trackerTypeAction); + _tracker = _serviceProvider.GetRequiredService(); + } public TAction ActionInstance => _action; - public async Task DispatchFastAsync() + public async Task DispatchFastAsync(CancellationToken ct) { + if (ct != default) + _cancelToken = ct; if (_chainKey == default) if (_forceSyncronous) await ProcessDispatch(false, Guid.Empty); @@ -45,8 +53,10 @@ public async Task DispatchFastAsync() } - public async Task DispatchSafeAsync() + public async Task DispatchSafeAsync(CancellationToken ct) { + if (ct != default) + _cancelToken = ct; if (_chainKey == default) { Guid nextKey = Guid.NewGuid(); @@ -66,7 +76,7 @@ public async Task DispatchSafeAsync() _ = ProcessDispatch(false, Guid.Empty); } - return _chainKey?.Id ?? Guid.Empty; + return _chainKey?.Pipeline.Id ?? Guid.Empty; } private static MethodInfo? _cachedEffectMethod; @@ -82,33 +92,47 @@ protected async Task ProcessDispatch(bool entryPoint, Guid nextId) _currentVersion = ServiceRegisterExt.ConfigureOptions.GetNextVersion(); nextChain = new DispatchTrackingIdentity() { - Id = nextId, + Pipeline = new DispatchPipeline(nextId), EntryType = typeof(TAction), Version = _currentVersion, Tracker = () => _tracker }; - _tracker.CreateExecutingAction(nextChain.Id, this, nextChain.Version); - nextChain.TrackedEntry = _tracker.CreateEntryPoint(nextChain.Id, this); + _tracker.CreateExecutingAction(nextChain.Pipeline.Id, this, nextChain.Version); + nextChain.TrackedEntry = _tracker.CreateEntryPoint(nextChain.Pipeline.Id, this); } var disaptchMiddlewares = _serviceProvider.GetServices(); - var dispatchMiddlewareTasks = new List(); - foreach (var item in disaptchMiddlewares) + bool hasMiddlewares = disaptchMiddlewares.Any(); + if (hasMiddlewares) { - dispatchMiddlewareTasks.Add(item.BeforeDispatch(_action)); + var dispatchMiddlewareTasks = new List(); + foreach (var item in disaptchMiddlewares) + { + dispatchMiddlewareTasks.Add(item.BeforeDispatch(_action)); + } + await Task.WhenAll(dispatchMiddlewareTasks); + } - await Task.WhenAll(dispatchMiddlewareTasks); + if (IsChainCancelled(nextChain)) return; - try { var dispatcherService = _serviceProvider.GetRequiredService(); var dispatchElement = dispatcherService.CreateDispatch(); if (nextChain != default) - await dispatchElement.Handler.MaintainChainKey(nextChain); + dispatchElement.Handler.MaintainChainKey(nextChain); + + if (_cancelToken != default) + dispatchElement.Handler.AssignToken(_cancelToken); + if (_forceSyncronous) + dispatchElement.Handler.NextAwaited(); + + if (_safe) + dispatchElement.Handler.NextSafe(); + if (_dispatchOrdering == DispatchOrdering.ReducersFirst) await RunReducer(nextChain); @@ -122,31 +146,29 @@ protected async Task ProcessDispatch(bool entryPoint, Guid nextId) { foreach (var item in disaptchMiddlewares) _ = item.OnDispatchFailure(ex, _action); - - - if (_forceSyncronous) throw; } finally { - - dispatchMiddlewareTasks = new List(); - foreach (var item in disaptchMiddlewares) + if (hasMiddlewares) { - dispatchMiddlewareTasks.Add(item.AfterDispatch(_action)); + var dispatchMiddlewareTasks = new List(); + foreach (var item in disaptchMiddlewares) + { + dispatchMiddlewareTasks.Add(item.AfterDispatch(_action)); + } + await Task.WhenAll(dispatchMiddlewareTasks); } - await Task.WhenAll(dispatchMiddlewareTasks); } } - public async Task DispatchAsync(bool asSafe = false) + public async Task DispatchAsync(CancellationToken ct = default) { - - if ((_action is ISafeAction) || asSafe) - return await DispatchSafeAsync(); - await DispatchFastAsync(); - return Guid.Empty; + if (_safe) + await DispatchSafeAsync(ct); + else + await DispatchFastAsync(ct); } private async Task RunEffects(DispatchTrackingIdentity? nextChain, DispatchFactoryElement dispatcherService) @@ -157,10 +179,16 @@ private async Task RunEffects(DispatchTrackingIdentity? nextChain, DispatchFacto var effectServices = _serviceProvider.GetServices(effectType); var effectMiddlewares = _serviceProvider.GetServices(); - var effectMiddlewareTasks = RunMiddlewareEffect(effectMiddlewares, p => p.BeforeEffect(_action), p => p == MiddlewareEffectBehavior.PerGroupEffects); + bool hasEffectMiddlewares = effectMiddlewares.Any(); + Task? effectMiddlewareTasks = default; + if (hasEffectMiddlewares) + { + effectMiddlewareTasks = RunMiddlewareEffect(effectMiddlewares, p => p.BeforeEffect(_action), p => p == MiddlewareEffectBehavior.PerGroupEffects); + + if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior == Configuration.MiddlewareTaskBehavior.Await) + await effectMiddlewareTasks; + } - if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior == Configuration.MiddlewareTaskBehavior.Await) - await effectMiddlewareTasks; foreach (var effectService in effectServices) { @@ -191,22 +219,25 @@ private async Task RunEffects(DispatchTrackingIdentity? nextChain, DispatchFacto if (!allEffects.IsCompletedSuccessfully) await allEffects; - - if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior != Configuration.MiddlewareTaskBehavior.Await) - _ = Task.Run(async () => + if (hasEffectMiddlewares) + { + if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior != Configuration.MiddlewareTaskBehavior.Await) + _ = Task.Run(async () => + { + if (effectMiddlewareTasks != default && !effectMiddlewareTasks.IsCompletedSuccessfully) + await effectMiddlewareTasks; + await RunMiddlewareEffect(effectMiddlewares, p => p.AfterEffect(_action), p => p == MiddlewareEffectBehavior.PerGroupEffects); + }); + else { - if (!effectMiddlewareTasks.IsCompletedSuccessfully) + if (effectMiddlewareTasks != default && !effectMiddlewareTasks.IsCompletedSuccessfully) await effectMiddlewareTasks; await RunMiddlewareEffect(effectMiddlewares, p => p.AfterEffect(_action), p => p == MiddlewareEffectBehavior.PerGroupEffects); - }); - else - { - if (!effectMiddlewareTasks.IsCompletedSuccessfully) - await effectMiddlewareTasks; - await RunMiddlewareEffect(effectMiddlewares, p => p.AfterEffect(_action), p => p == MiddlewareEffectBehavior.PerGroupEffects); + } } + } private async Task RunEffectValidators(Type effectService, IEnumerable effectMiddlewares) { @@ -243,26 +274,31 @@ private async Task RunReducer(DispatchTrackingIdentity? nextChain) { var actionType = typeof(TAction); var middlewares = _serviceProvider.GetServices(); - foreach (var stateType in _statePulseRegistry.KnownStates) + bool hasMiddlewares = middlewares.Any(); + foreach (var reducerDescriptor in _statePulseRegistry.GetReducersByAction(actionType)) { if (IsChainCancelled(nextChain)) return; - var reducerType = typeof(IReducer<,>).MakeGenericType(stateType, actionType); - var stateAccessorType = _statePulseRegistry.KnownStateToAccessors[stateType]; + + var stateAccessorType = _statePulseRegistry.KnownStateToAccessors[reducerDescriptor.StateType]; var stateService = () => _serviceProvider.GetRequiredService(stateAccessorType); - var reducerService = _serviceProvider.GetService(reducerType); + var reducerService = _serviceProvider.GetService(reducerDescriptor.ServiceType); // Trigger Reducer if (reducerService != default) { - + Task? middlewareTasks = default; var currentState = _statePulseRegistry.KnownStateAccessorsStateGetter[stateAccessorType](stateService())!; - var middlewareTasks = RunMiddlewareReducer(middlewares, p => p.BeforeReducing(currentState, _action)); - if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior == Configuration.MiddlewareTaskBehavior.Await) - await middlewareTasks; + if (hasMiddlewares) + { + middlewareTasks = RunMiddlewareReducer(middlewares, p => p.BeforeReducing(currentState, _action)); + if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior == Configuration.MiddlewareTaskBehavior.Await) + await middlewareTasks; + } - var newState = _statePulseRegistry.KnownReducersReduceMethod[reducerType](reducerService, - new object[] { + object? newState = default; + newState = _statePulseRegistry.KnownReducersReduceMethod[reducerDescriptor.ServiceType](reducerService, + new object[] { currentState, _action }!)!; @@ -279,26 +315,30 @@ private async Task RunReducer(DispatchTrackingIdentity? nextChain) Type originType = typeof(TAction); if (nextChain != default) originType = nextChain.EntryType; - var isAccepted = _statePulseRegistry.KnownStateAccessorsStateUpdater[stateAccessorType](stateService(), newState, originType, usedVersion, nextChain?.Id ?? Guid.Empty); + var isAccepted = _statePulseRegistry.KnownStateAccessorsStateUpdater[stateAccessorType](stateService(), newState, originType, usedVersion, nextChain?.Pipeline.Id ?? Guid.Empty); var stateVersioning = (StateVersioning)_statePulseRegistry.KnownStateAccessorsVersionGetter[stateAccessorType].Invoke(stateService())!; if (!isAccepted) return; // Regardless of settings ensure BeforeReducing is finished before calling after reducing. - if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior != Configuration.MiddlewareTaskBehavior.Await) - _ = Task.Run(async () => + if (hasMiddlewares) + { + if (ServiceRegisterExt.ConfigureOptions.MiddlewareTaskBehavior == Configuration.MiddlewareTaskBehavior.DoNotAwait) + _ = Task.Run(async () => + { + if (middlewareTasks != default && !middlewareTasks.IsCompletedSuccessfully) + await middlewareTasks; + _ = RunMiddlewareReducer(middlewares, p => p.AfterReducing(newState, _action)); + }); + else { - if (!middlewareTasks.IsCompletedSuccessfully) + if (middlewareTasks != default && !middlewareTasks.IsCompletedSuccessfully) await middlewareTasks; await RunMiddlewareReducer(middlewares, p => p.AfterReducing(newState, _action)); - }); - else - { - if (!middlewareTasks.IsCompletedSuccessfully) - await middlewareTasks; - await RunMiddlewareReducer(middlewares, p => p.AfterReducing(newState, _action)); + } } + } } } @@ -328,8 +368,8 @@ private Task RunMiddlewareReducer(IEnumerable middlewares, F return Task.WhenAll(middlewaresTasks); } private bool IsChainCancelled(DispatchTrackingIdentity? nextChain = default) - => - (nextChain != default && (nextChain.TrackedEntry.IsCancelled || nextChain.Tracker().IsCancelled(nextChain.Id, nextChain.Version))) || - (_chainKey != default && (_chainKey.TrackedEntry.IsCancelled || _chainKey.Tracker().IsCancelled(_chainKey.Id, _chainKey.Version))); + => _cancelToken.IsCancellationRequested || + (nextChain != default && (nextChain.TrackedEntry.IsCancelled || nextChain.Tracker().IsCancelled(nextChain.Pipeline.Id, nextChain.Version))) || + (_chainKey != default && (_chainKey.TrackedEntry.IsCancelled || _chainKey.Tracker().IsCancelled(_chainKey.Pipeline.Id, _chainKey.Version))); } diff --git a/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper_Flags.cs b/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper_Flags.cs index fb2ec7e..a015917 100644 --- a/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper_Flags.cs +++ b/src/StatePulse.NET/Engine/Implementations/DispatcherPrepper_Flags.cs @@ -1,22 +1,42 @@ using StatePulse.Net.Configuration; namespace StatePulse.Net.Engine.Implementations; -internal partial class DispatcherPrepper : IDispatcherPrepper +internal partial class DispatcherPrepper : IDispatcherPrepper where TAction : IAction - where TActionChain : IAction { private DispatchEffectBehavior _dispatchEffectBehavior = ServiceRegisterExt.ConfigureOptions.DispatchEffectBehavior; private DispatchOrdering _dispatchOrdering = ServiceRegisterExt.ConfigureOptions.DispatchOrderBehavior; private bool _forceSyncronous; - + public IDispatcherPrepper AsSafe() + { + _safe = true; + return this; + } + public IDispatcherPrepper AsUnSafe() + { + _safe = false; + return this; + } public IDispatcherPrepper Await() { _forceSyncronous = true; return this; } + /// + /// Forces this dispatch to run in fire‑and‑yield mode when called from an effect. + /// Effects should await their own follow‑up dispatches, and this flag provides + /// a built‑in safeguard against side-effect that should never be fully awaited. + /// + + public IDispatcherPrepper DoNotAwait() + { + _forceSyncronous = false; + return this; + } + public IDispatcherPrepper EffectsFirst() { _dispatchOrdering = DispatchOrdering.EffectsFirst; diff --git a/src/StatePulse.NET/Engine/Implementations/StatePulseRegistry.cs b/src/StatePulse.NET/Engine/Implementations/StatePulseRegistry.cs index 96b9ab5..f2ea7a8 100644 --- a/src/StatePulse.NET/Engine/Implementations/StatePulseRegistry.cs +++ b/src/StatePulse.NET/Engine/Implementations/StatePulseRegistry.cs @@ -3,6 +3,7 @@ namespace StatePulse.Net.Engine.Implementations; using StatePulse.Net; +using StatePulse.Net.Models; using System.Linq.Expressions; using System.Reflection; @@ -16,11 +17,14 @@ internal class StatePulseRegistry : IStatePulseRegistry private readonly Dictionary _knownEffects = new(); private readonly Dictionary _knownReducers = new(); + private readonly Dictionary> _knownReducersByAction = new(); + private readonly Dictionary> _knownReducersByState = new(); private readonly Dictionary _knownStateToReducers = new(); private readonly Dictionary> _knownReducersReduceMethod = new(); private readonly Dictionary> _knownReducersTaskResult = new(); private readonly List _knownActions = new(); private readonly Dictionary _knownActionValidators = new(); + public int DispatchMiddlewareCount { get; internal set; } public IReadOnlyList KnownStates => _knownStates; public IReadOnlyDictionary KnownEffects => _knownEffects; @@ -41,9 +45,31 @@ internal class StatePulseRegistry : IStatePulseRegistry public void RegisterEffect(Type effectType, Type interfaceType) => _knownEffects[effectType] = interfaceType; public void RegisterReducer(Type reducerType, Type interfaceType) { + + var stateType = reducerType.GetGenericArguments()[0]; + var actionType = reducerType.GetGenericArguments()[1]; + var descriptor = new ReducerDescriptor() + { + ActionType = actionType, + ServiceType = reducerType, + StateType = stateType + }; + if (_knownReducersByAction.ContainsKey(actionType) && !_knownReducersByAction[actionType].Contains(descriptor)) + _knownReducersByAction[actionType].Add(descriptor); + else if (!_knownReducersByAction.ContainsKey(actionType)) + _knownReducersByAction[actionType] = new() { descriptor }; + else + return; + + if (_knownReducersByState.ContainsKey(stateType) && !_knownReducersByState[stateType].Contains(descriptor)) + _knownReducersByState[stateType].Add(descriptor); + else if (!_knownReducersByState.ContainsKey(stateType)) + _knownReducersByState[stateType] = new() { descriptor }; + else + return; + var reduceMethodName = nameof(IReducer.Reduce); var method = reducerType.GetMethod(reduceMethodName)!; - var stateType = method.ReturnType; // This is TState _knownReducersTaskResult[reducerType] = stateType.BuildTaskResultGetter(); _knownReducersReduceMethod[reducerType] = method.CreateDynamicReflectionInvoker(); _knownReducers.TryAdd(reducerType, interfaceType); @@ -115,4 +141,10 @@ private static Func BuildChangeStateDele writerParam ).Compile(); } + + public List GetReducersByAction(Type actionType) + => !_knownReducersByAction.ContainsKey(actionType) ? new() : _knownReducersByAction[actionType].ToList(); + public List GetReducersByState(Type stateType) + => !_knownReducersByState.ContainsKey(stateType) ? new() : _knownReducersByState[stateType].ToList(); + } diff --git a/src/StatePulse.NET/ServiceRegisterExt.cs b/src/StatePulse.NET/ServiceRegisterExt.cs index 67a29af..4037468 100644 --- a/src/StatePulse.NET/ServiceRegisterExt.cs +++ b/src/StatePulse.NET/ServiceRegisterExt.cs @@ -25,7 +25,7 @@ public static IServiceCollection AddStatePulseServices(this IServiceCollection s bool isSingleThreadModel = ConfigureOptions.PulseTrackingPerformance == PulseTrackingModel.SingleThreadFast || ConfigureOptions.PulseTrackingPerformance == PulseTrackingModel.BlazorWebAssemblyFast; services.AddTransient(); - + services.AddScoped(); services.AddScoped(); services.AddSingleton(Registry); services.AutoRegisterTypes(ConfigureOptions.AutoRegisterTypes); @@ -99,12 +99,7 @@ private static IServiceCollection AddStatePulseEffectValidator(this IServiceColl private static IServiceCollection AddStatePulseAction(this IServiceCollection services, Type implementation) { - // Add Action Based Singleton Dispatch Tracker. - var dispatchTrackerIface = typeof(IDispatchTracker<>).MakeGenericType(implementation); - var dispatchTracker = typeof(DispatchTracker<>).MakeGenericType(implementation); - if (services.IsDispatchTrackerRegistered(dispatchTracker)) return services; Registry.RegisterAction(implementation); - services.AddScoped(dispatchTrackerIface, dispatchTracker); return services; } @@ -154,6 +149,9 @@ private static void RegisterSelfDetectedInterface(this IServiceCollection servic (iface == effectMiddlewareType || iface == reducerMiddlewareType || iface == dispatchMiddlewareType) && !services.IsImplementationRegistered(type, iface); + if (iface == dispatchMiddlewareType) + Registry.DispatchMiddlewareCount++; + if (isMiddlewareType) services.AddTransient(iface, type); else if (!iface.IsGenericType && (iface == actionType || iface == actionSafeType)) @@ -199,25 +197,7 @@ public static bool IsEffectValidatorImplementationRegistered(this IServiceCollec ); } - public static bool IsDispatchTrackerRegistered(this IServiceCollection services, Type implementationType) - { - if (!implementationType.IsGenericType) return false; - - var interfaces = implementationType.GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDispatchTracker<>)); - - foreach (var iface in interfaces) - { - if (services.Any(s => - s.ServiceType == iface && - s.ImplementationType == implementationType)) - { - return true; // Found match - } - } - return false; - } public static bool IsStateAccessorRegistered(this IServiceCollection services, Type implementationType) { diff --git a/src/StatePulse.Net.Abstractions/IDispatchPipeline.cs b/src/StatePulse.Net.Abstractions/IDispatchPipeline.cs new file mode 100644 index 0000000..81e5a62 --- /dev/null +++ b/src/StatePulse.Net.Abstractions/IDispatchPipeline.cs @@ -0,0 +1,6 @@ +namespace StatePulse.Net; + +public interface IDispatchPipeline +{ + Guid Id { get; } +} diff --git a/src/StatePulse.Net.Abstractions/IDispatcher.cs b/src/StatePulse.Net.Abstractions/IDispatcher.cs index dbb86eb..9e4b82b 100644 --- a/src/StatePulse.Net.Abstractions/IDispatcher.cs +++ b/src/StatePulse.Net.Abstractions/IDispatcher.cs @@ -5,6 +5,8 @@ namespace StatePulse.Net; /// public interface IDispatcher { + CancellationToken CancelToken { get; } + bool IsCancellationRequested { get; } IDispatcherPrepper Prepare(params object[] constructor) where TAction : IAction; [Obsolete("Use Prepared instead.")] diff --git a/src/StatePulse.Net.Abstractions/IDispatcherMiddleware.cs b/src/StatePulse.Net.Abstractions/IDispatcherMiddleware.cs index e4b2385..6c71733 100644 --- a/src/StatePulse.Net.Abstractions/IDispatcherMiddleware.cs +++ b/src/StatePulse.Net.Abstractions/IDispatcherMiddleware.cs @@ -3,6 +3,6 @@ public interface IDispatcherMiddleware { Task BeforeDispatch(object action); - Task OnDispatchFailure(Exception exception, object action); + Task OnDispatchFailure(Exception ex, object action); Task AfterDispatch(object action); } diff --git a/src/StatePulse.Net.Abstractions/IDispatcherPrepper.cs b/src/StatePulse.Net.Abstractions/IDispatcherPrepper.cs index 6e67d2c..5db4a4f 100644 --- a/src/StatePulse.Net.Abstractions/IDispatcherPrepper.cs +++ b/src/StatePulse.Net.Abstractions/IDispatcherPrepper.cs @@ -4,7 +4,18 @@ public interface IDispatcherPrepper where TAction : IAction { TAction ActionInstance { get; } IDispatcherPrepper Await(); - Task DispatchAsync(bool asSafe = false); + /// + /// Runs pipeline as same as ISafeAction with anti duplication mechanism
+ /// Note: This is not the recommended way, youo should use ISafeAction instead. + ///
+ IDispatcherPrepper AsSafe(); + /// + /// This execute the pipeline as a regular normal untrack manner where no anti-duplication/race condition features activate.
+ /// Note: this is useful to stop the tracking of a usually tracked action and make sure that action executes completely. + ///
+ IDispatcherPrepper AsUnSafe(); + IDispatcherPrepper DoNotAwait(); + Task DispatchAsync(CancellationToken ct = default); IDispatcherPrepper EffectsFirst(); IDispatcherPrepper ReducersFirst(); IDispatcherPrepper SequentialEffects(); diff --git a/src/StatePulse.Net.Abstractions/IStatePulseRegistry.cs b/src/StatePulse.Net.Abstractions/IStatePulseRegistry.cs index ec91ba5..756e38b 100644 --- a/src/StatePulse.Net.Abstractions/IStatePulseRegistry.cs +++ b/src/StatePulse.Net.Abstractions/IStatePulseRegistry.cs @@ -1,4 +1,6 @@ -namespace StatePulse.Net; +using StatePulse.Net.Models; + +namespace StatePulse.Net; public interface IStatePulseRegistry { @@ -22,5 +24,8 @@ public interface IStatePulseRegistry void RegisterReducer(Type reducerType, Type interfaceType); void RegisterAction(Type actionType); void RegisterEffectValidator(Type actionValType, Type interfaceType); + List GetReducersByAction(Type actionType); + List GetReducersByState(Type stateType); + } diff --git a/src/StatePulse.Net.Abstractions/Models/ReducerDescriptor.cs b/src/StatePulse.Net.Abstractions/Models/ReducerDescriptor.cs new file mode 100644 index 0000000..b3d668a --- /dev/null +++ b/src/StatePulse.Net.Abstractions/Models/ReducerDescriptor.cs @@ -0,0 +1,8 @@ +namespace StatePulse.Net.Models; + +public sealed record ReducerDescriptor +{ + public Type StateType { get; init; } = default!; + public Type ActionType { get; init; } = default!; + public Type ServiceType { get; init; } = default!; +} diff --git a/src/StatePulse.Net.Benchmark/Pulses/Reducers/IncreasedCounterReducer.cs b/src/StatePulse.Net.Benchmark/Pulses/Reducers/IncreasedCounterReducer.cs index ba1ba33..02fc2a8 100644 --- a/src/StatePulse.Net.Benchmark/Pulses/Reducers/IncreasedCounterReducer.cs +++ b/src/StatePulse.Net.Benchmark/Pulses/Reducers/IncreasedCounterReducer.cs @@ -6,8 +6,8 @@ namespace StatePulse.Net.Benchmark.Pulses.Reducers; public class IncreasedCounterReducer : IReducer { - public Task ReduceAsync(CounterState state, IncreasedCounterAction action) - => Task.FromResult(state with { Counter = state.Counter + 1 }); + public CounterState Reduce(CounterState state, IncreasedCounterAction action) + => state with { Counter = state.Counter + 1 }; } diff --git a/src/StatePulse.Net.Benchmark/StatePulse.Net.Benchmark.csproj b/src/StatePulse.Net.Benchmark/StatePulse.Net.Benchmark.csproj index d8cb307..f5e98bb 100644 --- a/src/StatePulse.Net.Benchmark/StatePulse.Net.Benchmark.csproj +++ b/src/StatePulse.Net.Benchmark/StatePulse.Net.Benchmark.csproj @@ -22,11 +22,14 @@ - + + + +
diff --git a/src/StatePulse.Net.Benchmark/Tests.cs b/src/StatePulse.Net.Benchmark/Tests.cs index b7996f8..da8c01d 100644 --- a/src/StatePulse.Net.Benchmark/Tests.cs +++ b/src/StatePulse.Net.Benchmark/Tests.cs @@ -20,7 +20,6 @@ public Tests() { c.DispatchOrderBehavior = Configuration.DispatchOrdering.ReducersFirst; c.DispatchEffectBehavior = Configuration.DispatchEffectBehavior.Parallel; - c.DispatchEffectExecutionBehavior = Configuration.DispatchEffectExecutionBehavior.FireAndForget; c.MiddlewareEffectBehavior = Configuration.MiddlewareEffectBehavior.PerGroupEffects; c.MiddlewareTaskBehavior = Configuration.MiddlewareTaskBehavior.DoNotAwait; c.ScanAssemblies = [typeof(Tests).Assembly]; @@ -41,15 +40,15 @@ public Tests() } [Benchmark] - public void StatePulse_Dispatch() + public async Task StatePulse_Dispatch() { - _ = _pulseDispatcher.Prepare().Await().DispatchAsync(); + await _pulseDispatcher.Prepare().DispatchAsync(); } [Benchmark] public async Task StatePulse_SafeDispatch() { - await _pulseDispatcher.Prepare().DispatchAsync(true); + await _pulseDispatcher.Prepare().AsSafe().DispatchAsync(); } [Benchmark] @@ -63,26 +62,28 @@ public async Task StatePulse_BusrtDispatch() public async Task StatePulse_BusrtSafeDispatch() { for (int i = 0; i < 100; i++) - await _pulseDispatcher.Prepare().DispatchAsync(true); + await _pulseDispatcher.Prepare().AsSafe().DispatchAsync(); } - [Benchmark] - public async Task StatePulse_FireYieldDispatch() - { - await _pulseDispatcher.Prepare().ExecFireAndForget().DispatchAsync(); - } - [Benchmark] - public async Task StatePulse_FireYield_SequentialEffectsDispatch() - { - await _pulseDispatcher.Prepare().ExecYieldAndFire().SequentialEffects().DispatchAsync(); - } + //[Benchmark] + //public async Task Fluxor_Dispatch() + //{ + // await Task.Run(() => _fluxDispatcher.Dispatch(new IncreaseCounterAction())); + + //} + + //[Benchmark] + //public async Task Fluxor_BusrtDispatch() + //{ + // for (int i = 0; i < 100; i++) + // await Task.Run(() => _fluxDispatcher.Dispatch(new IncreaseCounterAction())); + //} + + + + - [Benchmark] - public async Task StatePulse_AwaitedDispatch() - { - await _pulseDispatcher.Prepare().ExecYieldAndFire().Await().DispatchAsync(); - } } \ No newline at end of file diff --git a/tests/StatPulse.NET.Tests/TestBase.cs b/tests/StatPulse.NET.Tests/TestBase.cs index f04363c..0dbe464 100644 --- a/tests/StatPulse.NET.Tests/TestBase.cs +++ b/tests/StatPulse.NET.Tests/TestBase.cs @@ -24,11 +24,15 @@ public abstract class TestBase : IDisposable protected TestBase() { ServiceCollection = new ServiceCollection(); - // TOOD: Remove Scan Add Manual for Tests which is best policy would most lekily avoid inconsistent + // Remove Scan Add Manual for Tests which is best policy would most lekily avoid inconsistent // service exceptions du to thread safe on bulk testing. ServiceCollection.AddStatePulseServices(); + ServiceCollection.AddStatePulseService(); + ServiceCollection.AddStatePulseService(); + ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); + ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); @@ -53,6 +57,9 @@ protected TestBase() ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); + ServiceCollection.AddStatePulseService(); + ServiceCollection.AddStatePulseService(); + ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); ServiceCollection.AddStatePulseService(); diff --git a/tests/StatPulse.NET.Tests/TestCases/InitializerTests/StatePulseInitTests.cs b/tests/StatPulse.NET.Tests/TestCases/InitializerTests/StatePulseInitTests.cs index 9fb14ef..4621fd7 100644 --- a/tests/StatPulse.NET.Tests/TestCases/InitializerTests/StatePulseInitTests.cs +++ b/tests/StatPulse.NET.Tests/TestCases/InitializerTests/StatePulseInitTests.cs @@ -55,6 +55,21 @@ public async Task DispatchingActionShouldChangeStateUsingStateOf() Assert.Equal("Maksim Shimshon", state().ProfileName); } + + [Fact] + public async Task Should_Successful_SameActionReducer_MultipleStates() + { + var dispatcher = ServiceProvider.GetRequiredService(); + // Dispatch action that changes state + var state1 = ServiceProvider.GetRequiredService>(); + var state2 = ServiceProvider.GetRequiredService>(); + await dispatcher.Prepare().Await().DispatchAsync(); + + Assert.True(state1.State.IsOpened); + Assert.True(state2.State.IsSuccessful); + } + + [Fact] public async Task DispatchingEffectShouldCorrectlyTriggerActions() { @@ -66,6 +81,40 @@ public async Task DispatchingEffectShouldCorrectlyTriggerActions() Assert.NotEmpty(stateAccessor.State.NavigationItems ?? new()); } + [Fact] + public async Task DispatchChangingDiffPropsOnSameStateShouldNotHaveConcurrentIssues() + { + var dispatcher = ServiceProvider.GetRequiredService(); + var stateAccessor = ServiceProvider.GetRequiredService>(); + bool pass = true; + for (int i = 0; i < 100; i++) + { + var t1 = dispatcher.Prepare().Await().DispatchAsync(); + var t2 = dispatcher.Prepare().Await().DispatchAsync(); + await Task.WhenAll([t1, t2]); + if (stateAccessor.State.IsOpened != true || stateAccessor.State.NavigationItems == default || stateAccessor.State.NavigationItems.Count <= 0) + { + pass = false; + break; + } + await dispatcher.Prepare().Await().DispatchAsync(); + } + Assert.True(pass); + } + + [Fact] + public async Task DispatchCancelTokenShouldWork() + { + var dispatcher = ServiceProvider.GetRequiredService(); + var stateAccessor = ServiceProvider.GetRequiredService>(); + bool pass = true; + var ct = new CancellationTokenSource(); + var t1 = dispatcher.Prepare().Await().DispatchAsync(ct.Token); + ct.Cancel(); + await t1; + Assert.True(stateAccessor.State.NavigationItems == default); + } + [Theory] [InlineData("Test")] [InlineData("Error")] @@ -85,7 +134,7 @@ public async Task DispatchingBurstShouldTriggerSafetyCancel() { var scopedServices = ServiceProvider.CreateScope().ServiceProvider; var stateAccessor = scopedServices.GetRequiredService>(); - var tracker = scopedServices.GetRequiredService>(); + var tracker = scopedServices.GetRequiredService(); // Dispatch action that changes state int changes = 0; @@ -116,7 +165,8 @@ public async Task DispatchingBurstShouldTriggerSafetyCancel() _ = dispatcher.Prepare() .With(p => p.TestData, winingValue) .With(p => p.Delay, timing[i]) - .DispatchAsync(true); + .AsSafe() + .DispatchAsync(); possibleRaceConditions.Add(winingValue); } await Task.Delay(timing.Sum()); @@ -136,7 +186,7 @@ public async Task DispatchingBurstShouldTriggerSafetyCancel() public async Task DispatchingBurstShouldTriggerInconsistentResults() { var dispatcher = ServiceProvider.GetRequiredService(); - var tracker = ServiceProvider.GetRequiredService>(); + var tracker = ServiceProvider.GetRequiredService(); var stateAccessor = ServiceProvider.GetRequiredService>(); // Dispatch action that changes state int changes = 0; diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuOpenAction.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuOpenAction.cs index 42e3933..a863f22 100644 --- a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuOpenAction.cs +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuOpenAction.cs @@ -1,6 +1,7 @@ using StatePulse.Net; namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Actions; + public record MainMenuOpenAction : IAction { } diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuStateResetAction.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuStateResetAction.cs new file mode 100644 index 0000000..24e0056 --- /dev/null +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Actions/MainMenuStateResetAction.cs @@ -0,0 +1,7 @@ +using StatePulse.Net; + +namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Actions; + +internal sealed record MainMenuStateResetAction : IAction +{ +} diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuLoadNavigationItemsEffect.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuLoadNavigationItemsEffect.cs index 08a52e2..275ee0a 100644 --- a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuLoadNavigationItemsEffect.cs +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuLoadNavigationItemsEffect.cs @@ -2,10 +2,14 @@ using StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Actions; namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Effects; + internal class MainMenuLoadNavigationItemsEffect : IEffect { public async Task EffectAsync(MainMenuLoadNavigationItemsAction action, IDispatcher dispatcher) { - await dispatcher.Prepare(() => new MainMenuLoadNavigationItemsResultAction(new() { "sda" })).DispatchAsync(); + if (dispatcher.IsCancellationRequested) return; + await Task.Delay(10); + if (dispatcher.IsCancellationRequested) return; + await dispatcher.Prepared(new MainMenuLoadNavigationItemsResultAction(new() { "sda" })).DispatchAsync(); } } diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuOpenEffect.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuOpenEffect.cs index 14662c0..ccb74c9 100644 --- a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuOpenEffect.cs +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Effects/MainMenuOpenEffect.cs @@ -2,10 +2,12 @@ using StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Actions; namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Effects; + internal class MainMenuOpenEffect : IEffect { public async Task EffectAsync(MainMenuOpenAction action, IDispatcher dispatcher) { + if (dispatcher.IsCancellationRequested) return; await dispatcher.Prepare().DispatchAsync(); } } diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/MainMenuLoaderStartDispatchMiddleware.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/MainMenuLoaderStartDispatchMiddleware.cs index e632d46..1120ace 100644 --- a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/MainMenuLoaderStartDispatchMiddleware.cs +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/MainMenuLoaderStartDispatchMiddleware.cs @@ -18,6 +18,7 @@ public Task AfterEffect(object action) public Task AfterReducing(object state, object action) { + throw new Exception(); Console.WriteLine($"{action.GetType().Name} Executed."); return Task.CompletedTask; } diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/SecondDispatchMiddleware.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/SecondDispatchMiddleware.cs new file mode 100644 index 0000000..377592b --- /dev/null +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Middlewares/SecondDispatchMiddleware.cs @@ -0,0 +1,56 @@ +using StatePulse.Net; + +namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Middlewares; + +internal class SecondDispatchMiddleware : IDispatcherMiddleware, IEffectMiddleware, IReducerMiddleware +{ + public Task AfterDispatch(object action) + { + Console.WriteLine($"{action.GetType().Name} Executed."); + return Task.CompletedTask; + } + + public Task AfterEffect(object action) + { + Console.WriteLine($"{action.GetType().Name} Executed."); + return Task.CompletedTask; + } + + public Task AfterReducing(object state, object action) + { + Console.WriteLine($"{action.GetType().Name} Executed."); + return Task.CompletedTask; + } + + public Task BeforeDispatch(object action) + { + Console.WriteLine($"{action.GetType().Name} Starting."); + return Task.CompletedTask; + } + + public Task BeforeEffect(object action) + { + Console.WriteLine($"{action.GetType().Name} Executed."); + return Task.CompletedTask; + } + + public Task BeforeReducing(object state, object action) + { + Console.WriteLine($"{state.GetType().Name} {action.GetType().Name} Executed."); + return Task.CompletedTask; + } + + public Task OnDispatchFailure(Exception exception, object action) + => Task.CompletedTask; + + public Task WhenEffectValidationFailed(object action, object effectValidator) + { + Console.WriteLine($"{action.GetType().Name} {effectValidator.GetType().Name}r Executed."); + return Task.CompletedTask; + } + public Task WhenEffectValidationSucceed(object action, object effectValidator) + { + Console.WriteLine($"{action.GetType().Name} {effectValidator.GetType().Name}e Executed."); + return Task.CompletedTask; + } +} diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Reducers/MainMenuStateResetReducer.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Reducers/MainMenuStateResetReducer.cs new file mode 100644 index 0000000..c759fe9 --- /dev/null +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Reducers/MainMenuStateResetReducer.cs @@ -0,0 +1,11 @@ +using StatePulse.Net; +using StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Actions; +using StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Store; + +namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Reducers; + +internal sealed class MainMenuStateResetReducer : IReducer +{ + public MainMenuState Reduce(MainMenuState state, MainMenuStateResetAction action) + => new(); +} diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Reducers/SuccessfulReducer.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Reducers/SuccessfulReducer.cs new file mode 100644 index 0000000..55c7611 --- /dev/null +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Reducers/SuccessfulReducer.cs @@ -0,0 +1,11 @@ +using StatePulse.Net; +using StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Actions; +using StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Store; + +namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Reducers; + +internal class SuccessfulReducer : IReducer +{ + public MainMenuSecondState Reduce(MainMenuSecondState state, MainMenuOpenAction action) + => state with { IsSuccessful = true }; +} diff --git a/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Store/MainMenuSecondState.cs b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Store/MainMenuSecondState.cs new file mode 100644 index 0000000..3636a9d --- /dev/null +++ b/tests/StatPulse.NET.Tests/TestCases/Pulsars/MainMenu/Store/MainMenuSecondState.cs @@ -0,0 +1,8 @@ +using StatePulse.Net; + +namespace StatePulse.NET.Tests.TestCases.Pulsars.MainMenu.Store; + +public sealed record MainMenuSecondState : IStateFeature +{ + public bool IsSuccessful { get; set; } +}