@@ -28,25 +28,11 @@ public static class SuspensionHostExtensions
2828 /// surface it without CA1859 demanding the concrete singleton type leak into their signatures.</summary>
2929 private static readonly IObservable < RxVoid > _completed = ImmutableReturnRxVoidSignal . Instance ;
3030
31- /// <summary>Func used to load app state exactly once. Backing field for testing purposes.</summary>
32- private static Func < IObservable < RxVoid > > ? _ensureLoadAppStateFunc ;
33-
34- /// <summary>Suspension driver reference field. Backing field for testing purposes.</summary>
35- private static ISuspensionDriver ? _suspensionDriver ;
36-
37- /// <summary>Gets or sets the ensure load app state function. Internal for testing purposes only.</summary>
38- internal static Func < IObservable < RxVoid > > ? EnsureLoadAppStateFunc
39- {
40- get => Volatile . Read ( ref _ensureLoadAppStateFunc ) ;
41- set => Volatile . Write ( ref _ensureLoadAppStateFunc , value ) ;
42- }
43-
44- /// <summary>Gets or sets the suspension driver. Internal for testing purposes only.</summary>
45- internal static ISuspensionDriver ? SuspensionDriver
46- {
47- get => _suspensionDriver ;
48- set => _suspensionDriver = value ;
49- }
31+ /// <summary>
32+ /// The pending one-time app-state load of each host, keyed by host so that every host loads through the driver it
33+ /// was set up with. The table holds its keys weakly, so it does not keep a host alive.
34+ /// </summary>
35+ private static readonly ConditionalWeakTable < ISuspensionHost , PendingLoad > _pendingLoads = new ( ) ;
5036
5137 /// <summary>Provides app-state access and suspend/resume setup extension members for <see cref="ISuspensionHost"/>.</summary>
5238 /// <param name="item">The suspension host.</param>
@@ -73,7 +59,7 @@ public T GetAppState<T>()
7359 {
7460 ArgumentExceptionHelper . ThrowIfNull ( item ) ;
7561
76- Interlocked . Exchange ( ref _ensureLoadAppStateFunc , null ) ? . Invoke ( ) ;
62+ RunPendingLoad ( item ) ;
7763
7864 return ( T ) item . AppState ! ;
7965 }
@@ -148,18 +134,18 @@ public IDisposable SetupDefaultSuspendResume(ISuspensionDriver? driver)
148134 {
149135 ArgumentExceptionHelper . ThrowIfNull ( item ) ;
150136
151- MultipleDisposable ret = [ ] ;
152- _suspensionDriver ??= driver ?? AppLocator . Current . GetService < ISuspensionDriver > ( ) ;
137+ var resolvedDriver = driver ?? AppLocator . Current . GetService < ISuspensionDriver > ( ) ;
153138
154- if ( _suspensionDriver is null )
139+ if ( resolvedDriver is null )
155140 {
156141 item . Log ( ) . Error ( "Could not find a valid driver and therefore cannot setup Suspend/Resume." ) ;
157142 return EmptyDisposable . Instance ;
158143 }
159144
160- _ensureLoadAppStateFunc = ( ) => EnsureLoadAppState ( item , _suspensionDriver ) ;
145+ var pendingLoad = _pendingLoads . GetValue ( item , static _ => new PendingLoad ( ) ) ;
146+ pendingLoad . Arm ( ( ) => EnsureLoadAppState ( item , resolvedDriver ) ) ;
161147
162- var resolvedDriver = _suspensionDriver ! ;
148+ MultipleDisposable ret = [ ] ;
163149 ret . Add ( item . ShouldInvalidateState . Subscribe ( new DriverOperationObserver < RxVoid > (
164150 item ,
165151 _ => resolvedDriver . InvalidateState ( ) ,
@@ -173,15 +159,15 @@ public IDisposable SetupDefaultSuspendResume(ISuspensionDriver? driver)
173159 {
174160 // Materialize app state (one-time load) before saving, so a shutdown that races ahead of
175161 // the resume/launch load still persists real state rather than null (see #4353).
176- RunPendingLoad ( ) ;
162+ pendingLoad . Run ( ) ;
177163 return resolvedDriver . SaveState ( item . AppState ! ) ;
178164 } ,
179165 static token => token . Dispose ( ) ,
180166 "Persisted application state" ,
181167 "Tried to persist app state" ) ) ) ;
182168
183- ret . Add ( item . IsResuming . Subscribe ( new DelegateObserver < RxVoid > ( static _ => RunPendingLoad ( ) ) ) ) ;
184- ret . Add ( item . IsLaunchingNew . Subscribe ( new DelegateObserver < RxVoid > ( static _ => RunPendingLoad ( ) ) ) ) ;
169+ ret . Add ( item . IsResuming . Subscribe ( new DelegateObserver < RxVoid > ( _ => pendingLoad . Run ( ) ) ) ) ;
170+ ret . Add ( item . IsLaunchingNew . Subscribe ( new DelegateObserver < RxVoid > ( _ => pendingLoad . Run ( ) ) ) ) ;
185171
186172 return ret ;
187173 }
@@ -203,7 +189,7 @@ public TAppState GetAppState()
203189 {
204190 ArgumentExceptionHelper . ThrowIfNull ( item ) ;
205191
206- Interlocked . Exchange ( ref _ensureLoadAppStateFunc , null ) ? . Invoke ( ) ;
192+ RunPendingLoad ( item ) ;
207193
208194 return item . AppStateValue ! ;
209195 }
@@ -243,18 +229,18 @@ public IDisposable SetupDefaultSuspendResume(
243229 ArgumentExceptionHelper . ThrowIfNull ( item ) ;
244230 ArgumentExceptionHelper . ThrowIfNull ( typeInfo ) ;
245231
246- MultipleDisposable ret = [ ] ;
247- _suspensionDriver ??= driver ?? AppLocator . Current . GetService < ISuspensionDriver > ( ) ;
232+ var resolvedDriver = driver ?? AppLocator . Current . GetService < ISuspensionDriver > ( ) ;
248233
249- if ( _suspensionDriver is null )
234+ if ( resolvedDriver is null )
250235 {
251236 item . Log ( ) . Error ( "Could not find a valid driver and therefore cannot setup Suspend/Resume." ) ;
252237 return EmptyDisposable . Instance ;
253238 }
254239
255- _ensureLoadAppStateFunc = ( ) => EnsureLoadAppState ( item , _suspensionDriver , typeInfo ) ;
240+ var pendingLoad = _pendingLoads . GetValue ( item , static _ => new PendingLoad ( ) ) ;
241+ pendingLoad . Arm ( ( ) => EnsureLoadAppState ( item , resolvedDriver , typeInfo ) ) ;
256242
257- var resolvedDriver = _suspensionDriver ! ;
243+ MultipleDisposable ret = [ ] ;
258244 ret . Add ( item . ShouldInvalidateState . Subscribe ( new DriverOperationObserver < RxVoid > (
259245 item ,
260246 _ => resolvedDriver . InvalidateState ( ) ,
@@ -268,15 +254,15 @@ public IDisposable SetupDefaultSuspendResume(
268254 {
269255 // Materialize app state (one-time load) before saving, so a shutdown that races ahead of
270256 // the resume/launch load still persists real state rather than null (see #4353).
271- RunPendingLoad ( ) ;
257+ pendingLoad . Run ( ) ;
272258 return resolvedDriver . SaveState ( item . AppStateValue ! , typeInfo ) ;
273259 } ,
274260 static token => token . Dispose ( ) ,
275261 "Persisted application state" ,
276262 "Tried to persist app state" ) ) ) ;
277263
278- ret . Add ( item . IsResuming . Subscribe ( new DelegateObserver < RxVoid > ( static _ => RunPendingLoad ( ) ) ) ) ;
279- ret . Add ( item . IsLaunchingNew . Subscribe ( new DelegateObserver < RxVoid > ( static _ => RunPendingLoad ( ) ) ) ) ;
264+ ret . Add ( item . IsResuming . Subscribe ( new DelegateObserver < RxVoid > ( _ => pendingLoad . Run ( ) ) ) ) ;
265+ ret . Add ( item . IsLaunchingNew . Subscribe ( new DelegateObserver < RxVoid > ( _ => pendingLoad . Run ( ) ) ) ) ;
280266
281267 return ret ;
282268 }
@@ -292,25 +278,17 @@ public IDisposable SetupDefaultSuspendResume(
292278 [ RequiresDynamicCode (
293279 "This overload may invoke ISuspensionDriver.LoadState(), which is commonly reflection-based. "
294280 + "Prefer EnsureLoadAppState<TAppState>(ISuspensionHost<TAppState>, ISuspensionDriver?, JsonTypeInfo<TAppState>) for trimming/AOT scenarios." ) ]
295- private static IObservable < RxVoid > EnsureLoadAppState ( ISuspensionHost item , ISuspensionDriver ? driver = null )
281+ private static IObservable < RxVoid > EnsureLoadAppState ( ISuspensionHost item , ISuspensionDriver driver )
296282 {
297283 if ( item . AppState is not null )
298284 {
299285 return _completed ;
300286 }
301287
302- _suspensionDriver ??= driver ?? AppLocator . Current . GetService < ISuspensionDriver > ( ) ;
303-
304- if ( _suspensionDriver is null )
305- {
306- item . Log ( ) . Error ( "Could not find a valid driver and therefore cannot load app state." ) ;
307- return _completed ;
308- }
309-
310288 try
311289 {
312290 // Fall back to a freshly created state when the driver yields no persisted state (see #4349).
313- item . AppState = WaitForResult ( _suspensionDriver . LoadState ( ) ) ?? item . CreateNewAppState ? . Invoke ( ) ;
291+ item . AppState = WaitForResult ( driver . LoadState ( ) ) ?? item . CreateNewAppState ? . Invoke ( ) ;
314292 }
315293 catch ( Exception ex )
316294 {
@@ -329,7 +307,7 @@ private static IObservable<RxVoid> EnsureLoadAppState(ISuspensionHost item, ISus
329307 /// <returns>A completed observable.</returns>
330308 private static IObservable < RxVoid > EnsureLoadAppState < TAppState > (
331309 ISuspensionHost < TAppState > item ,
332- ISuspensionDriver ? driver ,
310+ ISuspensionDriver driver ,
333311 JsonTypeInfo < TAppState > typeInfo )
334312 where TAppState : class
335313 {
@@ -338,18 +316,10 @@ private static IObservable<RxVoid> EnsureLoadAppState<TAppState>(
338316 return _completed ;
339317 }
340318
341- _suspensionDriver ??= driver ?? AppLocator . Current . GetService < ISuspensionDriver > ( ) ;
342-
343- if ( _suspensionDriver is null )
344- {
345- item . Log ( ) . Error ( "Could not find a valid driver and therefore cannot load app state." ) ;
346- return _completed ;
347- }
348-
349319 try
350320 {
351321 // Fall back to a freshly created state when the driver yields no persisted state (see #4349).
352- item . AppStateValue = WaitForResult ( _suspensionDriver . LoadState ( typeInfo ) ) ?? item . CreateNewAppStateTyped ? . Invoke ( ) ;
322+ item . AppStateValue = WaitForResult ( driver . LoadState ( typeInfo ) ) ?? item . CreateNewAppStateTyped ? . Invoke ( ) ;
353323 }
354324 catch ( Exception ex )
355325 {
@@ -360,9 +330,17 @@ private static IObservable<RxVoid> EnsureLoadAppState<TAppState>(
360330 return _completed ;
361331 }
362332
363- /// <summary>Runs the pending one-time app-state load exactly once, if one is registered.</summary>
364- [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
365- private static void RunPendingLoad ( ) => Interlocked . Exchange ( ref _ensureLoadAppStateFunc , null ) ? . Invoke ( ) ;
333+ /// <summary>Runs the host's pending one-time app-state load exactly once, if one is registered.</summary>
334+ /// <param name="item">The suspension host.</param>
335+ private static void RunPendingLoad ( ISuspensionHost item )
336+ {
337+ if ( ! _pendingLoads . TryGetValue ( item , out var pendingLoad ) )
338+ {
339+ return ;
340+ }
341+
342+ pendingLoad . Run ( ) ;
343+ }
366344
367345 /// <summary>Subscribes to a single-value observable and blocks until it terminates, returning its last value.</summary>
368346 /// <typeparam name="T">The value type.</typeparam>
@@ -552,6 +530,22 @@ private void Finish()
552530 }
553531 }
554532
533+ /// <summary>Holds one host's pending one-time app-state load, bound to the driver that host was set up with.</summary>
534+ private sealed class PendingLoad
535+ {
536+ /// <summary>The load to run, or <see langword="null"/> once it has run or before any setup.</summary>
537+ private Func < IObservable < RxVoid > > ? _load ;
538+
539+ /// <summary>Registers the load to run, replacing any load that has not run yet.</summary>
540+ /// <param name="load">The load to run.</param>
541+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
542+ public void Arm ( Func < IObservable < RxVoid > > load ) => Volatile . Write ( ref _load , load ) ;
543+
544+ /// <summary>Runs the registered load exactly once; later calls do nothing until the next <see cref="Arm"/>.</summary>
545+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
546+ public void Run ( ) => Interlocked . Exchange ( ref _load , null ) ? . Invoke ( ) ;
547+ }
548+
555549 /// <summary>Captures the last value (or terminal error) of a blocking subscription and signals on termination.</summary>
556550 /// <typeparam name="T">The value type.</typeparam>
557551 /// <param name="signal">Set when the source terminates.</param>
0 commit comments