* 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, )
| 99 | * (freezing during navigation and rollback recovery). |
| 100 | */ |
| 101 | function createTransactionalSnapshot<T>( |
| 102 | source: Resource<T>, |
| 103 | router: Router, |
| 104 | injector: Injector, |
| 105 | ): { |
| 106 | snapshot: Signal<ResourceSnapshot<T>>; |
| 107 | frozenSnapshot: Signal<ResourceSnapshot<T> | null>; |
| 108 | } { |
| 109 | // Holds a snapshot of the resource to keep the UI masked (frozen) during pending navigations |
| 110 | // or while recovering from a cancelled navigation. |
| 111 | const frozenSnapshot = signal<ResourceSnapshot<T> | null>(null); |
| 112 | |
| 113 | // Tracks whether we are in a recovery phase after a cancelled navigation. |
| 114 | // The intended behavior is that on cancellation, the router reverts to the previous state. |
| 115 | // This reversion might trigger a new load of the previous state because the signal dependencies |
| 116 | // changed. If we were to release the frozen resource state immediately, the user would see a loading state |
| 117 | // for data they were just looking at. To avoid this "loading flash", we retain the frozen |
| 118 | // value (via frozenSnapshot) during this recovery load/reload until the resource settles. |
| 119 | const isRollbackRecoveryPending = signal(false); |
| 120 | |
| 121 | const sub = router.events.subscribe((e) => { |
| 122 | if (e instanceof NavigationStart) { |
| 123 | isRollbackRecoveryPending.set(false); |
| 124 | |
| 125 | if (frozenSnapshot() === null) { |
| 126 | // Freeze the snapshot at the start of navigation to keep the UI stable. |
| 127 | frozenSnapshot.set(source.snapshot()); |
| 128 | } |
| 129 | } else if (e instanceof NavigationEnd) { |
| 130 | // Navigation succeeded, so we can unfreeze and use the live state. |
| 131 | frozenSnapshot.set(null); |
| 132 | isRollbackRecoveryPending.set(false); |
| 133 | } else if (e instanceof NavigationSkipped) { |
| 134 | // If a navigation is skipped while we have a frozen snapshot (e.g. navigating to the |
| 135 | // current URL to cancel an in-flight navigation), the in-flight navigation is aborted |
| 136 | // and parameter rollback begins. We must maintain the frozen snapshot until the rollback |
| 137 | // recovery load completes to prevent flashing a loading state. |
| 138 | if (frozenSnapshot() !== null) { |
| 139 | isRollbackRecoveryPending.set(true); |
| 140 | } |
| 141 | } else if (e instanceof NavigationCancel || e instanceof NavigationError) { |
| 142 | const isRollback = |
| 143 | e instanceof NavigationError || |
| 144 | (e instanceof NavigationCancel && |
| 145 | e.code !== NavigationCancellationCode.SupersededByNewNavigation && |
| 146 | e.code !== NavigationCancellationCode.Redirect); |
| 147 | |
| 148 | if (!isRollback) return; |
| 149 | |
| 150 | isRollbackRecoveryPending.set(true); |
| 151 | } |
| 152 | }); |
| 153 | |
| 154 | injector.get(DestroyRef).onDestroy(() => sub.unsubscribe()); |
| 155 | |
| 156 | effect( |
| 157 | () => { |
| 158 | if ( |
no test coverage detected