diff --git a/Docs/releases.md b/Docs/releases.md index a05568a2..426fd415 100644 --- a/Docs/releases.md +++ b/Docs/releases.md @@ -1,7 +1,12 @@ # Releases +### New in 3.6 + * Ensure synchronous effects are executed synchronously ([#75](https://github.com/mrpmorris/fluxor/issues/75)) - + Reverts changes for [(#74) Endless loop redirects](https://github.com/mrpmorris/Fluxor/issues/74) as + these no longer occur. + ### New in 3.5 - * Bug fix for ([#74](https://github.com/mrpmorris/bfluxor/issues/74)) - Detect endless loop redirects + * Bug fix for ([#74](https://github.com/mrpmorris/Fluxor/issues/74)) - Handle endless loop redirects caused by Routing middleware. ### New in 3.4 * **Breaking change**: `FluxorException` is now an `abstract` class. diff --git a/Source/Fluxor-NoTutorials.sln b/Source/Fluxor-NoTutorials.sln new file mode 100644 index 00000000..38995848 --- /dev/null +++ b/Source/Fluxor-NoTutorials.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28809.33 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fluxor", "Fluxor\Fluxor.csproj", "{863909D3-7E81-4240-8C0A-6F57768D28FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fluxor.Blazor.Web", "Fluxor.Blazor.Web\Fluxor.Blazor.Web.csproj", "{6289FEA3-4F01-4100-BB4B-6F8938203886}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fluxor.Blazor.Web.ReduxDevTools", "Fluxor.Blazor.Web.ReduxDevTools\Fluxor.Blazor.Web.ReduxDevTools.csproj", "{E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|iPhone = Debug|iPhone + Debug|iPhoneSimulator = Debug|iPhoneSimulator + Release|Any CPU = Release|Any CPU + Release|iPhone = Release|iPhone + Release|iPhoneSimulator = Release|iPhoneSimulator + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Debug|iPhone.Build.0 = Debug|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Release|Any CPU.Build.0 = Release|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Release|iPhone.ActiveCfg = Release|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Release|iPhone.Build.0 = Release|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {863909D3-7E81-4240-8C0A-6F57768D28FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Debug|iPhone.Build.0 = Debug|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Release|Any CPU.Build.0 = Release|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Release|iPhone.ActiveCfg = Release|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Release|iPhone.Build.0 = Release|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {6289FEA3-4F01-4100-BB4B-6F8938203886}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Debug|iPhone.Build.0 = Debug|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Release|Any CPU.Build.0 = Release|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Release|iPhone.ActiveCfg = Release|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Release|iPhone.Build.0 = Release|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E33D5229-FFD7-410B-99E4-5FA2E02CCAFE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1F2E0DF-C651-48A3-83E3-3B77D34EC3A2} + EndGlobalSection +EndGlobal diff --git a/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj b/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj index cecbc20d..569d2e7e 100644 --- a/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj +++ b/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj @@ -3,7 +3,7 @@ netstandard2.1 3.0 - 3.5.0 + 3.6.0 Peter Morris ReduxDevTools for Fluxor Blazor (Web) @@ -17,8 +17,8 @@ git Redux Flux DotNet CSharp Blazor RazorComponents ReduxDevTools true - 3.5.0.0 - 3.5.0.0 + 3.6.0.0 + 3.6.0.0 true MrPMorris.snk diff --git a/Source/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj b/Source/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj index 57101297..cce43bc9 100644 --- a/Source/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj +++ b/Source/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj @@ -5,7 +5,7 @@ 3.0 Peter Morris - 3.5.0 + 3.6.0 A zero boilerplate Redux/Flux framework for Blazor Peter Morris MIT @@ -17,8 +17,8 @@ Redux Flux DotNet CSharp Blazor RazorComponents Fluxor for Blazor (Web) true - 3.5.0.0 - 3.5.0.0 + 3.6.0.0 + 3.6.0.0 true MrPMorris.snk diff --git a/Source/Fluxor.Blazor.Web/Middlewares/Routing/RoutingMiddleware.cs b/Source/Fluxor.Blazor.Web/Middlewares/Routing/RoutingMiddleware.cs index f528ecff..646e6613 100644 --- a/Source/Fluxor.Blazor.Web/Middlewares/Routing/RoutingMiddleware.cs +++ b/Source/Fluxor.Blazor.Web/Middlewares/Routing/RoutingMiddleware.cs @@ -1,7 +1,5 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; -using System; -using System.Linq; using System.Threading.Tasks; namespace Fluxor.Blazor.Web.Middlewares.Routing @@ -12,11 +10,9 @@ namespace Fluxor.Blazor.Web.Middlewares.Routing /// internal class RoutingMiddleware : Middleware { - private readonly TimeSpan LoopRedirectDetectionWindow; private readonly NavigationManager NavigationManager; private readonly IFeature Feature; private IStore Store; - private (string Url, DateTime NavigationTime)[] PreviousNavigations; /// /// Creates a new instance of the routing middleware @@ -27,8 +23,6 @@ public RoutingMiddleware(NavigationManager navigationManager, IFeature(); NavigationManager.LocationChanged += LocationChanged; } @@ -50,28 +44,8 @@ protected override void OnInternalMiddlewareChangeEnding() private void LocationChanged(object sender, LocationChangedEventArgs e) { - if (Store != null - && !IsInsideMiddlewareChange - && e.Location != Feature.State.Uri - && !LoopedRedirectDetected(e)) - { + if (Store != null && !IsInsideMiddlewareChange && e.Location != Feature.State.Uri) Store.Dispatch(new GoAction(e.Location)); - } - } - - private bool LoopedRedirectDetected(LocationChangedEventArgs e) - { - if (e.IsNavigationIntercepted) - return false; - - DateTime cutoffTime = DateTime.UtcNow.Subtract(LoopRedirectDetectionWindow); - PreviousNavigations = - PreviousNavigations - .Where(x => x.NavigationTime >= cutoffTime) - .Append((e.Location, DateTime.UtcNow)) - .ToArray(); - - return (PreviousNavigations.Count(x => x.Url == e.Location) >= 2); } } } diff --git a/Source/Fluxor/Fluxor.csproj b/Source/Fluxor/Fluxor.csproj index 95569362..87fe4a81 100644 --- a/Source/Fluxor/Fluxor.csproj +++ b/Source/Fluxor/Fluxor.csproj @@ -2,7 +2,7 @@ netstandard2.1;netcoreapp3.1 - 3.5.0 + 3.6.0 true Peter Morris @@ -16,8 +16,8 @@ MIT git - 3.5.0.0 - 3.5.0.0 + 3.6.0.0 + 3.6.0.0 true MrPMorris.snk false diff --git a/Source/Fluxor/Store.cs b/Source/Fluxor/Store.cs index 6b249e8c..f3a3a0de 100644 --- a/Source/Fluxor/Store.cs +++ b/Source/Fluxor/Store.cs @@ -1,5 +1,4 @@ -using Fluxor.Exceptions; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -184,29 +183,49 @@ private void EndMiddlewareChange(IDisposable[] disposables) private void TriggerEffects(object action) { - Task.Run(async() => + var recordedExceptions = new List(); + var effectsToExecute = Effects.Where(x => x.ShouldReactToAction(action)); + var executedEffects = new List(); + + Action collectExceptions = e => { - IEnumerable exceptions = Array.Empty(); + if (e is AggregateException aggregateException) + recordedExceptions.AddRange(aggregateException.Flatten().InnerExceptions); + else + recordedExceptions.Add(e); + }; + // Execute all tasks. Some will execute synchronously and complete immediately, + // so we need to catch their exceptions in the loop so they don't prevent + // other effects from executing. + // It's then up to the UI to decide if any of those exceptions should cause + // the app to terminate or not. + foreach (IEffect effect in effectsToExecute) + { try { - var triggeredEffects = Effects - .Where(x => x.ShouldReactToAction(action)) - .Select(x => x.HandleAsync(action, this)) - .ToArray(); - - await Task.WhenAll(triggeredEffects); + executedEffects.Add(effect.HandleAsync(action, this)); } - catch (AggregateException e) + catch (Exception e) + { + collectExceptions(e); + } + } + + Task.Run(async() => + { + try { - exceptions = e.Flatten().InnerExceptions; + await Task.WhenAll(executedEffects); } catch (Exception e) { - exceptions = new Exception[] { e }; + collectExceptions(e); } - foreach (Exception exception in exceptions) + // Let the UI decide if it wishes to deal with any unhandled exceptions. + // By default it should throw the exception if it is not handled. + foreach (Exception exception in recordedExceptions) UnhandledException?.Invoke(this, new Exceptions.UnhandledExceptionEventArgs(exception)); }); } diff --git a/Tests/Fluxor.UnitTests/StoreTests/Dispatch.cs b/Tests/Fluxor.UnitTests/StoreTests/Dispatch.cs index 7c84c3a4..45ac1c04 100644 --- a/Tests/Fluxor.UnitTests/StoreTests/Dispatch.cs +++ b/Tests/Fluxor.UnitTests/StoreTests/Dispatch.cs @@ -125,5 +125,33 @@ public async Task WhenCalled_ThenTriggersOnlyEffectsThatHandleTheDispatchedActio mockIncompatibleEffect.Verify(x => x.HandleAsync(action, It.IsAny()), Times.Never); mockCompatibleEffect.Verify(x => x.HandleAsync(action, It.IsAny()), Times.Once); } + + [Fact] + public async Task WhenSynchronousEffectThrowsException_ThenStillExecutesSubsequentEffects() + { + var subject = new TestStore(); + var action = new object(); + + var mockSynchronousEffectThatThrows = new Mock(); + mockSynchronousEffectThatThrows + .Setup(x => x.ShouldReactToAction(action)) + .Returns(true); + mockSynchronousEffectThatThrows + .Setup(x => x.HandleAsync(action, subject)) + .ThrowsAsync(new NotImplementedException()); + + var mockEffectThatFollows = new Mock(); + mockEffectThatFollows + .Setup(x => x.ShouldReactToAction(action)) + .Returns(true); + + await subject.InitializeAsync(); + subject.AddEffect(mockSynchronousEffectThatThrows.Object); + subject.AddEffect(mockEffectThatFollows.Object); + subject.Dispatch(action); + + mockSynchronousEffectThatThrows.Verify(x => x.HandleAsync(action, subject)); + mockEffectThatFollows.Verify(x => x.HandleAsync(action, subject)); + } } } \ No newline at end of file diff --git a/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsAggregateException.cs b/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsAggregateException.cs index 58ff4eee..8e086d46 100644 --- a/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsAggregateException.cs +++ b/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsAggregateException.cs @@ -11,7 +11,7 @@ protected override async Task HandleAsync(ThrowAggregateExceptionAction action, var exception2 = new InvalidCastException("Second embedded exception"); var exception3 = new InvalidProgramException("Third embedded exception"); - await Task.Delay(10).ConfigureAwait(false); + await Task.Delay(100); try { throw new AggregateException( diff --git a/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsSimpleException.cs b/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsSimpleException.cs index 4bfb22e4..a72a898e 100644 --- a/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsSimpleException.cs +++ b/Tests/Fluxor.UnitTests/SupportFiles/EffectThatThrowsSimpleException.cs @@ -7,7 +7,7 @@ public class EffectThatThrowsSimpleException : Effect