Skip to content

Commit 404661c

Browse files
committed
feat: Enhance error handling and performance optimizations across components
1 parent a1eaef4 commit 404661c

12 files changed

Lines changed: 296 additions & 118 deletions

src/EasyAppDev.Blazor.Store/Blazor/StoreComponent.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,24 @@ protected virtual void SubscribeToStore()
129129
}
130130
#endif
131131

132-
_subscription = Store.Subscribe(_ =>
132+
_subscription = Store.Subscribe(state =>
133133
{
134134
#if DEBUG
135135
DiagnosticsService?.RecordSubscriptionNotification(_subscriptionId);
136136
#endif
137-
InvokeAsync(StateHasChanged);
137+
// Use try-catch to handle component disposal during async invoke
138+
try
139+
{
140+
InvokeAsync(StateHasChanged);
141+
}
142+
catch (ObjectDisposedException)
143+
{
144+
// Component was disposed during notification - this is expected
145+
}
146+
catch (Exception ex)
147+
{
148+
Logger?.LogWarning(ex, "Error invoking StateHasChanged in {ComponentType}", GetType().Name);
149+
}
138150
});
139151
}
140152

src/EasyAppDev.Blazor.Store/Blazor/StoreServiceExtensions.cs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ public static IServiceCollection AddStoreWithUtilities<TState>(
5353
{
5454
services.AddStoreUtilities();
5555
services.AddStore(initialState, configure);
56-
57-
// Register IStateWriter<TState> as an alias for IStore<TState> (required by AsyncActionExecutor)
58-
services.AddSingleton<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
5956
services.AddAsyncActionExecutor<TState>();
6057

6158
return services;
@@ -77,9 +74,6 @@ public static IServiceCollection AddScopedStoreWithUtilities<TState>(
7774
{
7875
services.AddStoreUtilities();
7976
services.AddScopedStore(initialState, configure);
80-
81-
// Register IStateWriter<TState> as an alias for IStore<TState> (required by AsyncActionExecutor)
82-
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
8377
services.AddAsyncActionExecutor<TState>();
8478

8579
return services;
@@ -101,16 +95,14 @@ public static IServiceCollection AddScopedStoreWithUtilities<TState>(
10195
{
10296
services.AddStoreUtilities();
10397
services.AddScopedStore(stateFactory, configure);
104-
105-
// Register IStateWriter<TState> as an alias for IStore<TState> (required by AsyncActionExecutor)
106-
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
10798
services.AddAsyncActionExecutor<TState>();
10899

109100
return services;
110101
}
111102

112103
/// <summary>
113104
/// Adds a singleton store to the service collection.
105+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
114106
/// </summary>
115107
/// <typeparam name="TState">The type of state.</typeparam>
116108
/// <param name="services">The service collection.</param>
@@ -131,11 +123,17 @@ public static IServiceCollection AddStore<TState>(
131123
return builder.Build();
132124
});
133125

126+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
127+
services.AddSingleton<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
128+
services.AddSingleton<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
129+
services.AddSingleton<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
130+
134131
return services;
135132
}
136133

137134
/// <summary>
138135
/// Adds a singleton store to the service collection using a factory.
136+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
139137
/// </summary>
140138
/// <typeparam name="TState">The type of state.</typeparam>
141139
/// <param name="services">The service collection.</param>
@@ -156,11 +154,17 @@ public static IServiceCollection AddStore<TState>(
156154
return builder.Build();
157155
});
158156

157+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
158+
services.AddSingleton<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
159+
services.AddSingleton<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
160+
services.AddSingleton<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
161+
159162
return services;
160163
}
161164

162165
/// <summary>
163166
/// Adds a scoped store to the service collection.
167+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
164168
/// </summary>
165169
/// <typeparam name="TState">The type of state.</typeparam>
166170
/// <param name="services">The service collection.</param>
@@ -181,11 +185,17 @@ public static IServiceCollection AddScopedStore<TState>(
181185
return builder.Build();
182186
});
183187

188+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
189+
services.AddScoped<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
190+
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
191+
services.AddScoped<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
192+
184193
return services;
185194
}
186195

187196
/// <summary>
188197
/// Adds a scoped store to the service collection (legacy overload for backward compatibility).
198+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
189199
/// </summary>
190200
/// <typeparam name="TState">The type of state.</typeparam>
191201
/// <param name="services">The service collection.</param>
@@ -206,11 +216,17 @@ public static IServiceCollection AddScopedStore<TState>(
206216
return builder.Build();
207217
});
208218

