Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion adev/src/content/ecosystem/service-workers/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,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

Expand Down Expand Up @@ -374,3 +374,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.
2 changes: 2 additions & 0 deletions goldens/public-api/service-worker/config/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface Config {
// (undocumented)
appData?: {};
// (undocumented)
applicationMaxAge?: Duration;
// (undocumented)
assetGroups?: AssetGroup[];
// (undocumented)
dataGroups?: DataGroup[];
Expand Down
4 changes: 4 additions & 0 deletions packages/service-worker/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,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": [
Expand Down
3 changes: 3 additions & 0 deletions packages/service-worker/config/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/service-worker/config/src/in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Config {
dataGroups?: DataGroup[];
navigationUrls?: string[];
navigationRequestStrategy?: 'freshness' | 'performance';
applicationMaxAge?: Duration;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/service-worker/config/test/generator_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('Generator', () => {
'http://example.com/included',
'!http://example.com/excluded',
],
applicationMaxAge: '1d',
});

expect(config).toEqual({
Expand Down Expand Up @@ -114,6 +115,7 @@ describe('Generator', () => {
{positive: false, regex: '^http:\\/\\/example\\.com\\/excluded$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: 86400000,
hashTable: {
'/test/foo/test.html': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643',
'/test/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
Expand Down Expand Up @@ -207,6 +209,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
});
});

Expand All @@ -233,6 +236,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
hashTable: {},
});
});
Expand Down Expand Up @@ -418,6 +422,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
hashTable: {},
});
});
Expand Down Expand Up @@ -489,6 +494,7 @@ describe('Generator', () => {
{positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$'},
],
navigationRequestStrategy: 'performance',
applicationMaxAge: undefined,
hashTable: {
'/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
'/main.js': '41347a66676cdc0516934c76d9d13010df420f2c',
Expand Down
6 changes: 5 additions & 1 deletion packages/service-worker/worker/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/service-worker/worker/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Manifest {
dataGroups?: DataGroupConfig[];
navigationUrls: {positive: boolean; regex: string}[];
navigationRequestStrategy: 'freshness' | 'performance';
applicationMaxAge?: number;
hashTable: {[url: string]: string};
}

Expand Down
64 changes: 64 additions & 0 deletions packages/service-worker/worker/test/happy_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
});
});
})();

Expand Down