Skip to content

Commit

Permalink
feat(service-worker): include Cache#match options in ngsw-config
Browse files Browse the repository at this point in the history
Previously it was impossible to provide options for Cache#match. As it
is sometimes necessary to override the default behavior the Cache#match
options can now be configured for assets and data groups.

closes angular#28443
  • Loading branch information
spike-rabbit committed Jan 8, 2020
1 parent 2dffe65 commit 69e9711
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 8 deletions.
24 changes: 24 additions & 0 deletions aio/content/guide/service-worker-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ interface AssetGroup {
files?: string[];
urls?: string[];
};
cacheMatchOptions?: {
ignoreMethod?: boolean;
ignoreSearch?: boolean;
ignoreVary?: boolean;
};
}
```

Expand Down Expand Up @@ -110,6 +115,16 @@ This section describes the resources to cache, broken up into the following grou
* `urls` includes both URLs and URL patterns that will be matched at runtime. These resources are not fetched directly and do not have content hashes, but they will be cached according to their HTTP headers. This is most useful for CDNs such as the Google Fonts service.<br>
_(Negative glob patterns are not supported and `?` will be matched literally; i.e. it will not match any character other than `?`.)_

### `cacheMatchOptions`

These options are used to modify that matching behavior of requests.

* `ignoreSearch` ignore query parameters: Default: false

* `ignoreMethod` prevents validating of http method. Default false

* `ignoreVary` do not perform VARY header matching. Default false

## `dataGroups`

Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies.
Expand All @@ -127,6 +142,11 @@ export interface DataGroup {
timeout?: string;
strategy?: 'freshness' | 'performance';
};
cacheMatchOptions?: {
ignoreMethod?: boolean;
ignoreSearch?: boolean;
ignoreVary?: boolean;
};
}
```

Expand Down Expand Up @@ -181,6 +201,10 @@ The Angular service worker can use either of two caching strategies for data res

* `freshness` optimizes for currency of data, preferentially fetching requested data from the network. Only if the network times out, according to `timeout`, does the request fall back to the cache. This is useful for resources that change frequently; for example, account balances.

### `cacheMatchOptions`

