Skip to content

Commit 81e7d15

Browse files
alan-agius4dylhunn
authored andcommitted
feat(platform-browser): enable HTTP request caching when using provideClientHydration (#49699)
This commit adds support by default for HTTP caching when using `provideClientHydration`. Users can opt-out of this behaviour by using the `withoutHttpTransferCache` feature. ```ts import { bootstrapApplication, provideClientHydration, withNoHttpTransferCache, } from '@angular/platform-browser'; // ... bootstrapApplication(RootCmp, { providers: [provideClientHydration(withNoHttpTransferCache())] }); ``` PR Close #49699
1 parent 41992a2 commit 81e7d15

File tree

9 files changed

+155
-51
lines changed

9 files changed

+155
-51
lines changed

goldens/public-api/platform-browser/index.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,12 @@ export interface HydrationFeature<FeatureKind extends HydrationFeatureKind> {
153153
}
154154

155155
// @public
156-
export type HydrationFeatures = NoDomReuseFeature;
156+
export const enum HydrationFeatureKind {
157+
// (undocumented)
158+
NoDomReuseFeature = 0,
159+
// (undocumented)
160+
NoHttpTransferCache = 1
161+
}
157162

158163
// @public @deprecated
159164
export const makeStateKey: typeof makeStateKey_2;
@@ -189,14 +194,11 @@ export type MetaDefinition = {
189194
[prop: string]: string;
190195
};
191196

192-
// @public
193-
export type NoDomReuseFeature = HydrationFeature<HydrationFeatureKind.NoDomReuseFeature>;
194-
195197
// @public
196198
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef;
197199

198200
// @public
199-
export function provideClientHydration(...features: HydrationFeatures[]): EnvironmentProviders;
201+
export function provideClientHydration(...features: HydrationFeature<HydrationFeatureKind>[]): EnvironmentProviders;
200202

201203
// @public
202204
export function provideProtractorTestingSupport(): Provider[];
@@ -251,7 +253,10 @@ export const TransferState: {
251253
export const VERSION: Version;
252254

253255
// @public
254-
export function withoutDomReuse(): NoDomReuseFeature;
256+
export function withNoDomReuse(): HydrationFeature<HydrationFeatureKind.NoDomReuseFeature>;
257+
258+
// @public
259+
export function withNoHttpTransferCache(): HydrationFeature<HydrationFeatureKind.NoHttpTransferCache>;
255260

256261
// (No @packageDocumentation comment for this package)
257262

packages/common/http/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export {HttpParameterCodec, HttpParams, HttpParamsOptions, HttpUrlEncodingCodec}
1717
export {HttpFeature, HttpFeatureKind, provideHttpClient, withInterceptors, withInterceptorsFromDi, withJsonpSupport, withNoXsrfProtection, withRequestsMadeViaParent, withXsrfConfiguration} from './src/provider';
1818
export {HttpRequest} from './src/request';
1919
export {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpResponseBase, HttpSentEvent, HttpStatusCode, HttpUploadProgressEvent, HttpUserEvent} from './src/response';
20+
export {withHttpTransferCache as ɵwithHttpTransferCache} from './src/transfer_cache';
2021
export {HttpXhrBackend} from './src/xhr';
2122
export {HttpXsrfTokenExtractor} from './src/xsrf';

packages/common/http/test/transfer_cache_spec.ts

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

99
import {DOCUMENT} from '@angular/common';
10-
import {ApplicationRef, Component} from '@angular/core';
10+
import {ApplicationRef, Component, Injectable} from '@angular/core';
1111
import {makeStateKey, TransferState} from '@angular/core/src/transfer_state';
1212
import {fakeAsync, flush, TestBed} from '@angular/core/testing';
1313
import {withBody} from '@angular/private/testing';
@@ -39,16 +39,16 @@ describe('TransferCache', () => {
3939
TestBed.resetTestingModule();
4040
isStable = new BehaviorSubject<boolean>(false);
4141

42-
class ApplicationRefPathed extends ApplicationRef {
42+
@Injectable()
43+
class ApplicationRefPatched extends ApplicationRef {
4344
override isStable = new BehaviorSubject<boolean>(false);
4445
}
4546

4647
TestBed.configureTestingModule({
4748
declarations: [SomeComponent],
4849
providers: [
4950
{provide: DOCUMENT, useFactory: () => document},
50-
{provide: ApplicationRef, useClass: ApplicationRefPathed},
51-
{provide: ApplicationRef, useClass: ApplicationRefPathed},
51+
{provide: ApplicationRef, useClass: ApplicationRefPatched},
5252
withHttpTransferCache(),
5353
provideHttpClient(),
5454
provideHttpClientTesting(),

packages/platform-browser/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ng_module(
1414
deps = [
1515
"//packages:types",
1616
"//packages/common",
17+
"//packages/common/http",
1718
"//packages/core",
1819
"//packages/zone.js/lib:zone_d_ts",
1920
"@npm//@types/hammerjs",

packages/platform-browser/src/hydration.ts

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ɵwithHttpTransferCache as withHttpTransferCache} from '@angular/common/http';
910
import {EnvironmentProviders, makeEnvironmentProviders, Provider, ɵwithDomHydration as withDomHydration} from '@angular/core';
1011

1112
/**
12-
* The list of features as an enum to uniquely type each feature.
13+
* The list of features as an enum to uniquely type each `HydrationFeature`.
14+
* @see HydrationFeature
15+
*
16+
* @publicApi
17+
* @developerPreview
1318
*/
1419
export const enum HydrationFeatureKind {
15-
NoDomReuseFeature
20+
NoDomReuseFeature,
21+
NoHttpTransferCache
1622
}
1723

1824
/**
@@ -30,49 +36,36 @@ export interface HydrationFeature<FeatureKind extends HydrationFeatureKind> {
3036
* Helper function to create an object that represents a Hydration feature.
3137
*/
3238
function hydrationFeature<FeatureKind extends HydrationFeatureKind>(
33-
kind: FeatureKind, providers: Provider[]): HydrationFeature<FeatureKind> {
39+
kind: FeatureKind, providers: Provider[] = []): HydrationFeature<FeatureKind> {
3440
return {ɵkind: kind, ɵproviders: providers};
3541
}
3642

37-
/**
38-
* A type alias that represents a feature which disables DOM reuse during hydration
39-
* (effectively making Angular re-render the whole application from scratch).
40-
* The type is used to describe the return value of the `withoutDomReuse` function.
41-
*
42-
* @see `withoutDomReuse`
43-
* @see `provideClientHydration`
44-
*
45-
* @publicApi
46-
* @developerPreview
47-
*/
48-
export type NoDomReuseFeature = HydrationFeature<HydrationFeatureKind.NoDomReuseFeature>;
49-
5043
/**
5144
* Disables DOM nodes reuse during hydration. Effectively makes
5245
* Angular re-render an application from scratch on the client.
5346
*
5447
* @publicApi
5548
* @developerPreview
5649
*/
57-
export function withoutDomReuse(): NoDomReuseFeature {
50+
export function withNoDomReuse(): HydrationFeature<HydrationFeatureKind.NoDomReuseFeature> {
5851
// This feature has no providers and acts as a flag that turns off
5952
// non-destructive hydration (which otherwise is turned on by default).
60-
const providers: Provider[] = [];
61-
return hydrationFeature(HydrationFeatureKind.NoDomReuseFeature, providers);
53+
return hydrationFeature(HydrationFeatureKind.NoDomReuseFeature);
6254
}
6355

6456
/**
65-
* A type alias that represents all Hydration features available for use with
66-
* `provideClientHydration`. Features can be enabled by adding special functions to the
67-
* `provideClientHydration` call. See documentation for each symbol to find corresponding
68-
* function name. See also `provideClientHydration` documentation on how to use those functions.
69-
*
70-
* @see `provideClientHydration`
57+
* Disables HTTP transfer cache. Effectively causes HTTP requests to be performed twice: once on the
58+
* server and other one on the browser.
7159
*
7260
* @publicApi
7361
* @developerPreview
7462
*/
75-
export type HydrationFeatures = NoDomReuseFeature;
63+
export function withNoHttpTransferCache():
64+
HydrationFeature<HydrationFeatureKind.NoHttpTransferCache> {
65+
// This feature has no providers and acts as a flag that turns off
66+
// HTTP transfer cache (which otherwise is turned on by default).
67+
return hydrationFeature(HydrationFeatureKind.NoHttpTransferCache);
68+
}
7669

7770
/**
7871
* Sets up providers necessary to enable hydration functionality for the application.
@@ -102,19 +95,31 @@ export type HydrationFeatures = NoDomReuseFeature;
10295
* export class AppModule {}
10396
* ```
10497
*
105-
* @see `HydrationFeatures`
98+
* @see `withNoDomReuse`
99+
* @see `withNoHttpTransferCache`
106100
*
107101
* @param features Optional features to configure additional router behaviors.
108102
* @returns A set of providers to enable hydration.
109103
*
110104
* @publicApi
111105
* @developerPreview
112106
*/
113-
export function provideClientHydration(...features: HydrationFeatures[]): EnvironmentProviders {
114-
const shouldUseDomHydration =
115-
!features.find(feature => feature.ɵkind === HydrationFeatureKind.NoDomReuseFeature);
107+
export function provideClientHydration(...features: HydrationFeature<HydrationFeatureKind>[]):
108+
EnvironmentProviders {
109+
const providers: Provider[] = [];
110+
const featuresKind = new Set<HydrationFeatureKind>();
111+
112+
for (const {ɵproviders, ɵkind} of features) {
113+
featuresKind.add(ɵkind);
114+
115+
if (ɵproviders.length) {
116+
providers.push(ɵproviders);
117+
}
118+
}
119+
116120
return makeEnvironmentProviders([
117-
(shouldUseDomHydration ? withDomHydration() : []),
118-
features.map(feature => feature.ɵproviders),
121+
(featuresKind.has(HydrationFeatureKind.NoDomReuseFeature) ? [] : withDomHydration()),
122+
(featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) ? [] : withHttpTransferCache()),
123+
providers,
119124
]);
120125
}

packages/platform-browser/src/platform-browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer';
7777
export {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
7878
export {HAMMER_GESTURE_CONFIG, HAMMER_LOADER, HammerGestureConfig, HammerLoader, HammerModule} from './dom/events/hammer_gestures';
7979
export {DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl, SafeValue} from './security/dom_sanitization_service';
80-
export {HydrationFeature, provideClientHydration, NoDomReuseFeature, HydrationFeatures, withoutDomReuse} from './hydration';
80+
export {HydrationFeature, provideClientHydration, HydrationFeatureKind, withNoDomReuse, withNoHttpTransferCache} from './hydration';
8181

8282
export * from './private_export';
8383
export {VERSION} from './version';

packages/platform-browser/test/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ ts_library(
2929
"//packages/animations/browser",
3030
"//packages/animations/browser/testing",
3131
"//packages/common",
32+
"//packages/common/http",
33+
"//packages/common/http/testing",
3234
"//packages/compiler",
3335
"//packages/core",
3436
"//packages/core/testing",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {DOCUMENT} from '@angular/common';
10+
import {HttpClient, provideHttpClient} from '@angular/common/http';
11+
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
12+
import {ApplicationRef, Component, Injectable} from '@angular/core';
13+
import {TestBed} from '@angular/core/testing';
14+
import {withBody} from '@angular/private/testing';
15+
import {BehaviorSubject} from 'rxjs';
16+
17+
import {provideClientHydration, withNoHttpTransferCache} from '../public_api';
18+
19+
describe('provideClientHydration', () => {
20+
@Component({selector: 'test-hydrate-app', template: ''})
21+
class SomeComponent {
22+
}
23+
24+
function makeRequestAndExpectOne(url: string, body: string): void {
25+
TestBed.inject(HttpClient).get(url).subscribe();
26+
TestBed.inject(HttpTestingController).expectOne(url).flush(body);
27+
}
28+
29+
function makeRequestAndExpectNone(url: string): void {
30+
TestBed.inject(HttpClient).get(url).subscribe();
31+
TestBed.inject(HttpTestingController).expectNone(url);
32+
}
33+
34+
@Injectable()
35+
class ApplicationRefPatched extends ApplicationRef {
36+
override isStable = new BehaviorSubject<boolean>(false);
37+
}
38+
39+
describe('default', () => {
40+
beforeEach(withBody('<test-hydrate-app></test-hydrate-app>', () => {
41+
TestBed.resetTestingModule();
42+
43+
TestBed.configureTestingModule({
44+
declarations: [SomeComponent],
45+
providers: [
46+
{provide: DOCUMENT, useFactory: () => document},
47+
{provide: ApplicationRef, useClass: ApplicationRefPatched},
48+
provideClientHydration(),
49+
provideHttpClient(),
50+
provideHttpClientTesting(),
51+
],
52+
});
53+
54+
const appRef = TestBed.inject(ApplicationRef);
55+
appRef.bootstrap(SomeComponent);
56+
}));
57+
58+
it(`should use cached HTTP calls`, () => {
59+
makeRequestAndExpectOne('/test-1', 'foo');
60+
// Do the same call, this time it should served from cache.
61+
makeRequestAndExpectNone('/test-1');
62+
});
63+
});
64+
65+
describe('withNoHttpTransferCache', () => {
66+
beforeEach(withBody('<test-hydrate-app></test-hydrate-app>', () => {
67+
TestBed.resetTestingModule();
68+
69+
TestBed.configureTestingModule({
70+
declarations: [SomeComponent],
71+
providers: [
72+
{provide: DOCUMENT, useFactory: () => document},
73+
{provide: ApplicationRef, useClass: ApplicationRefPatched},
74+
provideClientHydration(withNoHttpTransferCache()),
75+
provideHttpClient(),
76+
provideHttpClientTesting(),
77+
],
78+
});
79+
80+
const appRef = TestBed.inject(ApplicationRef);
81+
appRef.bootstrap(SomeComponent);
82+
}));
83+
84+
it(`should not cached HTTP calls`, () => {
85+
makeRequestAndExpectOne('/test-1', 'foo');
86+
// Do the same call, this time should pass through as cache is disabled.
87+
makeRequestAndExpectOne('/test-1', 'foo');
88+
});
89+
});
90+
});

packages/platform-server/test/hydration_spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pendin
1616
import {getComponentDef} from '@angular/core/src/render3/definition';
1717
import {unescapeTransferStateContent} from '@angular/core/src/transfer_state';
1818
import {TestBed} from '@angular/core/testing';
19-
import {bootstrapApplication, HydrationFeatures, provideClientHydration, withoutDomReuse} from '@angular/platform-browser';
19+
import {bootstrapApplication, HydrationFeature, HydrationFeatureKind, provideClientHydration, withNoDomReuse} from '@angular/platform-browser';
2020
import {provideRouter, RouterOutlet, Routes} from '@angular/router';
2121
import {first} from 'rxjs/operators';
2222

@@ -246,12 +246,12 @@ describe('platform-server integration', () => {
246246
*/
247247
async function ssr(
248248
component: Type<unknown>, doc?: string, envProviders?: Provider[],
249-
hydrationFeatures?: HydrationFeatures[]): Promise<string> {
249+
hydrationFeatures: HydrationFeature<HydrationFeatureKind>[] = []): Promise<string> {
250250
const defaultHtml = '<html><head></head><body><app></app></body></html>';
251251
const providers = [
252252
...(envProviders ?? []),
253253
provideServerRendering(),
254-
provideClientHydration(...(hydrationFeatures || [])),
254+
provideClientHydration(...hydrationFeatures),
255255
];
256256

257257
const bootstrap = () => bootstrapApplication(component, {providers});
@@ -272,7 +272,7 @@ describe('platform-server integration', () => {
272272
*/
273273
async function hydrate(
274274
html: string, component: Type<unknown>, envProviders?: Provider[],
275-
hydrationFeatures?: HydrationFeatures[]): Promise<ApplicationRef> {
275+
hydrationFeatures: HydrationFeature<HydrationFeatureKind>[] = []): Promise<ApplicationRef> {
276276
// Destroy existing platform, a new one will be created later by the `bootstrapApplication`.
277277
destroyPlatform();
278278

@@ -289,14 +289,14 @@ describe('platform-server integration', () => {
289289
const providers = [
290290
...(envProviders ?? []),
291291
{provide: DOCUMENT, useFactory: _document, deps: []},
292-
provideClientHydration(...(hydrationFeatures || [])),
292+
provideClientHydration(...hydrationFeatures),
293293
];
294294

295295
return bootstrapApplication(component, {providers});
296296
}
297297

298298
describe('public API', () => {
299-
it('should allow to disable DOM hydration using `withoutDomReuse` feature', async () => {
299+
it('should allow to disable DOM hydration using `withNoDomReuse` feature', async () => {
300300
@Component({
301301
standalone: true,
302302
selector: 'app',
@@ -310,7 +310,7 @@ describe('platform-server integration', () => {
310310
}
311311

312312
const html =
313-
await ssr(SimpleComponent, undefined, [withDebugConsole()], [withoutDomReuse()]);
313+
await ssr(SimpleComponent, undefined, [withDebugConsole()], [withNoDomReuse()]);
314314
const ssrContents = getAppContents(html);
315315

316316
// There should be no `ngh` annotations.
@@ -319,7 +319,7 @@ describe('platform-server integration', () => {
319319
resetTViewsFor(SimpleComponent);
320320

321321
const appRef =
322-
await hydrate(html, SimpleComponent, [withDebugConsole()], [withoutDomReuse()]);
322+
await hydrate(html, SimpleComponent, [withDebugConsole()], [withNoDomReuse()]);
323323
const compRef = getComponentRef<SimpleComponent>(appRef);
324324
appRef.tick();
325325

0 commit comments

Comments
 (0)