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
This commit is contained in:
Henry Zhang
2024-10-10 11:41:01 +11:00
committed by Andrew Kushnir
parent 925de81490
commit 8ddce80a0b
9 changed files with 91 additions and 2 deletions
@@ -347,7 +347,7 @@ A request is considered to be a navigation request if:
* The URL must not contain a file extension (that is, a `.`) in the last path segment
* The URL must not contain `__`
HELPFUL: To configure whether navigation requests are sent through to the network or not, see the [navigationRequestStrategy](#navigationrequeststrategy) section.
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.
#### Matching navigation request URLs
@@ -391,3 +391,7 @@ This optional property enables you to configure how the service worker handles n
| `'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> |
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.
### `applicationMaxAge`
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.
@@ -26,6 +26,8 @@ export interface Config {
// (undocumented)
appData?: {};
// (undocumented)
applicationMaxAge?: Duration;
// (undocumented)
assetGroups?: AssetGroup[];
// (undocumented)
dataGroups?: DataGroup[];
@@ -177,6 +177,10 @@
],
"default": "performance",
"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."
},
"applicationMaxAge": {
"type": "string",
"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."
}
},
"required": [
@@ -43,6 +43,9 @@ export class Generator {
hashTable: withOrderedKeys(unorderedHashTable),
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
navigationRequestStrategy: config.navigationRequestStrategy ?? 'performance',
applicationMaxAge: config.applicationMaxAge
? parseDurationToMs(config.applicationMaxAge)
: undefined,
};
}
+1
View File
@@ -28,6 +28,7 @@ export interface Config {
dataGroups?: DataGroup[];
navigationUrls?: string[];
navigationRequestStrategy?: 'freshness' | 'performance';
applicationMaxAge?: Duration;
}
/**
@@ -58,6 +58,7 @@ describe('Generator', () => {
'http://example.com/included',
'!http://example.com/excluded',
],
applicationMaxAge: '1d',
});
expect(config).toEqual({
@@ -115,6 +116,7 @@ describe('Generator', () => {
{positive: false, regex: '^http:\\/\\/example\\.com\\/excluded$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: 86400000,
hashTable: {
'/test/foo/test.html': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643',
'/test/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
@@ -208,6 +210,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
});
});
@@ -234,6 +237,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
hashTable: {},
});
});
@@ -425,6 +429,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
hashTable: {},
});
});
@@ -498,6 +503,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
hashTable: {
'/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
'/main.js': '41347a66676cdc0516934c76d9d13010df420f2c',
+5 -1
View File
@@ -497,10 +497,14 @@ export class Driver implements Debuggable, UpdateSource {
// Decide which version of the app to use to serve this request. This is asynchronous as in
// some cases, a record will need to be written to disk about the assignment that is made.
const appVersion = await this.assignVersion(event);
// If there's a configured max age, check whether this version is within that age.
const isVersionWithinMaxAge =
appVersion?.manifest.applicationMaxAge === undefined ||
this.adapter.time - appVersion.manifest.timestamp < appVersion.manifest.applicationMaxAge;
let res: Response | null = null;
try {
if (appVersion !== null) {
if (appVersion !== null && isVersionWithinMaxAge) {
try {
// Handle the request. First try the AppVersion. If that doesn't work, fall back on the
// network.
@@ -19,6 +19,7 @@ export interface Manifest {
dataGroups?: DataGroupConfig[];
navigationUrls: {positive: boolean; regex: string}[];
navigationRequestStrategy: 'freshness' | 'performance';
applicationMaxAge?: number;
hashTable: {[url: string]: string};
}
@@ -2562,6 +2562,70 @@ import {envIsSupported} from '../testing/utils';
return {server, scope, driver};
}
});
describe('applicationMaxAge', () => {
// When within the `applicationMaxAge`, the app should act like `performance` mode
// When outside of it, it should act like `freshness` mode, except it also uncaches asset
// requests
it("doesn't create navigate requests within the maxAge", async () => {
const {server, scope, driver} = createSwForMaxAge();
await makeRequest(scope, '/foo.txt');
await driver.initialized;
await server.clearRequests();
// Create multiple requests to prove no navigation OR asset requests were made.
// By default the navigation request is not sent, it's replaced
// with the index request - thus, the `this is foo` value.
expect(await makeNavigationRequest(scope, '/', '')).toBe('this is foo');
expect(await makeNavigationRequest(scope, '/foo', '')).toBe('this is foo');
expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo');
expect(await makeRequest(scope, '/bar.txt')).toBe('this is bar');
server.assertNoOtherRequests();
});
it('creates navigate requests outside the maxAge', async () => {
const {server, scope, driver} = createSwForMaxAge();
await makeRequest(scope, '/foo.txt');
await driver.initialized;
await server.clearRequests();
await scope.advance(3000);
// Create multiple requests to prove the navigation and asset requests are all made
// When enabled, the navigation request is made each time and not replaced
// with the index request - thus, the `null` value.
expect(await makeNavigationRequest(scope, '/', '')).toBe(null);
expect(await makeNavigationRequest(scope, '/foo', '')).toBe(null);
expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo');
expect(await makeRequest(scope, '/bar.txt')).toBe('this is bar');
server.assertSawRequestFor('/');
server.assertSawRequestFor('/foo');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertNoOtherRequests();
});
function createSwForMaxAge() {
const scope = new SwTestHarnessBuilder().build();
// set the timestamp of the manifest using the server time so it's always "new" on test start
const maxAgeManifest: Manifest = {
...manifest,
timestamp: scope.time,
applicationMaxAge: 2000,
};
const server = serverBuilderBase.withManifest(maxAgeManifest).build();
const driver = new Driver(scope, scope, new CacheDatabase(scope));
scope.updateServerState(server);
return {server, scope, driver};
}
});
});
})();