Skip to content

Commit

Permalink
Merge pull request #459 from TimeWarpEngineering/Cramer/2024-07-25/Next
Browse files Browse the repository at this point in the history
StateInitializationPreProcessor
  • Loading branch information
StevenTCramer authored Jul 31, 2024
2 parents eec844c + 61b3cac commit a055018
Show file tree
Hide file tree
Showing 21 changed files with 386 additions and 114 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<!-- Set common properties regarding assembly information and nuget packages -->
<PropertyGroup>
<TimeWarpStateVersion>11.0.0-beta.74+8.0.303</TimeWarpStateVersion>
<TimeWarpStateVersion>11.0.0-beta.75+8.0.303</TimeWarpStateVersion>
<Authors>Steven T. Cramer</Authors>
<Product>TimeWarp State</Product>
<PackageVersion>$(TimeWarpStateVersion)</PackageVersion>
Expand Down
14 changes: 13 additions & 1 deletion Source/TimeWarp.State.Plus/EventIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@

internal class EventIds
{
// Features - ActionTracking
// Feature - ActionTracking
// Pipeline - Middleware
public static readonly EventId ActionTrackingBehavior_StartTracking = new(1000, nameof(ActionTrackingBehavior_StartTracking));
public static readonly EventId ActionTrackingBehavior_StartProcessing = new(1001, nameof(ActionTrackingBehavior_StartProcessing));
public static readonly EventId ActionTrackingBehavior_CompletedProcessing = new(1002, nameof(ActionTrackingBehavior_CompletedProcessing));
public static readonly EventId ActionTrackingBehavior_CompletedTracking = new(1003, nameof(ActionTrackingBehavior_CompletedTracking));

// Feature - PersistentStatePostProcessor
public static readonly EventId PersistentStatePostProcessor_StartProcessing = new(1100, nameof(PersistentStatePostProcessor_StartProcessing));
public static readonly EventId PersistentStatePostProcessor_SaveToSessionStorage = new(1101, nameof(PersistentStatePostProcessor_SaveToSessionStorage));
public static readonly EventId PersistentStatePostProcessor_SaveToLocalStorage = new(1102, nameof(PersistentStatePostProcessor_SaveToLocalStorage));

// Feature - StateInitializedNotificationHandler
public static readonly EventId StateInitializedNotificationHandler_Handling = new(1200, nameof(StateInitializedNotificationHandler_Handling));

// Feature - PersistenceService
public static readonly EventId PersistenceService_LoadState = new(1300, nameof(PersistenceService_LoadState));
public static readonly EventId PersistenceService_LoadState_SerializedState = new(1301, nameof(PersistenceService_LoadState_SerializedState));
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task Process(TRequest request, TResponse response, CancellationToke

if (persistentStateAttribute is null) return;

Logger.LogDebug("PersistentStatePostProcessor: {FullName}", typeof(TRequest).FullName);
Logger.LogTrace(EventIds.PersistentStatePostProcessor_StartProcessing, "Start Processing: {FullName}", typeof(TRequest).FullName);

object state = Store.GetState(currentType);

Expand All @@ -43,11 +43,23 @@ public async Task Process(TRequest request, TResponse response, CancellationToke
// TODO:
break;
case PersistentStateMethod.SessionStorage:
Logger.LogDebug("Save to Session Storage {StateTypeName}", currentType.Name);
Logger.LogTrace
(
EventIds.PersistentStatePostProcessor_SaveToSessionStorage
,"Save {StateTypeName} to Session Storage with value {json}"
, currentType.Name
, JsonSerializer.Serialize(state)
);
await SessionStorageService.SetItemAsync(currentType.Name, state, cancellationToken);
break;
case PersistentStateMethod.LocalStorage:
Logger.LogDebug("Save to Local Storage {StateTypeName}", currentType.Name);
Logger.LogTrace
(
EventIds.PersistentStatePostProcessor_SaveToLocalStorage
,"Save {StateTypeName} to Local Storage with value {json}"
, currentType.Name
, JsonSerializer.Serialize(state)
);
await LocalSessionStorageService.SetItemAsync(currentType.Name, state, cancellationToken);
break;
case PersistentStateMethod.PreRender:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,30 @@ ILogger<PersistenceService> logger
}
public async Task<object?> LoadState(Type stateType, PersistentStateMethod persistentStateMethod)
{
string typeName =
stateType.Name ??
string typeName =
stateType.Name ??
throw new InvalidOperationException("The type provided has a null full name, which is not supported for persistence operations.");
Logger.LogInformation("PersistenceService.LoadState Loading State for {stateType}", stateType);

Logger.LogInformation(EventIds.PersistenceService_LoadState, "Loading State for {stateType}", stateType);

string? serializedState = persistentStateMethod switch
{
PersistentStateMethod.SessionStorage => await SessionStorageService.GetItemAsStringAsync(typeName),
PersistentStateMethod.LocalStorage => await LocalStorageService.GetItemAsStringAsync(typeName),
PersistentStateMethod.PreRender => null,// TODO
PersistentStateMethod.Server => null,// TODO
PersistentStateMethod.PreRender => null, // TODO
PersistentStateMethod.Server => null, // TODO
_ => null
};

object? result =
serializedState != null ?
JsonSerializer.Deserialize(serializedState, stateType, JsonSerializerOptions) :
null;
Logger.LogTrace
(
EventIds.PersistenceService_LoadState_SerializedState,
"Serialized State: {serializedState}",
serializedState
);

object? result =
serializedState != null ? JsonSerializer.Deserialize(serializedState, stateType, JsonSerializerOptions) : null;

if (result is IState state)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async Task Handle(StateInitializedNotification stateInitializedNotificati
string assemblyQualifiedName = stateInitializedNotification.StateType.AssemblyQualifiedName ?? throw new InvalidOperationException();

string typeName = assemblyQualifiedName.Replace(fullName, $"{fullName}+Load+Action");
Logger.LogDebug("StateInitializedNotificationHandler: {StateTypeName}", stateInitializedNotification.StateType.Name);
Logger.LogDebug(EventIds.StateInitializedNotificationHandler_Handling, "StateInitializedNotificationHandler: {StateTypeName}", stateInitializedNotification.StateType.Name);
var actionType = Type.GetType(typeName);

if (actionType != null)
Expand Down
1 change: 1 addition & 0 deletions Source/TimeWarp.State.Plus/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
global using Microsoft.JSInterop;
global using System.Reflection;
global using System.Text.Json;
global using TimeWarp.State.Plus;
Original file line number Diff line number Diff line change
Expand Up @@ -46,41 +46,52 @@ private static string GenerateLoadClassCode(string namespaceName, string classNa
{
string camelCaseClassName = ToCamelCase(className);

return $$"""
return $$$"""
#nullable enable

#pragma warning disable CS1591
namespace {{namespaceName}};
namespace {{{namespaceName}}};

using TimeWarp.Features.Persistence;
using TimeWarp.State;

public partial class {{className}}
public partial class {{{className}}}
{
public static class Load
{
public class Action : IAction { }
public static class Load
{
public class Action : IAction;

public class Handler
(
IStore store,
IPersistenceService PersistenceService
): ActionHandler<Action>(store)
public class Handler : ActionHandler<Action>
{
private readonly IPersistenceService PersistenceService;
private readonly ILogger<Handler> Logger;

public Handler
(
IStore store,
IPersistenceService persistenceService,
ILogger<Handler> logger
) : base(store)
{
PersistenceService = persistenceService;
Logger = logger;
}
public override async System.Threading.Tasks.Task Handle(Action action, System.Threading.CancellationToken cancellationToken)
{
try
{
public override async System.Threading.Tasks.Task Handle(Action action, System.Threading.CancellationToken cancellationToken)
{
try
{
object? state = await PersistenceService.LoadState(typeof({{className}}), PersistentStateMethod.{{persistentStateMethod}});
if (state is {{className}} {{camelCaseClassName}}) Store.SetState({{camelCaseClassName}});
}
catch (Exception)
{
// if this is a JavaScript not available exception then we are prerendering and just swallow it
}
}
object? state = await PersistenceService.LoadState(typeof({{{className}}}), PersistentStateMethod.{{{persistentStateMethod}}});
if (state is {{{className}}} {{{camelCaseClassName}}}) Store.SetState({{{camelCaseClassName}}});
else Logger.LogTrace("{{{className}}} is null");
}
catch (Exception exception)
{
Logger.LogError(exception, "Error loading {{{className}}}");
// if this is a JavaScript not available exception then we are prerendering and just swallow it
}
}
}
}
}
#pragma warning restore CS1591

Expand Down
5 changes: 4 additions & 1 deletion Source/TimeWarp.State/EventIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ internal class EventIds
public static readonly EventId Subscriptions_RemovingComponentSubscriptions = new(302, nameof(Subscriptions_RemovingComponentSubscriptions));
public static readonly EventId Subscriptions_ReRenderingSubscribers = new(303, nameof(Subscriptions_ReRenderingSubscribers));
public static readonly EventId Subscriptions_RemoveSubscription = new(304, nameof(Subscriptions_RemoveSubscription));


// Routing
// Pipeline - Middleware
Expand Down Expand Up @@ -72,4 +71,8 @@ internal class EventIds
public static readonly EventId TimeWarpStateComponent_ShouldReRender = new(701, nameof(TimeWarpStateComponent_ShouldReRender));
public static readonly EventId TimeWarpStateComponent_Disposing = new(702, nameof(TimeWarpStateComponent_Disposing));
public static readonly EventId TimeWarpStateComponent_RenderCount = new(702, nameof(TimeWarpStateComponent_RenderCount));

// StateInitializationPreProcessor
public static readonly EventId StateInitializationPreProcessor_Waiting = new(800, nameof(StateInitializationPreProcessor_Waiting));
public static readonly EventId StateInitializationPreProcessor_Completed = new(801, nameof(StateInitializationPreProcessor_Completed));
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ public static IServiceCollection AddTimeWarpState
EnsureHttpClient(serviceCollection);
EnsureStates(serviceCollection, timeWarpStateOptions);
EnsureMediator(serviceCollection, timeWarpStateOptions);


serviceCollection.AddTransient(typeof(IRequestPreProcessor<>), typeof(StateInitializationPreProcessor<>));

if (timeWarpStateOptions.UseStateTransactionBehavior)
{
serviceCollection.AddScoped(typeof(IPipelineBehavior<,>), typeof(StateTransactionBehavior<,>));
}


return serviceCollection;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace TimeWarp.State;

Check warning on line 1 in Source/TimeWarp.State/Features/StateInitialization/StateInitializationPreProcessor.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Namespace does not correspond to file location

Namespace does not correspond to file location, must be: 'TimeWarp.State.Features.StateInitialization'

public class StateInitializationPreProcessor<TRequest> : IRequestPreProcessor<TRequest> where TRequest : IAction
{
private readonly IStore Store;
private readonly ILogger<StateInitializationPreProcessor<TRequest>> Logger;

public StateInitializationPreProcessor(IStore store, ILogger<StateInitializationPreProcessor<TRequest>> logger)
{
Store = store;
Logger = logger;
}

public async Task Process(TRequest request, CancellationToken cancellationToken)
{
string typeName = typeof(TRequest).GetEnclosingStateType().FullName ?? throw new InvalidOperationException();

// Wait for the state initialization to complete before processing the action
if (Store.StateInitializationTasks.TryGetValue(typeName, out Task? initializationTask))
{
try
{
Logger.LogTrace
(
EventIds.StateInitializationPreProcessor_Waiting,
"Waiting for state initialization to complete. State type: {StateType}",
typeName
);

await initializationTask;

Logger.LogTrace
(
EventIds.StateInitializationPreProcessor_Completed,
"State initialization completed. State type: {StateType}",
typeName
);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error occurred while waiting for state initialization.");
throw;
}
}
}
}
2 changes: 2 additions & 0 deletions Source/TimeWarp.State/Store/IStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public interface IStore
void RemoveState<TState>() where TState : IState;

void Reset();

ConcurrentDictionary<string, Task> StateInitializationTasks { get; }
}
39 changes: 23 additions & 16 deletions Source/TimeWarp.State/Store/Store.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal partial class Store : IStore
/// </summary>
/// <remarks>Useful when logging </remarks>
public Guid Guid { get; } = Guid.NewGuid();
public ConcurrentDictionary<string, Task> StateInitializationTasks { get; } = new();

public Store
(
Expand Down Expand Up @@ -103,38 +104,44 @@ public object GetState(Type stateType)
if (!States.TryGetValue(typeName, out IState? state))
{
Logger.LogDebug(EventIds.Store_CreateState, "Creating State of type: {typeName}", typeName);

// will use default constructor if none exists
state = (IState)ServiceProvider.GetRequiredService(stateType);

// we need to set the sender if the default constructor was used
state.Sender = ServiceProvider.GetRequiredService<ISender>();

state.Initialize();
if (!States.TryAdd(typeName, state))
{
throw new InvalidOperationException($"An element with the key '{typeName}' already exists in the States dictionary.");
}

// Publish the state initialization notification asynchronously
Task initializationTask = Publisher.Publish(new StateInitializedNotification(stateType))
.ContinueWith
(
t =>
{
if (t.Exception != null)
{
Logger.LogError(t.Exception, "Error occurred while publishing state initialization notification.");
}
},
TaskScheduler.Default
);

// Fire-and-forget publishing the state initialization notification with exception handling
Task.Run(async () =>
{
try
{
await Publisher.Publish(new StateInitializedNotification(stateType));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error occurred while publishing state initialization notification.");
}
});
StateInitializationTasks[typeName] = initializationTask;
}
else
{
Logger.LogDebug(EventIds.Store_GetState, "State of type ({typeName}) exists with Guid: {state_Guid}", typeName, state.Guid);
}

return state;
}
}

public object? GetPreviousState(Type stateType)
{
string typeName = stateType.FullName ?? throw new InvalidOperationException();
Expand Down
27 changes: 20 additions & 7 deletions Tests/Test.App/Test.App.Client/Components/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
@using Test.App.Client.Pages

Check warning on line 2 in Tests/Test.App/Test.App.Client/Components/NavMenu.razor

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Redundant using directive

Using directive is not required by the code and can be safely removed
@inherits BaseComponent

@code
{
private async Task GoBack()

Check warning on line 7 in Tests/Test.App/Test.App.Client/Components/NavMenu.razor

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Type member is never used (private accessibility)

Method 'GoBack' is never used
{
await RouteState.GoBack();
}
}


<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid" title=@RenderModeDisplayString>
<a class="navbar-brand" href="">Test.App.Server</a>
Expand Down Expand Up @@ -86,6 +95,12 @@
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> @PersistenceTestPage.Title
</NavLink>
</div>

<div class="nav-item px-3">
<NavLink class="nav-link" href=@ServerSidePersistenceTestPage.Route>
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> @ServerSidePersistenceTestPage.Title
</NavLink>
</div>

<div class="nav-section-divider">
<hr class="nav-divider">
Expand Down Expand Up @@ -150,10 +165,8 @@
</nav>
</div>

@code
{
private async Task GoBack()
{
await RouteState.GoBack();
}
}
<style>
.nav-link {
font-size: 0.750rem; /* Adjust the font size as needed */
}
</style>
Loading

0 comments on commit a055018

Please sign in to comment.