55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.dev/license
77 */
8- import { createEnvironmentInjector , runInInjectionContext , Resource } from '@angular/core' ;
8+ import {
9+ createEnvironmentInjector ,
10+ runInInjectionContext ,
11+ Resource ,
12+ effect ,
13+ DestroyRef ,
14+ } from '@angular/core' ;
915import { OperatorFunction , pipe } from 'rxjs' ;
1016import { ResourceContext , ResourceResult } from '../models' ;
1117import { NavigationTransition } from '../navigation_transition' ;
1218import { ActivatedRoute , ActivatedRouteSnapshot , initializeActivatedRoute } from '../router_state' ;
1319import { TreeNode } from '../utils/tree' ;
14- import { BLOCKING_SYMBOL , InternalRouterResource , routerResource } from '../router_resource' ;
20+ import {
21+ BLOCKING_SYMBOL ,
22+ hasValueOrResolved ,
23+ InternalRouterResource ,
24+ routerResource ,
25+ SOURCE_RESOURCE_SYMBOL ,
26+ } from '../router_resource' ;
1527import { switchTap } from './switch_tap' ;
1628
1729export function setupAndRunResources (
1830 abortSignal : AbortSignal ,
1931) : OperatorFunction < NavigationTransition , NavigationTransition > {
2032 return pipe (
2133 switchTap ( ( { newlyCreatedRoutes, targetRouterState} ) => {
22- if ( ! newlyCreatedRoutes || ! targetRouterState ) {
34+ if ( ! newlyCreatedRoutes || ! targetRouterState || abortSignal . aborted ) {
2335 return ;
2436 }
2537
2638 const resourceSetupPromises : Array < Promise < void > > = [ ] ;
39+ const blockingResourcePromises : Array < Promise < void > > = [ ] ;
2740
2841 const traverse = ( stateNode : TreeNode < ActivatedRoute > ) => {
2942 const route = stateNode . value ;
3043 if ( route ) {
3144 initializeActivatedRoute ( route ) ;
32- processRoute ( route , newlyCreatedRoutes , resourceSetupPromises , abortSignal ) ;
45+ processRoute (
46+ route ,
47+ newlyCreatedRoutes ,
48+ resourceSetupPromises ,
49+ abortSignal ,
50+ blockingResourcePromises ,
51+ ) ;
3352 }
3453
3554 for ( const childState of stateNode . children ) {
@@ -39,8 +58,7 @@ export function setupAndRunResources(
3958
4059 traverse ( targetRouterState . _root ) ;
4160
42- return Promise . all ( resourceSetupPromises ) ;
43- // TODO: wait for blocking resources
61+ return Promise . all ( resourceSetupPromises ) . then ( ( ) => Promise . all ( blockingResourcePromises ) ) ;
4462 } ) ,
4563 ) ;
4664}
@@ -50,6 +68,7 @@ function processRoute(
5068 newlyCreatedRoutes : Set < ActivatedRoute > ,
5169 resourceSetupPromises : Array < Promise < void > > ,
5270 abortSignal : AbortSignal ,
71+ blockingResourcePromises : Array < Promise < void > > ,
5372) {
5473 const resources = route . routeConfig ?. resources ;
5574 if ( ! resources ) {
@@ -58,16 +77,19 @@ function processRoute(
5877
5978 if ( newlyCreatedRoutes . has ( route ) ) {
6079 // This route is new. We need to run its resources function once.
61- resourceSetupPromises . push ( setupNewRouterResources ( route . _futureSnapshot , route , abortSignal ) ) ;
80+ resourceSetupPromises . push (
81+ setupNewRouterResources ( route . _futureSnapshot , route , abortSignal , blockingResourcePromises ) ,
82+ ) ;
6283 } else {
63- updateExistingResources ( route ) ;
84+ updateExistingResources ( route , blockingResourcePromises , abortSignal ) ;
6485 }
6586}
6687
6788async function setupNewRouterResources (
6889 snapshot : ActivatedRouteSnapshot ,
6990 route : ActivatedRoute ,
7091 abortSignal : AbortSignal ,
92+ blockingResourcePromises : Promise < void > [ ] ,
7193) {
7294 const resourcesFn = snapshot ?. routeConfig ?. resources ;
7395 const parentInjector = snapshot ?. _environmentInjector ;
@@ -120,22 +142,42 @@ async function setupNewRouterResources(
120142 }
121143
122144 route . resources = route . _futureSnapshot . resources = snapshot . resources = wrappedResult ;
123- prohibitBlockingResources ( route , wrappedResult ) ;
145+ setupBlocking ( route , wrappedResult , blockingResourcePromises , abortSignal ) ;
124146}
125147
126- function updateExistingResources ( route : ActivatedRoute ) {
148+ function updateExistingResources (
149+ route : ActivatedRoute ,
150+ blockingResourcePromises : Promise < void > [ ] ,
151+ abortSignal : AbortSignal ,
152+ ) {
127153 // This route is reused. We must eagerly update the resource context signals
128154 // so that resources can react and fetch new data during the pending navigation.
129155 const currentResources = route . snapshot ?. resources ;
130156 if ( ! currentResources ) {
131157 return ;
132158 }
133159
160+ Object . values ( currentResources ) . forEach ( ( r ) => {
161+ const underlyingRes = ( r as InternalRouterResource ) [ SOURCE_RESOURCE_SYMBOL ] ;
162+ if ( underlyingRes . status ( ) === 'error' ) {
163+ // If a resource previously failed and the route is reused identically,
164+ // the parameter signals won't change, meaning the internal effect won't automatically refetch.
165+ // We must manually trigger a reload to ensure the new navigation attempts a retry.
166+ ( underlyingRes as unknown as { reload ?: ( ) => boolean } ) . reload ?.( ) ;
167+ }
168+ } ) ;
169+
134170 route . _futureSnapshot . resources = currentResources ;
135- prohibitBlockingResources ( route , currentResources ) ;
171+ setupBlocking ( route , currentResources , blockingResourcePromises , abortSignal ) ;
136172}
137173
138- function prohibitBlockingResources ( route : ActivatedRoute , resourceResult : ResourceResult ) {
174+ function setupBlocking (
175+ route : ActivatedRoute ,
176+ resourceResult : ResourceResult ,
177+ blockingResourcePromises : Array < Promise < void > > ,
178+ abortSignal : AbortSignal ,
179+ ) {
180+ if ( abortSignal . aborted ) return ;
139181 const childInjector = route . _localInjector ;
140182 if ( ! childInjector || ! resourceResult ) return ;
141183
@@ -144,6 +186,47 @@ function prohibitBlockingResources(route: ActivatedRoute, resourceResult: Resour
144186 if ( res [ BLOCKING_SYMBOL ] === false ) {
145187 continue ;
146188 }
147- throw new Error ( 'blocking resources not implemented yet' ) ;
189+ const promise = new Promise < void > ( ( resolve , reject ) => {
190+ const underlyingRes = res [ SOURCE_RESOURCE_SYMBOL ] ;
191+ let isDestroyed = false ;
192+ let unregisterOnDestroy : ( ( ) => void ) | undefined ;
193+
194+ const cleanup = ( ) => {
195+ isDestroyed = true ;
196+ blockingEffect . destroy ( ) ;
197+ unregisterOnDestroy ?.( ) ;
198+ abortSignal . removeEventListener ( 'abort' , onAbort ) ;
199+ } ;
200+
201+ const onAbort = ( ) => {
202+ cleanup ( ) ;
203+ resolve ( ) ;
204+ } ;
205+
206+ abortSignal . addEventListener ( 'abort' , onAbort , { once : true } ) ;
207+
208+ const blockingEffect = effect (
209+ ( ) => {
210+ if ( isDestroyed ) {
211+ return ;
212+ }
213+ const status = underlyingRes . status ( ) ;
214+ if ( status === 'error' ) {
215+ cleanup ( ) ;
216+ reject ( underlyingRes . error ( ) ) ;
217+ } else if ( hasValueOrResolved ( underlyingRes ) ) {
218+ cleanup ( ) ;
219+ resolve ( ) ;
220+ }
221+ } ,
222+ { injector : childInjector , manualCleanup : true } ,
223+ ) ;
224+
225+ unregisterOnDestroy = childInjector . get ( DestroyRef ) . onDestroy ( ( ) => {
226+ cleanup ( ) ;
227+ resolve ( ) ;
228+ } ) ;
229+ } ) ;
230+ blockingResourcePromises . push ( promise ) ;
148231 }
149232}
0 commit comments