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

StateInitializationPreProcessor #459

Merged
merged 18 commits into from
Jul 31, 2024
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
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
@inherits BaseComponent

@code
{
private async Task GoBack()
{
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" [email protected]>
<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
Loading