Skip to content

Commit 7811d95

Browse files
alan-agius4AndrewKushnir
authored andcommitted
refactor(core): improve resource loading with async/await
Refactor to use async/await for clearer asynchronous operations and enhanced error handling. Simplify resource caching and streamline the resolution of component templates and styles. Update in the router to align with the new async resource resolution. (cherry picked from commit 4ed8781)
1 parent e5b0a97 commit 7811d95

File tree

6 files changed

+79
-82
lines changed

6 files changed

+79
-82
lines changed

packages/core/src/metadata/resource_loading.ts

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88

99
import {RuntimeError, RuntimeErrorCode} from '../errors';
1010
import {Type} from '../interface/type';
11-
1211
import type {Component} from './directives';
1312

13+
let componentResourceResolutionQueue = new Map<Type<any>, Component>();
14+
15+
// Track when existing ɵcmp for a Type is waiting on resources.
16+
const componentDefPendingResolution = new Set<Type<any>>();
17+
1418
/**
1519
* Used to resolve resource URLs on `@Component` when used with JIT compilation.
1620
*
@@ -20,8 +24,7 @@ import type {Component} from './directives';
2024
* selector: 'my-comp',
2125
* templateUrl: 'my-comp.html', // This requires asynchronous resolution
2226
* })
23-
* class MyComponent{
24-
* }
27+
* class MyComponent { }
2528
*
2629
* // Calling `renderComponent` will fail because `renderComponent` is a synchronous process
2730
* // and `MyComponent`'s `@Component.templateUrl` needs to be resolved asynchronously.
@@ -31,90 +34,90 @@ import type {Component} from './directives';
3134
*
3235
* // Use browser's `fetch()` function as the default resource resolution strategy.
3336
* resolveComponentResources(fetch).then(() => {
34-
* // After resolution all URLs have been converted into `template` strings.
35-
* renderComponent(MyComponent);
37+
* // After resolution all URLs have been converted into `template` strings.
38+
* renderComponent(MyComponent);
3639
* });
3740
*
3841
* ```
3942
*
40-
* NOTE: In AOT the resolution happens during compilation, and so there should be no need
43+
* @remarks In AOT the resolution happens during compilation, and so there should be no need
4144
* to call this method outside JIT mode.
4245
*
4346
* @param resourceResolver a function which is responsible for returning a `Promise` to the
4447
* contents of the resolved URL. Browser's `fetch()` method is a good default implementation.
4548
*/
46-
export function resolveComponentResources(
49+
export async function resolveComponentResources(
4750
resourceResolver: (url: string) => Promise<string | {text(): Promise<string>; status?: number}>,
4851
): Promise<void> {
49-
// Store all promises which are fetching the resources.
50-
const componentResolved: Promise<void>[] = [];
52+
const currentQueue = componentResourceResolutionQueue;
53+
componentResourceResolutionQueue = new Map();
5154

5255
// Cache so that we don't fetch the same resource more than once.
53-
const urlMap = new Map<string, Promise<string>>();
56+
const urlCache = new Map<string, Promise<string>>();
57+
58+
// Helper to dedupe resource fetches
5459
function cachedResourceResolve(url: string): Promise<string> {
55-
let promise = urlMap.get(url);
56-
if (!promise) {
57-
const resp = resourceResolver(url);
58-
urlMap.set(url, (promise = resp.then((res) => unwrapResponse(url, res))));
60+
const promiseCached = urlCache.get(url);
61+
if (promiseCached) {
62+
return promiseCached;
5963
}
64+
65+
const promise = resourceResolver(url).then((response) => unwrapResponse(url, response));
66+
urlCache.set(url, promise);
67+
6068
return promise;
6169
}
6270

63-
componentResourceResolutionQueue.forEach((component: Component, type: Type<any>) => {
64-
const promises: Promise<void>[] = [];
71+
const resolutionPromises = Array.from(currentQueue).map(async ([type, component]) => {
72+
if (component.styleUrl && component.styleUrls?.length) {
73+
throw new Error(
74+
'@Component cannot define both `styleUrl` and `styleUrls`. ' +
75+
'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple',
76+
);
77+
}
78+
79+
const componentTasks: Promise<void>[] = [];
80+
6581
if (component.templateUrl) {
66-
promises.push(
82+
componentTasks.push(
6783
cachedResourceResolve(component.templateUrl).then((template) => {
6884
component.template = template;
6985
}),
7086
);
7187
}
88+
7289
const styles =
73-
typeof component.styles === 'string' ? [component.styles] : component.styles || [];
90+
typeof component.styles === 'string' ? [component.styles] : (component.styles ?? []);
7491
component.styles = styles;
7592

76-
if (component.styleUrl && component.styleUrls?.length) {
77-
throw new Error(
78-
'@Component cannot define both `styleUrl` and `styleUrls`. ' +
79-
'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple',
80-
);
81-
} else if (component.styleUrls?.length) {
82-
const styleOffset = component.styles.length;
83-
const styleUrls = component.styleUrls;
84-
component.styleUrls.forEach((styleUrl, index) => {
85-
styles.push(''); // pre-allocate array.
86-
promises.push(
87-
cachedResourceResolve(styleUrl).then((style) => {
88-
styles[styleOffset + index] = style;
89-
styleUrls.splice(styleUrls.indexOf(styleUrl), 1);
90-
if (styleUrls.length == 0) {
91-
component.styleUrls = undefined;
92-
}
93-
}),
94-
);
95-
});
96-
} else if (component.styleUrl) {
97-
promises.push(
98-
cachedResourceResolve(component.styleUrl).then((style) => {
99-
styles.push(style);
100-
component.styleUrl = undefined;
101-
}),
93+
let {styleUrl, styleUrls} = component;
94+
if (styleUrl) {
95+
styleUrls = [styleUrl];
96+
component.styleUrl = undefined;
97+
}
98+
99+
if (styleUrls?.length) {
100+
const allFetched = Promise.all(styleUrls.map((url) => cachedResourceResolve(url))).then(
101+
(fetchedStyles) => {
102+
styles.push(...fetchedStyles);
103+
component.styleUrls = undefined;
104+
},
102105
);
106+
107+
componentTasks.push(allFetched);
103108
}
104109

105-
const fullyResolved = Promise.all(promises).then(() => componentDefResolved(type));
106-
componentResolved.push(fullyResolved);
110+
await Promise.all(componentTasks);
111+
componentDefPendingResolution.delete(type);
107112
});
108-
clearResolutionOfComponentResourcesQueue();
109-
return Promise.all(componentResolved).then(() => undefined);
110-
}
111-
112-
let componentResourceResolutionQueue = new Map<Type<any>, Component>();
113113

114-
// Track when existing ɵcmp for a Type is waiting on resources.
115-
const componentDefPendingResolution = new Set<Type<any>>();
114+
await Promise.all(resolutionPromises);
115+
}
116116

117-
export function maybeQueueResolutionOfComponentResources(type: Type<any>, metadata: Component) {
117+
export function maybeQueueResolutionOfComponentResources(
118+
type: Type<any>,
119+
metadata: Component,
120+
): void {
118121
if (componentNeedsResolution(metadata)) {
119122
componentResourceResolutionQueue.set(type, metadata);
120123
componentDefPendingResolution.add(type);
@@ -128,10 +131,11 @@ export function isComponentDefPendingResolution(type: Type<any>): boolean {
128131
export function componentNeedsResolution(component: Component): boolean {
129132
return !!(
130133
(component.templateUrl && !component.hasOwnProperty('template')) ||
131-
(component.styleUrls && component.styleUrls.length) ||
134+
component.styleUrls?.length ||
132135
component.styleUrl
133136
);
134137
}
138+
135139
export function clearResolutionOfComponentResourcesQueue(): Map<Type<any>, Component> {
136140
const old = componentResourceResolutionQueue;
137141
componentResourceResolutionQueue = new Map();
@@ -140,32 +144,30 @@ export function clearResolutionOfComponentResourcesQueue(): Map<Type<any>, Compo
140144

141145
export function restoreComponentResolutionQueue(queue: Map<Type<any>, Component>): void {
142146
componentDefPendingResolution.clear();
143-
queue.forEach((_, type) => componentDefPendingResolution.add(type));
147+
for (const type of queue.keys()) {
148+
componentDefPendingResolution.add(type);
149+
}
144150
componentResourceResolutionQueue = queue;
145151
}
146152

147-
export function isComponentResourceResolutionQueueEmpty() {
153+
export function isComponentResourceResolutionQueueEmpty(): boolean {
148154
return componentResourceResolutionQueue.size === 0;
149155
}
150156

151-
function unwrapResponse(
157+
async function unwrapResponse(
152158
url: string,
153159
response: string | {text(): Promise<string>; status?: number},
154-
): string | Promise<string> {
160+
): Promise<string> {
155161
if (typeof response === 'string') {
156162
return response;
157163
}
164+
158165
if (response.status !== undefined && response.status !== 200) {
159-
return Promise.reject(
160-
new RuntimeError(
161-
RuntimeErrorCode.EXTERNAL_RESOURCE_LOADING_FAILED,
162-
ngDevMode && `Could not load resource: ${url}. Response status: ${response.status}`,
163-
),
166+
throw new RuntimeError(
167+
RuntimeErrorCode.EXTERNAL_RESOURCE_LOADING_FAILED,
168+
ngDevMode && `Could not load resource: ${url}. Response status: ${response.status}`,
164169
);
165170
}
166-
return response.text();
167-
}
168171

169-
function componentDefResolved(type: Type<any>): void {
170-
componentDefPendingResolution.delete(type);
172+
return response.text();
171173
}

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,6 @@
393393
"cleanUpFormContainer",
394394
"cleanUpValidators",
395395
"cleanUpView",
396-
"clearResolutionOfComponentResourcesQueue",
397396
"coerceToAsyncValidator",
398397
"coerceToValidator",
399398
"collectNativeNodes",
@@ -403,7 +402,6 @@
403402
"collectStylingFromTAttrs",
404403
"compileNgModuleFactory",
405404
"componentDefPendingResolution",
406-
"componentDefResolved",
407405
"componentResourceResolutionQueue",
408406
"compose",
409407
"composeAsync",

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,6 @@
395395
"checkStylingProperty",
396396
"classIndexOf",
397397
"cleanUpView",
398-
"clearResolutionOfComponentResourcesQueue",
399398
"coerceToAsyncValidator",
400399
"coerceToValidator",
401400
"collectNativeNodes",
@@ -405,7 +404,6 @@
405404
"collectStylingFromTAttrs",
406405
"compileNgModuleFactory",
407406
"componentDefPendingResolution",
408-
"componentDefResolved",
409407
"componentResourceResolutionQueue",
410408
"compose",
411409
"composeAsync",

packages/core/test/linker/source_map_integration_node_only_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ describe('jit source mapping', () => {
248248

249249
async function resolveCompileAndCreateComponent(comType: any, template: string) {
250250
await resolveComponentResources(createResolver(template));
251-
return await compileAndCreateComponent(comType);
251+
return compileAndCreateComponent(comType);
252252
}
253253

254254
let ɵcompilerFacade: CompilerFacade;

packages/core/test/metadata/resource_loading_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Did you run and wait for 'resolveComponentResources()'?`.trim(),
175175
};
176176
compileComponent(MyComponent, metadata);
177177

178-
expect(() => resolveComponentResources(testResolver)).toThrowError(
178+
await expectAsync(resolveComponentResources(testResolver)).toBeRejectedWithError(
179179
/@Component cannot define both `styleUrl` and `styleUrls`/,
180180
);
181181
});

packages/router/src/router_config_loader.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,17 @@ function maybeUnwrapDefaultExport<T>(input: T | DefaultExport<T>): T {
172172
return isWrappedDefaultExport(input) ? input['default'] : input;
173173
}
174174

175-
function maybeResolveResources<T>(value: T): Promise<T> {
175+
async function maybeResolveResources<T>(value: T): Promise<T> {
176176
// In JIT mode we usually resolve the resources of components on bootstrap, however
177177
// that won't have happened for lazy-loaded. Attempt to load any pending
178178
// resources again here.
179179
if ((typeof ngJitMode === 'undefined' || ngJitMode) && typeof fetch === 'function') {
180-
return resolveComponentResources(fetch)
181-
.catch((error) => {
182-
console.error(error);
183-
return Promise.resolve();
184-
})
185-
.then(() => value);
180+
try {
181+
await resolveComponentResources(fetch);
182+
} catch (error) {
183+
console.error(error);
184+
}
186185
}
187186

188-
return Promise.resolve(value);
187+
return value;
189188
}

0 commit comments

Comments
 (0)