See [assetGroups](#assetgroups) for details.

## `navigationUrls`

This optional section enables you to specify a custom list of URLs that will be redirected to the index file.
Expand Down
30 changes: 30 additions & 0 deletions packages/service-worker/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@
}
},
"additionalProperties": false
},
"cacheMatchOptions" : {
"type": "object",
"description": "Provide options that are passed to Cache#match",
"properties" : {
"ignoreMethod": {
"type": "boolean"
},
"ignoreSearch": {
"type": "boolean"
},
"ignoreVary": {
"type": "boolean"
}
}
}
},
"required": [
Expand Down Expand Up @@ -122,6 +137,21 @@
"maxAge"
],
"additionalProperties": false
},
"cacheMatchOptions" : {
"type": "object",
"description": "Provide options that are passed to Cache#match",
"properties" : {
"ignoreMethod": {
"type": "boolean"
},
"ignoreSearch": {
"type": "boolean"
},
"ignoreVary": {
"type": "boolean"
}
}
}
},
"required": [
Expand Down
2 changes: 2 additions & 0 deletions packages/service-worker/config/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class Generator {
name: group.name,
installMode: group.installMode || 'prefetch',
updateMode: group.updateMode || group.installMode || 'prefetch',
cacheMatchOptions: group.cacheMatchOptions,
urls: matchedFiles.map(url => joinUrls(this.baseHref, url)),
patterns: (group.resources.urls || []).map(url => urlToRegex(url, this.baseHref, true)),
};
Expand All @@ -81,6 +82,7 @@ export class Generator {
patterns: group.urls.map(url => urlToRegex(url, this.baseHref, true)),
strategy: group.cacheConfig.strategy || 'performance',
maxSize: group.cacheConfig.maxSize,
cacheMatchOptions: group.cacheMatchOptions,
maxAge: parseDurationToMs(group.cacheConfig.maxAge),
timeoutMs: group.cacheConfig.timeout && parseDurationToMs(group.cacheConfig.timeout),
version: group.version !== undefined ? group.version : 1,
Expand Down
13 changes: 13 additions & 0 deletions packages/service-worker/config/src/in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface AssetGroup {
installMode?: 'prefetch'|'lazy';
updateMode?: 'prefetch'|'lazy';
resources: {files?: Glob[]; urls?: Glob[];};
cacheMatchOptions?: CacheQueryOptions;
}

/**
Expand All @@ -53,4 +54,16 @@ export interface DataGroup {
cacheConfig: {
maxSize: number; maxAge: Duration; timeout?: Duration; strategy?: 'freshness' | 'performance';
};
cacheMatchOptions?: CacheQueryOptions;
}

/**
* Configuration for Cache#match. (copied from lib DOM)
*
* @publicApi
*/
export interface CacheQueryOptions {
ignoreMethod?: boolean;
ignoreSearch?: boolean;
ignoreVary?: boolean;
}
13 changes: 7 additions & 6 deletions packages/service-worker/worker/src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export abstract class AssetGroup {
async cacheStatus(url: string): Promise<UpdateCacheStatus> {
const cache = await this.cache;
const meta = await this.metadata;
const res = await cache.match(this.adapter.newRequest(url));
const res = await cache.match(this.adapter.newRequest(url), this.config.cacheMatchOptions);
if (res === undefined) {
return UpdateCacheStatus.NOT_CACHED;
}
Expand Down Expand Up @@ -120,7 +120,7 @@ export abstract class AssetGroup {

// Look for a cached response. If one exists, it can be used to resolve the fetch
// operation.
const cachedResponse = await cache.match(req);
const cachedResponse = await cache.match(req, this.config.cacheMatchOptions);
if (cachedResponse !== undefined) {
// A response has already been cached (which presumably matches the hash for this
// resource). Check whether it's safe to serve this resource from cache.
Expand Down Expand Up @@ -252,7 +252,7 @@ export abstract class AssetGroup {
const metaTable = await this.metadata;

// Lookup the response in the cache.
const response = await cache.match(this.adapter.newRequest(url));
const response = await cache.match(this.adapter.newRequest(url), this.config.cacheMatchOptions);
if (response === undefined) {
// It's not found, return null.
return null;
Expand Down Expand Up @@ -514,7 +514,7 @@ export class PrefetchAssetGroup extends AssetGroup {
const req = this.adapter.newRequest(url);

// First, check the cache to see if there is already a copy of this resource.
const alreadyCached = (await cache.match(req)) !== undefined;
const alreadyCached = (await cache.match(req, this.config.cacheMatchOptions)) !== undefined;

// If the resource is in the cache already, it can be skipped.
if (alreadyCached) {
Expand Down Expand Up @@ -550,7 +550,8 @@ export class PrefetchAssetGroup extends AssetGroup {

// It's possible that the resource in question is already cached. If so,
// continue to the next one.
const alreadyCached = (await cache.match(req) !== undefined);
const alreadyCached =
(await cache.match(req, this.config.cacheMatchOptions) !== undefined);
if (alreadyCached) {
return;
}
Expand Down Expand Up @@ -591,7 +592,7 @@ export class LazyAssetGroup extends AssetGroup {
const req = this.adapter.newRequest(url);

// First, check the cache to see if there is already a copy of this resource.
const alreadyCached = (await cache.match(req)) !== undefined;
const alreadyCached = (await cache.match(req, this.config.cacheMatchOptions)) !== undefined;

// If the resource is in the cache already, it can be skipped.
if (alreadyCached) {
Expand Down
2 changes: 1 addition & 1 deletion packages/service-worker/worker/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ export class DataGroup {
Promise<{res: Response, age: number}|null> {
// Look for a response in the cache. If one exists, return it.
const cache = await this.cache;
let res = await cache.match(req);
let res = await cache.match(req, this.config.cacheMatchOptions);
if (res !== undefined) {
// A response was found in the cache, but its age is not yet known. Look it up.
try {
Expand Down
2 changes: 2 additions & 0 deletions packages/service-worker/worker/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface AssetGroupConfig {
updateMode: 'prefetch'|'lazy';
urls: string[];
patterns: string[];
cacheMatchOptions?: CacheQueryOptions;
}

export interface DataGroupConfig {
Expand All @@ -38,6 +39,7 @@ export interface DataGroupConfig {
timeoutMs?: number;
refreshAheadMs?: number;
maxAge: number;
cacheMatchOptions?: CacheQueryOptions;
}

export function hashManifest(manifest: Manifest): ManifestHash {
Expand Down
11 changes: 10 additions & 1 deletion packages/service-worker/worker/test/prefetch_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
}

const dist = new MockFileSystemBuilder()
.addFile('/foo.txt', 'this is foo')
.addFile('/foo.txt', 'this is foo', {Vary: 'Accept'})
.addFile('/bar.txt', 'this is bar')
.build();

Expand Down Expand Up @@ -83,6 +83,15 @@ import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
const err = await errorFrom(group.initializeFully());
expect(err.message).toContain('Hash mismatch');
});
it('respond with a resource even if a header listed in vary is present', async() => {
await group.initializeFully();
scope.updateServerState();
server.clearRequests();
const res1 = await group.handleFetch(
scope.newRequest('/foo.txt', {headers: {Accept: 'text/plain'}}), scope);
expect(await res1 !.text()).toEqual('this is foo');
server.assertNoOtherRequests();
});
});
})();

Expand Down
13 changes: 13 additions & 0 deletions packages/service-worker/worker/testing/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ export class MockCache {
// TODO: cleanup typings. Typescript doesn't know this can resolve to undefined.
let res = this.cache.get(url);
if (res !== undefined) {
let varyHeaders = res.headers.get('Vary');
if (varyHeaders && (!options || !options.ignoreVary)) {
if (typeof request === 'string') {
return null !;
}
for (let varyHeader of varyHeaders.split(/, ?/)) {
let requestVary = request.headers.get(varyHeader);
let resVary = res.headers.get(varyHeader);
if (requestVary !== resVary) {
return null !;
}
}
}
res = res.clone();
}
return res !;
Expand Down
1 change: 1 addition & 0 deletions packages/service-worker/worker/testing/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
updateMode: 'prefetch',
urls: files,
patterns: [],
cacheMatchOptions: {ignoreVary: true}
},
],
navigationUrls: [], hashTable,
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/service-worker/config.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface AssetGroup {
cacheMatchOptions?: CacheQueryOptions;
installMode?: 'prefetch' | 'lazy';
name: string;
resources: {
Expand All @@ -23,6 +24,7 @@ export interface DataGroup {
timeout?: Duration;
strategy?: 'freshness' | 'performance';
};
cacheMatchOptions?: CacheQueryOptions;
name: string;
urls: Glob[];
version?: number;
Expand Down

0 comments on commit 69e9711

Please sign in to comment.