diff --git a/adev/src/content/ecosystem/service-workers/config.md b/adev/src/content/ecosystem/service-workers/config.md
index ea752174c25..a4d16eea298 100644
--- a/adev/src/content/ecosystem/service-workers/config.md
+++ b/adev/src/content/ecosystem/service-workers/config.md
@@ -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:
- Redirecting to an authentication website when authentication is not handled by the application
- Redirecting specific URLs to avoid breaking existing links/bookmarks after a website redesign
- Redirecting to a different website, such as a server-status page, while a page is temporarily down
|
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.
diff --git a/goldens/public-api/service-worker/config/index.api.md b/goldens/public-api/service-worker/config/index.api.md
index 79429bdfbd9..43ae3d67632 100644
--- a/goldens/public-api/service-worker/config/index.api.md
+++ b/goldens/public-api/service-worker/config/index.api.md
@@ -26,6 +26,8 @@ export interface Config {
// (undocumented)
appData?: {};
// (undocumented)
+ applicationMaxAge?: Duration;
+ // (undocumented)
assetGroups?: AssetGroup[];
// (undocumented)
dataGroups?: DataGroup[];
diff --git a/packages/service-worker/config/schema.json b/packages/service-worker/config/schema.json
index fb1ab0cf368..d9c62082559 100644
--- a/packages/service-worker/config/schema.json
+++ b/packages/service-worker/config/schema.json
@@ -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": [
diff --git a/packages/service-worker/config/src/generator.ts b/packages/service-worker/config/src/generator.ts
index 8382eaa9632..9d2659a3d73 100644
--- a/packages/service-worker/config/src/generator.ts
+++ b/packages/service-worker/config/src/generator.ts
@@ -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,
};
}
diff --git a/packages/service-worker/config/src/in.ts b/packages/service-worker/config/src/in.ts
index 76d877e2912..22f9763f88f 100644
--- a/packages/service-worker/config/src/in.ts
+++ b/packages/service-worker/config/src/in.ts
@@ -28,6 +28,7 @@ export interface Config {
dataGroups?: DataGroup[];
navigationUrls?: string[];
navigationRequestStrategy?: 'freshness' | 'performance';
+ applicationMaxAge?: Duration;
}
/**
diff --git a/packages/service-worker/config/test/generator_spec.ts b/packages/service-worker/config/test/generator_spec.ts
index ef146471550..cb679a28ae9 100644
--- a/packages/service-worker/config/test/generator_spec.ts
+++ b/packages/service-worker/config/test/generator_spec.ts
@@ -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',
diff --git a/packages/service-worker/worker/src/driver.ts b/packages/service-worker/worker/src/driver.ts
index 84a15775943..d7d2b635926 100644
--- a/packages/service-worker/worker/src/driver.ts
+++ b/packages/service-worker/worker/src/driver.ts
@@ -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.
diff --git a/packages/service-worker/worker/src/manifest.ts b/packages/service-worker/worker/src/manifest.ts
index 0d35d477ecd..9ed2a6e25ba 100644
--- a/packages/service-worker/worker/src/manifest.ts
+++ b/packages/service-worker/worker/src/manifest.ts
@@ -19,6 +19,7 @@ export interface Manifest {
dataGroups?: DataGroupConfig[];
navigationUrls: {positive: boolean; regex: string}[];
navigationRequestStrategy: 'freshness' | 'performance';
+ applicationMaxAge?: number;
hashTable: {[url: string]: string};
}
diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts
index 5fc6f2d05b5..8f9c64f6220 100644
--- a/packages/service-worker/worker/test/happy_spec.ts
+++ b/packages/service-worker/worker/test/happy_spec.ts
@@ -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};
+ }
+ });
});
})();