Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execute synchronous tasks synchronously, await asynchronous results asychronously (Fixes #76) #77

Merged
merged 4 commits into from
Aug 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Docs/releases.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
65 changes: 65 additions & 0 deletions Source/Fluxor-NoTutorials.sln
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<Version>3.5.0</Version>
<Version>3.6.0</Version>
<Authors>Peter Morris</Authors>
<Company />
<Product>ReduxDevTools for Fluxor Blazor (Web)</Product>
Expand All @@ -17,8 +17,8 @@
<RepositoryType>git</RepositoryType>
<PackageTags>Redux Flux DotNet CSharp Blazor RazorComponents ReduxDevTools</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>3.5.0.0</AssemblyVersion>
<FileVersion>3.5.0.0</FileVersion>
<AssemblyVersion>3.6.0.0</AssemblyVersion>
<FileVersion>3.6.0.0</FileVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>MrPMorris.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
Expand Down
6 changes: 3 additions & 3 deletions Source/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<RazorLangVersion>3.0</RazorLangVersion>
<Authors>Peter Morris</Authors>
<Company />
<Version>3.5.0</Version>
<Version>3.6.0</Version>
<Description>A zero boilerplate Redux/Flux framework for Blazor</Description>
<Copyright>Peter Morris</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand All @@ -17,8 +17,8 @@
<PackageTags>Redux Flux DotNet CSharp Blazor RazorComponents</PackageTags>
<Product>Fluxor for Blazor (Web)</Product>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>3.5.0.0</AssemblyVersion>
<FileVersion>3.5.0.0</FileVersion>
<AssemblyVersion>3.6.0.0</AssemblyVersion>
<FileVersion>3.6.0.0</FileVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>MrPMorris.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,11 +10,9 @@ namespace Fluxor.Blazor.Web.Middlewares.Routing
/// </summary>
internal class RoutingMiddleware : Middleware
{
private readonly TimeSpan LoopRedirectDetectionWindow;
private readonly NavigationManager NavigationManager;
private readonly IFeature<RoutingState> Feature;
private IStore Store;
private (string Url, DateTime NavigationTime)[] PreviousNavigations;

/// <summary>
/// Creates a new instance of the routing middleware
Expand All @@ -27,8 +23,6 @@ public RoutingMiddleware(NavigationManager navigationManager, IFeature<RoutingSt
{
NavigationManager = navigationManager;
Feature = feature;
LoopRedirectDetectionWindow = TimeSpan.FromMilliseconds(100);
PreviousNavigations = Array.Empty<(string Url, DateTime NavigationTime)>();
NavigationManager.LocationChanged += LocationChanged;
}

Expand All @@ -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);
}
}
}
6 changes: 3 additions & 3 deletions Source/Fluxor/Fluxor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.1;netcoreapp3.1</TargetFrameworks>
<Version>3.5.0</Version>
<Version>3.6.0</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Peter Morris</Authors>
<Company />
Expand All @@ -16,8 +16,8 @@
<PackageLicenseFile></PackageLicenseFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryType>git</RepositoryType>
<AssemblyVersion>3.5.0.0</AssemblyVersion>
<FileVersion>3.5.0.0</FileVersion>
<AssemblyVersion>3.6.0.0</AssemblyVersion>
<FileVersion>3.6.0.0</FileVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>MrPMorris.snk</AssemblyOriginatorKeyFile>
<DelaySign>false</DelaySign>
Expand Down
47 changes: 33 additions & 14 deletions Source/Fluxor/Store.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Fluxor.Exceptions;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -184,29 +183,49 @@ private void EndMiddlewareChange(IDisposable[] disposables)

private void TriggerEffects(object action)
{
Task.Run(async() =>
var recordedExceptions = new List<Exception>();
var effectsToExecute = Effects.Where(x => x.ShouldReactToAction(action));
var executedEffects = new List<Task>();

Action<Exception> collectExceptions = e =>
{
IEnumerable<Exception> exceptions = Array.Empty<Exception>();
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));
});
}
Expand Down
28 changes: 28 additions & 0 deletions Tests/Fluxor.UnitTests/StoreTests/Dispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,33 @@ public async Task WhenCalled_ThenTriggersOnlyEffectsThatHandleTheDispatchedActio
mockIncompatibleEffect.Verify(x => x.HandleAsync(action, It.IsAny<IDispatcher>()), Times.Never);
mockCompatibleEffect.Verify(x => x.HandleAsync(action, It.IsAny<IDispatcher>()), Times.Once);
}

[Fact]
public async Task WhenSynchronousEffectThrowsException_ThenStillExecutesSubsequentEffects()
{
var subject = new TestStore();
var action = new object();

var mockSynchronousEffectThatThrows = new Mock<IEffect>();
mockSynchronousEffectThatThrows
.Setup(x => x.ShouldReactToAction(action))
.Returns(true);
mockSynchronousEffectThatThrows
.Setup(x => x.HandleAsync(action, subject))
.ThrowsAsync(new NotImplementedException());

var mockEffectThatFollows = new Mock<IEffect>();
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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class EffectThatThrowsSimpleException : Effect<ThrowSimpleExceptionAction
{
protected override async Task HandleAsync(ThrowSimpleExceptionAction action, IDispatcher dispatcher)
{
await Task.Delay(10).ConfigureAwait(false);
await Task.Delay(100);
try
{
throw new InvalidOperationException("This is a simple exception");
Expand Down