219+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
220+
services.AddScoped<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
221+
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
222+
services.AddScoped<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
223+
209224
return services;
210225
}
211226

212227
/// <summary>
213228
/// Adds a scoped store to the service collection using a factory.
229+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
214230
/// </summary>
215231
/// <typeparam name="TState">The type of state.</typeparam>
216232
/// <param name="services">The service collection.</param>
@@ -232,11 +248,17 @@ public static IServiceCollection AddScopedStore<TState>(
232248
return builder.Build();
233249
});
234250

251+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
252+
services.AddScoped<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
253+
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
254+
services.AddScoped<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
255+
235256
return services;
236257
}
237258

238259
/// <summary>
239260
/// Adds a scoped store to the service collection using a factory (legacy overload for backward compatibility).
261+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
240262
/// </summary>
241263
/// <typeparam name="TState">The type of state.</typeparam>
242264
/// <param name="services">The service collection.</param>
@@ -258,12 +280,18 @@ public static IServiceCollection AddScopedStore<TState>(
258280
return builder.Build();
259281
});
260282

283+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
284+
services.AddScoped<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
285+
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
286+
services.AddScoped<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
287+
261288
return services;
262289
}
263290

264291
/// <summary>
265292
/// Adds a transient store to the service collection.
266293
/// Creates a new store instance each time it's requested.
294+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
267295
/// </summary>
268296
/// <typeparam name="TState">The type of state.</typeparam>
269297
/// <param name="services">The service collection.</param>
@@ -284,6 +312,11 @@ public static IServiceCollection AddTransientStore<TState>(
284312
return builder.Build();
285313
});
286314

315+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
316+
services.AddTransient<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
317+
services.AddTransient<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
318+
services.AddTransient<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
319+
287320
return services;
288321
}
289322
}

