Skip to content

Commit 8ddce80

Browse files
henry-alakazhangAndrewKushnir
authored andcommitted
feat(service-worker): allow specifying maxAge for entire application (#49601)
This commit adds an `applicationMaxAge` to the service worker configuration. When set, it will only assign a cached version to clients within the `maxAge`. Afterwards, it will ignored any expired application versions and fetch exclusively from the network. The default is `undefined`, for which the behaviour is the same as it currently is. PR Close #49601
1 parent 925de81 commit 8ddce80

File tree

9 files changed

+91
-2
lines changed

9 files changed

+91
-2
lines changed

adev/src/content/ecosystem/service-workers/config.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ A request is considered to be a navigation request if:
347347
* The URL must not contain a file extension (that is, a `.`) in the last path segment
348348
* The URL must not contain `__`
349349

350-
HELPFUL: To configure whether navigation requests are sent through to the network or not, see the [navigationRequestStrategy](#navigationrequeststrategy) section.
350+
HELPFUL: To configure whether navigation requests are sent through to the network or not, see the [navigationRequestStrategy](#navigationrequeststrategy) section and [applicationMaxAge](#application-max-age) sections.
351351

352352
#### Matching navigation request URLs
353353

@@ -391,3 +391,7 @@ This optional property enables you to configure how the service worker handles n
391391
| `'freshness'` | Passes the requests through to the network and falls back to the `performance` behavior when offline. This value is useful when the server redirects the navigation requests elsewhere using a `3xx` HTTP redirect status code. Reasons for using this value include: <ul> <li> Redirecting to an authentication website when authentication is not handled by the application </li> <li> Redirecting specific URLs to avoid breaking existing links/bookmarks after a website redesign </li> <li> Redirecting to a different website, such as a server-status page, while a page is temporarily down </li> </ul> |
392392

393393
IMPORTANT: The `freshness` strategy usually results in more requests sent to the server, which can increase response latency. It is recommended that you use the default performance strategy whenever possible.
394+
395+
### `applicationMaxAge`
396+
397+
This optional property enables you to configure how long the service worker will cache any requests. Within the `maxAge`, files will be served from cache. Beyond it, all requests will only be served from the network, including asset and data requests.

goldens/public-api/service-worker/config/index.api.md

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface Config {
2626
// (undocumented)
2727
appData?: {};
2828
// (undocumented)
29+
applicationMaxAge?: Duration;
30+
// (undocumented)
2931
assetGroups?: AssetGroup[];
3032
// (undocumented)
3133
dataGroups?: DataGroup[];

packages/service-worker/config/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@
177177
],
178178
"default": "performance",
179179
"description": "The Angular service worker can use two request strategies for navigation requests. 'performance', the default, skips navigation requests. The other strategy, 'freshness', forces all navigation requests through the network."
180+
},
181+
"applicationMaxAge": {
182+
"type": "string",
183+
"description": "Indicates how long the entire application is allowed to remain in the cache before being considered invalid and bypassed. 'maxAge' is a duration string, using the following unit suffixes: d= days, h= hours, m= minutes, s= seconds, u= milliseconds. For example, the string '3d12h' will cache content for up to three and a half days."
180184
}
181185
},
182186
"required": [

packages/service-worker/config/src/generator.ts

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export class Generator {
4343
hashTable: withOrderedKeys(unorderedHashTable),
4444
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
4545
navigationRequestStrategy: config.navigationRequestStrategy ?? 'performance',
46+
applicationMaxAge: config.applicationMaxAge
47+
? parseDurationToMs(config.applicationMaxAge)
48+
: undefined,
4649
};
4750
}
4851

packages/service-worker/config/src/in.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface Config {
2828
dataGroups?: DataGroup[];
2929
navigationUrls?: string[];
3030
navigationRequestStrategy?: 'freshness' | 'performance';
31+
applicationMaxAge?: Duration;
3132
}
3233

3334
/**

packages/service-worker/config/test/generator_spec.ts

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('Generator', () => {
5858
'http://example.com/included',
5959
'!http://example.com/excluded',
6060
],
61+
applicationMaxAge: '1d',
6162
});
6263

6364
expect(config).toEqual({
@@ -115,6 +116,7 @@ describe('Generator', () => {
115116
{positive: false, regex: '^http:\\/\\/example\\.com\\/excluded$'},
116117
],
117118
navigationRequestStrategy: 'performance',
119+
applicationMaxAge: 86400000,
118120
hashTable: {
119121
'/test/foo/test.html': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643',
120122
'/test/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
@@ -208,6 +210,7 @@ describe('Generator', () => {
208210
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
209211
],
210212
navigationRequestStrategy: 'performance',
213+
applicationMaxAge: undefined,
211214
});
212215
});
213216

@@ -234,6 +237,7 @@ describe('Generator', () => {
234237
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
235238
],
236239
navigationRequestStrategy: 'performance',
240+
applicationMaxAge: undefined,
237241
hashTable: {},
238242
});
239243
});
@@ -425,6 +429,7 @@ describe('Generator', () => {
425429
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
426430
],
427431
navigationRequestStrategy: 'performance',
432+
applicationMaxAge: undefined,
428433
hashTable: {},
429434
});
430435
});
@@ -498,6 +503,7 @@ describe('Generator', () => {
498503
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
499504
],
500505
navigationRequestStrategy: 'performance',
506+
applicationMaxAge: undefined,
501507
hashTable: {
502508
'/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
503509
'/main.js': '41347a66676cdc0516934c76d9d13010df420f2c',

packages/service-worker/worker/src/driver.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -497,10 +497,14 @@ export class Driver implements Debuggable, UpdateSource {
497497
// Decide which version of the app to use to serve this request. This is asynchronous as in
498498
// some cases, a record will need to be written to disk about the assignment that is made.
499499
const appVersion = await this.assignVersion(event);
500+
// If there's a configured max age, check whether this version is within that age.
501+
const isVersionWithinMaxAge =
502+
appVersion?.manifest.applicationMaxAge === undefined ||
503+
this.adapter.time - appVersion.manifest.timestamp < appVersion.manifest.applicationMaxAge;
500504
let res: Response | null = null;
501505

502506
try {
503-
if (appVersion !== null) {
507+
if (appVersion !== null && isVersionWithinMaxAge) {
504508
try {
505509
// Handle the request. First try the AppVersion. If that doesn't work, fall back on the
506510
// network.

packages/service-worker/worker/src/manifest.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface Manifest {
1919
dataGroups?: DataGroupConfig[];
2020
navigationUrls: {positive: boolean; regex: string}[];
2121
navigationRequestStrategy: 'freshness' | 'performance';
22+
applicationMaxAge?: number;
2223
hashTable: {[url: string]: string};
2324
}
2425

packages/service-worker/worker/test/happy_spec.ts

+64
Original file line numberDiff line numberDiff line change
@@ -2562,6 +2562,70 @@ import {envIsSupported} from '../testing/utils';
25622562
return {server, scope, driver};
25632563
}
25642564
});
2565+
2566+
describe('applicationMaxAge', () => {
2567+
// When within the `applicationMaxAge`, the app should act like `performance` mode
2568+
// When outside of it, it should act like `freshness` mode, except it also uncaches asset
2569+
// requests
2570+
it("doesn't create navigate requests within the maxAge", async () => {
2571+
const {server, scope, driver} = createSwForMaxAge();
2572+
2573+
await makeRequest(scope, '/foo.txt');
2574+
await driver.initialized;
2575+
await server.clearRequests();
2576+
2577+
// Create multiple requests to prove no navigation OR asset requests were made.
2578+
// By default the navigation request is not sent, it's replaced
2579+
// with the index request - thus, the `this is foo` value.
2580+
expect(await makeNavigationRequest(scope, '/', '')).toBe('this is foo');
2581+
expect(await makeNavigationRequest(scope, '/foo', '')).toBe('this is foo');
2582+
2583+
expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo');
2584+
expect(await makeRequest(scope, '/bar.txt')).toBe('this is bar');
2585+
2586+
server.assertNoOtherRequests();
2587+
});
2588+
2589+
it('creates navigate requests outside the maxAge', async () => {
2590+
const {server, scope, driver} = createSwForMaxAge();
2591+
2592+
await makeRequest(scope, '/foo.txt');
2593+
await driver.initialized;
2594+
await server.clearRequests();
2595+
2596+
await scope.advance(3000);
2597+
2598+
// Create multiple requests to prove the navigation and asset requests are all made
2599+
// When enabled, the navigation request is made each time and not replaced
2600+
// with the index request - thus, the `null` value.
2601+
expect(await makeNavigationRequest(scope, '/', '')).toBe(null);
2602+
expect(await makeNavigationRequest(scope, '/foo', '')).toBe(null);
2603+
2604+
expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo');
2605+
expect(await makeRequest(scope, '/bar.txt')).toBe('this is bar');
2606+
2607+
server.assertSawRequestFor('/');
2608+
server.assertSawRequestFor('/foo');
2609+
server.assertSawRequestFor('/foo.txt');
2610+
server.assertSawRequestFor('/bar.txt');
2611+
server.assertNoOtherRequests();
2612+
});
2613+
2614+
function createSwForMaxAge() {
2615+
const scope = new SwTestHarnessBuilder().build();
2616+
// set the timestamp of the manifest using the server time so it's always "new" on test start
2617+
const maxAgeManifest: Manifest = {
2618+
...manifest,
2619+
timestamp: scope.time,
2620+
applicationMaxAge: 2000,
2621+
};
2622+
const server = serverBuilderBase.withManifest(maxAgeManifest).build();
2623+
const driver = new Driver(scope, scope, new CacheDatabase(scope));
2624+
scope.updateServerState(server);
2625+
2626+
return {server, scope, driver};
2627+
}
2628+
});
25652629
});
25662630
})();
25672631

0 commit comments

Comments
 (0)