* Creates a signal that tracks the resource snapshot and handles transactional behavior * (freezing during navigation and rollback recovery).
( source: Resource<T>, router: Router, injector: Injector, )
| 72 | * (freezing during navigation and rollback recovery). |
| 73 | */ |
| 74 | function createTransactionalSnapshot<T>( |
| 75 | source: Resource<T>, |
| 76 | router: Router, |
| 77 | injector: Injector, |
| 78 | ): { |
| 79 | snapshot: Signal<ResourceSnapshot<T>>; |
| 80 | frozenSnapshot: Signal<ResourceSnapshot<T> | null>; |
| 81 | } { |
| 82 | // Holds a snapshot of the resource to keep the UI masked (frozen) during pending navigations |
| 83 | // or while recovering from a cancelled navigation. |
| 84 | const frozenSnapshot = signal<ResourceSnapshot<T> | null>(null); |
| 85 | |
| 86 | // Tracks whether we are in a recovery phase after a cancelled navigation. |
| 87 | // The intended behavior is that on cancellation, the router reverts to the previous state. |
| 88 | // This reversion might trigger a new load of the previous state because the signal dependencies |
| 89 | // changed. If we were to release the frozen resource state immediately, the user would see a loading state |
| 90 | // for data they were just looking at. To avoid this "loading flash", we retain the frozen |
| 91 | // value (via frozenSnapshot) during this recovery load/reload until the resource settles. |
| 92 | const isRollbackRecoveryPending = signal(false); |
| 93 | |
| 94 | const sub = router.events.subscribe((e) => { |
| 95 | if (e instanceof NavigationStart) { |
| 96 | isRollbackRecoveryPending.set(false); |
| 97 | |
| 98 | if (frozenSnapshot() === null) { |
| 99 | // Freeze the snapshot at the start of navigation to keep the UI stable. |
| 100 | frozenSnapshot.set(source.snapshot()); |
| 101 | } |
| 102 | } else if (e instanceof NavigationEnd || e instanceof NavigationSkipped) { |
| 103 | // Navigation succeeded or was skipped, so we can unfreeze and use the live state. |
| 104 | frozenSnapshot.set(null); |
| 105 | isRollbackRecoveryPending.set(false); |
| 106 | } else if (e instanceof NavigationCancel || e instanceof NavigationError) { |
| 107 | const isRollback = |
| 108 | e instanceof NavigationError || |
| 109 | (e instanceof NavigationCancel && |
| 110 | e.code !== NavigationCancellationCode.SupersededByNewNavigation && |
| 111 | e.code !== NavigationCancellationCode.Redirect); |
| 112 | |
| 113 | if (!isRollback) return; |
| 114 | |
| 115 | isRollbackRecoveryPending.set(true); |
| 116 | } |
| 117 | }); |
| 118 | |
| 119 | injector.get(DestroyRef).onDestroy(() => sub.unsubscribe()); |
| 120 | |
| 121 | effect( |
| 122 | () => { |
| 123 | if (isRollbackRecoveryPending() && !source.isLoading()) { |
| 124 | isRollbackRecoveryPending.set(false); |
| 125 | frozenSnapshot.set(null); |
| 126 | } |
| 127 | }, |
| 128 | {injector}, |
| 129 | ); |
| 130 | |
| 131 | return { |
no test coverage detected