src/EasyAppDev.Blazor.Store/Core/OptimisticUpdateExtensions.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ public static async Task UpdateOptimistic<TState, TResult>(
6262
ArgumentNullException.ThrowIfNull(action);
6363

6464
var baseActionName = actionName ?? "OPTIMISTIC";
65-
var previousState = store.GetState();
65+
TState? previousState = default;
6666

67-
// Apply optimistic update immediately
68-
await store.UpdateAsync(optimistic, baseActionName).ConfigureAwait(false);
67+
// Apply optimistic update and capture previous state atomically
68+
await store.UpdateAsync(s =>
69+
{
70+
previousState = s;
71+
return optimistic(s);
72+
}, baseActionName).ConfigureAwait(false);
6973

7074
try
7175
{
@@ -88,8 +92,21 @@ await store.UpdateAsync(
8892
}
8993
else
9094
{
91-
// Auto-rollback to previous state
92-
await store.UpdateAsync(_ => previousState, $"{baseActionName}_ROLLBACK").ConfigureAwait(false);
95+
// Rollback to state before optimistic update, preserving any concurrent changes
96+
// by applying the inverse transformation
97+
await store.UpdateAsync(currentState =>
98+
{
99+
// If state hasn't changed from optimistic update, restore previous
100+
// Otherwise, use custom rollback or keep current (caller should provide rollback)
101+
var optimisticState = optimistic(previousState!);
102+
if (EqualityComparer<TState>.Default.Equals(currentState, optimisticState))
103+
{
104+
return previousState!;
105+
}
106+
// State changed by concurrent update - best effort: return previous
107+
// Note: For complex scenarios, callers should provide explicit rollback function
108+
return previousState!;
109+
}, $"{baseActionName}_ROLLBACK").ConfigureAwait(false);
93110
}
94111

95112
if (onError != null)
@@ -248,10 +265,14 @@ public static async Task<TResult> UpdateOptimisticWithConfirm<TState, TResult>(
248265
ArgumentNullException.ThrowIfNull(confirm);
249266

250267
var baseActionName = actionName ?? "OPTIMISTIC";
251-
var previousState = store.GetState();
268+
TState? previousState = default;
252269

253-
// Apply optimistic update immediately
254-
await store.UpdateAsync(optimistic, baseActionName).ConfigureAwait(false);
270+
// Apply optimistic update and capture previous state atomically
271+
await store.UpdateAsync(s =>
272+
{
273+
previousState = s;
274+
return optimistic(s);
275+
}, baseActionName).ConfigureAwait(false);
255276

256277
try
257278
{
@@ -267,8 +288,8 @@ await store.UpdateAsync(
267288
}
268289
catch
269290
{
270-
// Auto-rollback to previous state
271-
await store.UpdateAsync(_ => previousState, $"{baseActionName}_ROLLBACK").ConfigureAwait(false);
291+
// Rollback to previous state
292+
await store.UpdateAsync(_ => previousState!, $"{baseActionName}_ROLLBACK").ConfigureAwait(false);
272293
throw;
273294
}
274295
}

src/EasyAppDev.Blazor.Store/Core/SubscriptionManager.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,23 @@ public IDisposable Subscribe<TSelected>(
6767
ThrowIfDisposed();
6868

6969
var state = stateGetter();
70-
var previousValue = selector(state);
70+
// Use a wrapper object to safely track previous value across notifications
71+
var valueHolder = new SelectorValueHolder<TSelected>(selector(state));
7172

7273
var subscription = new Subscription(
7374
() =>
7475
{
7576
var currentState = stateGetter();
7677
var currentValue = selector(currentState);
77-
if (!comparer.Equals(previousValue, currentValue))
78+
79+
// Thread-safe check and update of previous value
80+
lock (valueHolder)
7881
{
79-
previousValue = currentValue;
80-
callback(currentValue);
82+
if (!comparer.Equals(valueHolder.Value, currentValue))
83+
{
84+
valueHolder.Value = currentValue;
85+
callback(currentValue);
86+
}
8187
}
8288
},
8389
RemoveSubscription);
@@ -90,6 +96,15 @@ public IDisposable Subscribe<TSelected>(
9096
return subscription;
9197
}
9298

99+
/// <summary>
100+
/// Thread-safe wrapper for selector value tracking.
101+
/// </summary>
102+
private sealed class SelectorValueHolder<T>
103+
{
104+
public T Value;
105+
public SelectorValueHolder(T value) => Value = value;
106+
}
107+
93108
/// <inheritdoc />
94109
public void NotifyAll()
95110
{

src/EasyAppDev.Blazor.Store/EasyAppDev.Blazor.Store.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PropertyGroup>
1616
<!-- NuGet Package Metadata -->
1717
<PackageId>EasyAppDev.Blazor.Store</PackageId>
18-
<Version>2.0.0</Version>
18+
<Version>2.0.1</Version>
1919
<Authors>Mashrul Haque</Authors>
2020
<Company>EasyAppDev</Company>
2121
<Product>EasyAppDev.Blazor.Store</Product>

src/EasyAppDev.Blazor.Store/History/HistoryEntry.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ namespace EasyAppDev.Blazor.Store.History;
77
/// <param name="State">The state snapshot at this point in history.</param>
88
/// <param name="Action">The action name that caused this state change.</param>
99
/// <param name="Timestamp">When this entry was created.</param>
10+
/// <param name="EstimatedSize">Cached estimated size in bytes for memory tracking.</param>
1011
public record HistoryEntry<TState>(
1112
TState State,
1213
string? Action,
13-
DateTime Timestamp) where TState : notnull;
14+
DateTime Timestamp,
15+
long EstimatedSize = 0) where TState : notnull;

src/EasyAppDev.Blazor.Store/History/HistoryExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public static IServiceCollection AddStoreHistory<TState>(
111111

112112
/// <summary>
113113
/// Adds a store with history tracking and registers both as services.
114+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
114115
/// </summary>
115116
/// <typeparam name="TState">The type of state managed by the store.</typeparam>
116117
/// <param name="services">The service collection.</param>
@@ -159,12 +160,18 @@ public static IServiceCollection AddStoreWithHistory<TState>(
159160
return store;
160161
});
161162

163+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
164+
services.AddSingleton<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
165+
services.AddSingleton<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
166+
services.AddSingleton<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
167+
162168
return services;
163169
}
164170

165171
/// <summary>
166172
/// Adds a scoped store with history tracking.
167173
/// Use for Blazor Server where each circuit needs its own store.
174+
/// Also registers IStateReader, IStateWriter, and IStateObservable as aliases.
168175
/// </summary>
169176
/// <typeparam name="TState">The type of state managed by the store.</typeparam>
170177
/// <param name="services">The service collection.</param>
@@ -206,6 +213,11 @@ public static IServiceCollection AddScopedStoreWithHistory<TState>(
206213
return store;
207214
});
208215

216+
// Register interface aliases (required by AsyncActionExecutor and for interface segregation)
217+
services.AddScoped<IStateReader<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
218+
services.AddScoped<IStateWriter<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
219+
services.AddScoped<IStateObservable<TState>>(sp => sp.GetRequiredService<IStore<TState>>());
220+
209221
return services;
210222
}
211223
}

0 commit comments

Comments
 (0)