Skip to content

Commit 079f4bc

Browse files
alan-agius4AndrewKushnir
authored andcommitted
fix(http): wait for all XHR requests to finish before stabilizing application (#49776)
Previously, since the `HttpXhrBackend` is a singleton, the macrotask was created and completed only for the initial request since it was stored as in property in the class instance. This commit replaces this logic to create a macro task for every XHR request. Closes #49730 PR Close #49776
1 parent ce38be0 commit 079f4bc

File tree

5 files changed

+35
-25
lines changed

5 files changed

+35
-25
lines changed

goldens/public-api/common/http/index.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import * as i0 from '@angular/core';
1010
import { InjectionToken } from '@angular/core';
1111
import { ModuleWithProviders } from '@angular/core';
1212
import { Observable } from 'rxjs';
13-
import { OnDestroy } from '@angular/core';
1413
import { Provider } from '@angular/core';
1514
import { XhrFactory } from '@angular/common';
1615

@@ -2129,12 +2128,10 @@ export interface HttpUserEvent<T> {
21292128
}
21302129

21312130
// @public
2132-
export class HttpXhrBackend implements HttpBackend, OnDestroy {
2131+
export class HttpXhrBackend implements HttpBackend {
21332132
constructor(xhrFactory: XhrFactory);
21342133
handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
21352134
// (undocumented)
2136-
ngOnDestroy(): void;
2137-
// (undocumented)
21382135
static ɵfac: i0.ɵɵFactoryDeclaration<HttpXhrBackend, never>;
21392136
// (undocumented)
21402137
static ɵprov: i0.ɵɵInjectableDeclaration<HttpXhrBackend>;

integration/platform-server/e2e/http-transferstate-lazy-spec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ describe('Http TransferState Lazy', function() {
1616
browser.driver.get(browser.baseUrl + 'http-transferstate-lazy');
1717

1818
// Test the contents from the server.
19-
const serverDiv = browser.driver.findElement(by.css('div'));
20-
expect(serverDiv.getText()).toBe('API response');
19+
const serverDivOne = browser.driver.findElement(by.css('div.one'));
20+
expect(serverDivOne.getText()).toBe('API 1 response');
21+
22+
const serverDivTwo = browser.driver.findElement(by.css('div.two'));
23+
expect(serverDivTwo.getText()).toBe('API 2 response');
2124

2225
// Bootstrap the client side app and retest the contents
2326
browser.executeScript('doBootstrap()');
24-
expect(element(by.css('div')).getText()).toBe('API response');
27+
expect(element(by.css('div.one')).getText()).toBe('API 1 response');
28+
expect(element(by.css('div.two')).getText()).toBe('API 2 response');
2529

2630
// Make sure there were no client side errors.
2731
verifyNoBrowserErrors();

integration/platform-server/src/http-transferstate-lazy/transfer-state.component.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@ import {isPlatformServer} from '@angular/common';
1010
import {HttpClient} from '@angular/common/http';
1111
import {Component, Inject, PLATFORM_ID, TransferState, makeStateKey} from '@angular/core';
1212

13-
const httpCacheKey = makeStateKey<string>('http');
13+
const httpCacheKeyOne = makeStateKey<string>('http-one');
14+
const httpCacheKeyTwo = makeStateKey<string>('http-two');
1415

1516
@Component({
1617
selector: 'transfer-state-app-http',
1718
template: `
18-
<div>{{ response }}</div>
19+
<div class="one">{{ responseOne }}</div>
20+
<div class="two">{{ responseTwo }}</div>
1921
`,
2022
})
2123
export class TransferStateComponent {
22-
response: string = '';
24+
responseOne: string = '';
25+
responseTwo: string = '';
2326

2427
constructor(
2528
@Inject(PLATFORM_ID) private platformId: {},
@@ -30,11 +33,17 @@ export class TransferStateComponent {
3033
ngOnInit() {
3134
if (isPlatformServer(this.platformId)) {
3235
this.httpClient.get<any>(`http://localhost:4206/api`).subscribe((response) => {
33-
this.transferState.set(httpCacheKey, response.data);
34-
this.response = response.data;
36+
this.transferState.set(httpCacheKeyOne, response.data);
37+
this.responseOne = response.data;
38+
});
39+
40+
this.httpClient.get<any>(`http://localhost:4206/api-2`).subscribe((response) => {
41+
this.transferState.set(httpCacheKeyTwo, response.data);
42+
this.responseTwo = response.data;
3543
});
3644
} else {
37-
this.response = this.transferState.get(httpCacheKey, '');
45+
this.responseOne = this.transferState.get(httpCacheKeyOne, '');
46+
this.responseTwo = this.transferState.get(httpCacheKeyTwo, '');
3847
}
3948
}
4049
}

integration/platform-server/src/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ app.get('/favicon.ico', (req, res) => {
4646

4747
// Mock API
4848
app.get('/api', (req, res) => {
49-
res.json({ data: 'API response'});
49+
res.json({ data: 'API 1 response'});
50+
});
51+
52+
app.get('/api-2', (req, res) => {
53+
res.json({ data: 'API 2 response'});
5054
});
5155

5256
//-----------ADD YOUR SERVER SIDE RENDERED APP HERE ----------------------

packages/common/http/src/xhr.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {XhrFactory} from '@angular/common';
10-
import {Injectable, OnDestroy} from '@angular/core';
10+
import {Injectable} from '@angular/core';
1111
import {Observable, Observer} from 'rxjs';
1212

1313
import {HttpBackend} from './backend';
@@ -40,9 +40,7 @@ function getResponseUrl(xhr: any): string|null {
4040
* @publicApi
4141
*/
4242
@Injectable()
43-
export class HttpXhrBackend implements HttpBackend, OnDestroy {
44-
private macroTaskCanceller: VoidFunction|undefined;
45-
43+
export class HttpXhrBackend implements HttpBackend {
4644
constructor(private xhrFactory: XhrFactory) {}
4745

4846
/**
@@ -295,12 +293,14 @@ export class HttpXhrBackend implements HttpBackend, OnDestroy {
295293
}
296294
}
297295

296+
let macroTaskCanceller: VoidFunction|undefined;
297+
298298
/** Tear down logic to cancel the backround macrotask. */
299299
const onLoadStart = () => {
300-
this.macroTaskCanceller ??= createBackgroundMacroTask();
300+
macroTaskCanceller ??= createBackgroundMacroTask();
301301
};
302302
const onLoadEnd = () => {
303-
this.macroTaskCanceller?.();
303+
macroTaskCanceller?.();
304304
};
305305

306306
xhr.addEventListener('loadstart', onLoadStart);
@@ -321,7 +321,7 @@ export class HttpXhrBackend implements HttpBackend, OnDestroy {
321321
xhr.removeEventListener('timeout', onError);
322322

323323
// Cancel the background macrotask.
324-
this.macroTaskCanceller?.();
324+
macroTaskCanceller?.();
325325

326326
if (req.reportProgress) {
327327
xhr.removeEventListener('progress', onDownProgress);
@@ -337,10 +337,6 @@ export class HttpXhrBackend implements HttpBackend, OnDestroy {
337337
};
338338
});
339339
}
340-
341-
ngOnDestroy(): void {
342-
this.macroTaskCanceller?.();
343-
}
344340
}
345341

346342
// Cannot use `Number.MAX_VALUE` as it does not fit into a 32-bit signed integer.

0 commit comments

Comments
 (0)