PrecacheController.ts
12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {assert} from 'workbox-core/_private/assert.js';
import {cacheNames} from 'workbox-core/_private/cacheNames.js';
import {logger} from 'workbox-core/_private/logger.js';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {waitUntil} from 'workbox-core/_private/waitUntil.js';
import {Strategy} from 'workbox-strategies/Strategy.js';
import {RouteHandlerCallback, WorkboxPlugin} from 'workbox-core/types.js';
import {createCacheKey} from './utils/createCacheKey.js';
import {PrecacheInstallReportPlugin} from './utils/PrecacheInstallReportPlugin.js';
import {PrecacheCacheKeyPlugin} from './utils/PrecacheCacheKeyPlugin.js';
import {printCleanupDetails} from './utils/printCleanupDetails.js';
import {printInstallDetails} from './utils/printInstallDetails.js';
import {PrecacheStrategy} from './PrecacheStrategy.js';
import {PrecacheEntry, InstallResult, CleanupResult} from './_types.js';
import './_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
declare global {
interface ServiceWorkerGlobalScope {
__WB_MANIFEST: Array<PrecacheEntry | string>;
}
}
interface PrecacheControllerOptions {
cacheName?: string;
plugins?: WorkboxPlugin[];
fallbackToNetwork?: boolean;
}
/**
* Performs efficient precaching of assets.
*
* @memberof workbox-precaching
*/
class PrecacheController {
private _installAndActiveListenersAdded?: boolean;
private readonly _strategy: Strategy;
private readonly _urlsToCacheKeys: Map<string, string> = new Map();
private readonly _urlsToCacheModes: Map<
string,
| 'reload'
| 'default'
| 'no-store'
| 'no-cache'
| 'force-cache'
| 'only-if-cached'
> = new Map();
private readonly _cacheKeysToIntegrities: Map<string, string> = new Map();
/**
* Create a new PrecacheController.
*
* @param {Object} [options]
* @param {string} [options.cacheName] The cache to use for precaching.
* @param {string} [options.plugins] Plugins to use when precaching as well
* as responding to fetch events for precached assets.
* @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to
* get the response from the network if there's a precache miss.
*/
constructor({
cacheName,
plugins = [],
fallbackToNetwork = true,
}: PrecacheControllerOptions = {}) {
this._strategy = new PrecacheStrategy({
cacheName: cacheNames.getPrecacheName(cacheName),
plugins: [
...plugins,
new PrecacheCacheKeyPlugin({precacheController: this}),
],
fallbackToNetwork,
});
// Bind the install and activate methods to the instance.
this.install = this.install.bind(this);
this.activate = this.activate.bind(this);
}
/**
* @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and
* used to cache assets and respond to fetch events.
*/
get strategy(): Strategy {
return this._strategy;
}
/**
* Adds items to the precache list, removing any duplicates and
* stores the files in the
* {@link workbox-core.cacheNames|"precache cache"} when the service
* worker installs.
*
* This method can be called multiple times.
*
* @param {Array<Object|string>} [entries=[]] Array of entries to precache.
*/
precache(entries: Array<PrecacheEntry | string>): void {
this.addToCacheList(entries);
if (!this._installAndActiveListenersAdded) {
self.addEventListener('install', this.install);
self.addEventListener('activate', this.activate);
this._installAndActiveListenersAdded = true;
}
}
/**
* This method will add items to the precache list, removing duplicates
* and ensuring the information is valid.
*
* @param {Array<workbox-precaching.PrecacheController.PrecacheEntry|string>} entries
* Array of entries to precache.
*/
addToCacheList(entries: Array<PrecacheEntry | string>): void {
if (process.env.NODE_ENV !== 'production') {
assert!.isArray(entries, {
moduleName: 'workbox-precaching',
className: 'PrecacheController',
funcName: 'addToCacheList',
paramName: 'entries',
});
}
const urlsToWarnAbout: string[] = [];
for (const entry of entries) {
// See https://github.com/GoogleChrome/workbox/issues/2259
if (typeof entry === 'string') {
urlsToWarnAbout.push(entry);
} else if (entry && entry.revision === undefined) {
urlsToWarnAbout.push(entry.url);
}
const {cacheKey, url} = createCacheKey(entry);
const cacheMode =
typeof entry !== 'string' && entry.revision ? 'reload' : 'default';
if (
this._urlsToCacheKeys.has(url) &&
this._urlsToCacheKeys.get(url) !== cacheKey
) {
throw new WorkboxError('add-to-cache-list-conflicting-entries', {
firstEntry: this._urlsToCacheKeys.get(url),
secondEntry: cacheKey,
});
}
if (typeof entry !== 'string' && entry.integrity) {
if (
this._cacheKeysToIntegrities.has(cacheKey) &&
this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity
) {
throw new WorkboxError('add-to-cache-list-conflicting-integrities', {
url,
});
}
this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
}
this._urlsToCacheKeys.set(url, cacheKey);
this._urlsToCacheModes.set(url, cacheMode);
if (urlsToWarnAbout.length > 0) {
const warningMessage =
`Workbox is precaching URLs without revision ` +
`info: ${urlsToWarnAbout.join(', ')}\nThis is generally NOT safe. ` +
`Learn more at https://bit.ly/wb-precache`;
if (process.env.NODE_ENV === 'production') {
// Use console directly to display this warning without bloating
// bundle sizes by pulling in all of the logger codebase in prod.
console.warn(warningMessage);
} else {
logger.warn(warningMessage);
}
}
}
}
/**
* Precaches new and updated assets. Call this method from the service worker
* install event.
*
* Note: this method calls `event.waitUntil()` for you, so you do not need
* to call it yourself in your event handlers.
*
* @param {ExtendableEvent} event
* @return {Promise<workbox-precaching.InstallResult>}
*/
install(event: ExtendableEvent): Promise<InstallResult> {
// waitUntil returns Promise<any>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return waitUntil(event, async () => {
const installReportPlugin = new PrecacheInstallReportPlugin();
this.strategy.plugins.push(installReportPlugin);
// Cache entries one at a time.
// See https://github.com/GoogleChrome/workbox/issues/2528
for (const [url, cacheKey] of this._urlsToCacheKeys) {
const integrity = this._cacheKeysToIntegrities.get(cacheKey);
const cacheMode = this._urlsToCacheModes.get(url);
const request = new Request(url, {
integrity,
cache: cacheMode,
credentials: 'same-origin',
});
await Promise.all(
this.strategy.handleAll({
params: {cacheKey},
request,
event,
}),
);
}
const {updatedURLs, notUpdatedURLs} = installReportPlugin;
if (process.env.NODE_ENV !== 'production') {
printInstallDetails(updatedURLs, notUpdatedURLs);
}
return {updatedURLs, notUpdatedURLs};
});
}
/**
* Deletes assets that are no longer present in the current precache manifest.
* Call this method from the service worker activate event.
*
* Note: this method calls `event.waitUntil()` for you, so you do not need
* to call it yourself in your event handlers.
*
* @param {ExtendableEvent} event
* @return {Promise<workbox-precaching.CleanupResult>}
*/
activate(event: ExtendableEvent): Promise<CleanupResult> {
// waitUntil returns Promise<any>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return waitUntil(event, async () => {
const cache = await self.caches.open(this.strategy.cacheName);
const currentlyCachedRequests = await cache.keys();
const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
const deletedURLs = [];
for (const request of currentlyCachedRequests) {
if (!expectedCacheKeys.has(request.url)) {
await cache.delete(request);
deletedURLs.push(request.url);
}
}
if (process.env.NODE_ENV !== 'production') {
printCleanupDetails(deletedURLs);
}
return {deletedURLs};
});
}
/**
* Returns a mapping of a precached URL to the corresponding cache key, taking
* into account the revision information for the URL.
*
* @return {Map<string, string>} A URL to cache key mapping.
*/
getURLsToCacheKeys(): Map<string, string> {
return this._urlsToCacheKeys;
}
/**
* Returns a list of all the URLs that have been precached by the current
* service worker.
*
* @return {Array<string>} The precached URLs.
*/
getCachedURLs(): Array<string> {
return [...this._urlsToCacheKeys.keys()];
}
/**
* Returns the cache key used for storing a given URL. If that URL is
* unversioned, like `/index.html', then the cache key will be the original
* URL with a search parameter appended to it.
*
* @param {string} url A URL whose cache key you want to look up.
* @return {string} The versioned URL that corresponds to a cache key
* for the original URL, or undefined if that URL isn't precached.
*/
getCacheKeyForURL(url: string): string | undefined {
const urlObject = new URL(url, location.href);
return this._urlsToCacheKeys.get(urlObject.href);
}
/**
* @param {string} url A cache key whose SRI you want to look up.
* @return {string} The subresource integrity associated with the cache key,
* or undefined if it's not set.
*/
getIntegrityForCacheKey(cacheKey: string): string | undefined {
return this._cacheKeysToIntegrities.get(cacheKey);
}
/**
* This acts as a drop-in replacement for
* [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match)
* with the following differences:
*
* - It knows what the name of the precache is, and only checks in that cache.
* - It allows you to pass in an "original" URL without versioning parameters,
* and it will automatically look up the correct cache key for the currently
* active revision of that URL.
*
* E.g., `matchPrecache('index.html')` will find the correct precached
* response for the currently active service worker, even if the actual cache
* key is `'/index.html?__WB_REVISION__=1234abcd'`.
*
* @param {string|Request} request The key (without revisioning parameters)
* to look up in the precache.
* @return {Promise<Response|undefined>}
*/
async matchPrecache(
request: string | Request,
): Promise<Response | undefined> {
const url = request instanceof Request ? request.url : request;
const cacheKey = this.getCacheKeyForURL(url);
if (cacheKey) {
const cache = await self.caches.open(this.strategy.cacheName);
return cache.match(cacheKey);
}
return undefined;
}
/**
* Returns a function that looks up `url` in the precache (taking into
* account revision information), and returns the corresponding `Response`.
*
* @param {string} url The precached URL which will be used to lookup the
* `Response`.
* @return {workbox-routing~handlerCallback}
*/
createHandlerBoundToURL(url: string): RouteHandlerCallback {
const cacheKey = this.getCacheKeyForURL(url);
if (!cacheKey) {
throw new WorkboxError('non-precached-url', {url});
}
return (options) => {
options.request = new Request(url);
options.params = {cacheKey, ...options.params};
return this.strategy.handle(options);
};
}
}
export {PrecacheController};