· 6 years ago · Apr 06, 2019, 07:24 AM
1(function () {
2 'use strict';
3
4 /**
5 * @license
6 * Copyright Google Inc. All Rights Reserved.
7 *
8 * Use of this source code is governed by an MIT-style license that can be
9 * found in the LICENSE file at https://angular.io/license
10 */
11 /**
12 * Adapts the service worker to its runtime environment.
13 *
14 * Mostly, this is used to mock out identifiers which are otherwise read
15 * from the global scope.
16 */
17 class Adapter {
18 /**
19 * Wrapper around the `Request` constructor.
20 */
21 newRequest(input, init) {
22 return new Request(input, init);
23 }
24 /**
25 * Wrapper around the `Response` constructor.
26 */
27 newResponse(body, init) { return new Response(body, init); }
28 /**
29 * Wrapper around the `Headers` constructor.
30 */
31 newHeaders(headers) { return new Headers(headers); }
32 /**
33 * Test if a given object is an instance of `Client`.
34 */
35 isClient(source) { return (source instanceof Client); }
36 /**
37 * Read the current UNIX time in milliseconds.
38 */
39 get time() { return Date.now(); }
40 /**
41 * Extract the pathname of a URL.
42 */
43 parseUrl(url, relativeTo) {
44 const parsed = new URL(url, relativeTo);
45 return { origin: parsed.origin, path: parsed.pathname };
46 }
47 /**
48 * Wait for a given amount of time before completing a Promise.
49 */
50 timeout(ms) {
51 return new Promise(resolve => { setTimeout(() => resolve(), ms); });
52 }
53 }
54
55 /**
56 * @license
57 * Copyright Google Inc. All Rights Reserved.
58 *
59 * Use of this source code is governed by an MIT-style license that can be
60 * found in the LICENSE file at https://angular.io/license
61 */
62 /**
63 * An error returned in rejected promises if the given key is not found in the table.
64 */
65 class NotFound {
66 constructor(table, key) {
67 this.table = table;
68 this.key = key;
69 }
70 }
71
72 /**
73 * @license
74 * Copyright Google Inc. All Rights Reserved.
75 *
76 * Use of this source code is governed by an MIT-style license that can be
77 * found in the LICENSE file at https://angular.io/license
78 */
79 /**
80 * An implementation of a `Database` that uses the `CacheStorage` API to serialize
81 * state within mock `Response` objects.
82 */
83 class CacheDatabase {
84 constructor(scope, adapter) {
85 this.scope = scope;
86 this.adapter = adapter;
87 this.tables = new Map();
88 }
89 'delete'(name) {
90 if (this.tables.has(name)) {
91 this.tables.delete(name);
92 }
93 return this.scope.caches.delete(`ngsw:db:${name}`);
94 }
95 list() {
96 return this.scope.caches.keys().then(keys => keys.filter(key => key.startsWith('ngsw:db:')));
97 }
98 open(name) {
99 if (!this.tables.has(name)) {
100 const table = this.scope.caches.open(`ngsw:db:${name}`)
101 .then(cache => new CacheTable(name, cache, this.adapter));
102 this.tables.set(name, table);
103 }
104 return this.tables.get(name);
105 }
106 }
107 /**
108 * A `Table` backed by a `Cache`.
109 */
110 class CacheTable {
111 constructor(table, cache, adapter) {
112 this.table = table;
113 this.cache = cache;
114 this.adapter = adapter;
115 }
116 request(key) { return this.adapter.newRequest('/' + key); }
117 'delete'(key) { return this.cache.delete(this.request(key)); }
118 keys() {
119 return this.cache.keys().then(requests => requests.map(req => req.url.substr(1)));
120 }
121 read(key) {
122 return this.cache.match(this.request(key)).then(res => {
123 if (res === undefined) {
124 return Promise.reject(new NotFound(this.table, key));
125 }
126 return res.json();
127 });
128 }
129 write(key, value) {
130 return this.cache.put(this.request(key), this.adapter.newResponse(JSON.stringify(value)));
131 }
132 }
133
134 /**
135 * @license
136 * Copyright Google Inc. All Rights Reserved.
137 *
138 * Use of this source code is governed by an MIT-style license that can be
139 * found in the LICENSE file at https://angular.io/license
140 */
141 var UpdateCacheStatus;
142 (function (UpdateCacheStatus) {
143 UpdateCacheStatus[UpdateCacheStatus["NOT_CACHED"] = 0] = "NOT_CACHED";
144 UpdateCacheStatus[UpdateCacheStatus["CACHED_BUT_UNUSED"] = 1] = "CACHED_BUT_UNUSED";
145 UpdateCacheStatus[UpdateCacheStatus["CACHED"] = 2] = "CACHED";
146 })(UpdateCacheStatus || (UpdateCacheStatus = {}));
147
148 /**
149 * @license
150 * Copyright Google Inc. All Rights Reserved.
151 *
152 * Use of this source code is governed by an MIT-style license that can be
153 * found in the LICENSE file at https://angular.io/license
154 */
155 class SwCriticalError extends Error {
156 constructor() {
157 super(...arguments);
158 this.isCritical = true;
159 }
160 }
161 function errorToString(error) {
162 if (error instanceof Error) {
163 return `${error.message}\n${error.stack}`;
164 }
165 else {
166 return `${error}`;
167 }
168 }
169
170 /**
171 * @license
172 * Copyright Google Inc. All Rights Reserved.
173 *
174 * Use of this source code is governed by an MIT-style license that can be
175 * found in the LICENSE file at https://angular.io/license
176 */
177 /**
178 * Compute the SHA1 of the given string
179 *
180 * see http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
181 *
182 * WARNING: this function has not been designed not tested with security in mind.
183 * DO NOT USE IT IN A SECURITY SENSITIVE CONTEXT.
184 *
185 * Borrowed from @angular/compiler/src/i18n/digest.ts
186 */
187 function sha1(str) {
188 const utf8 = str;
189 const words32 = stringToWords32(utf8, Endian.Big);
190 return _sha1(words32, utf8.length * 8);
191 }
192 function sha1Binary(buffer) {
193 const words32 = arrayBufferToWords32(buffer, Endian.Big);
194 return _sha1(words32, buffer.byteLength * 8);
195 }
196 function _sha1(words32, len) {
197 const w = new Array(80);
198 let [a, b, c, d, e] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
199 words32[len >> 5] |= 0x80 << (24 - len % 32);
200 words32[((len + 64 >> 9) << 4) + 15] = len;
201 for (let i = 0; i < words32.length; i += 16) {
202 const [h0, h1, h2, h3, h4] = [a, b, c, d, e];
203 for (let j = 0; j < 80; j++) {
204 if (j < 16) {
205 w[j] = words32[i + j];
206 }
207 else {
208 w[j] = rol32(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
209 }
210 const [f, k] = fk(j, b, c, d);
211 const temp = [rol32(a, 5), f, e, k, w[j]].reduce(add32);
212 [e, d, c, b, a] = [d, c, rol32(b, 30), a, temp];
213 }
214 [a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)];
215 }
216 return byteStringToHexString(words32ToByteString([a, b, c, d, e]));
217 }
218 function add32(a, b) {
219 return add32to64(a, b)[1];
220 }
221 function add32to64(a, b) {
222 const low = (a & 0xffff) + (b & 0xffff);
223 const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
224 return [high >>> 16, (high << 16) | (low & 0xffff)];
225 }
226 // Rotate a 32b number left `count` position
227 function rol32(a, count) {
228 return (a << count) | (a >>> (32 - count));
229 }
230 var Endian;
231 (function (Endian) {
232 Endian[Endian["Little"] = 0] = "Little";
233 Endian[Endian["Big"] = 1] = "Big";
234 })(Endian || (Endian = {}));
235 function fk(index, b, c, d) {
236 if (index < 20) {
237 return [(b & c) | (~b & d), 0x5a827999];
238 }
239 if (index < 40) {
240 return [b ^ c ^ d, 0x6ed9eba1];
241 }
242 if (index < 60) {
243 return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
244 }
245 return [b ^ c ^ d, 0xca62c1d6];
246 }
247 function stringToWords32(str, endian) {
248 const words32 = Array((str.length + 3) >>> 2);
249 for (let i = 0; i < words32.length; i++) {
250 words32[i] = wordAt(str, i * 4, endian);
251 }
252 return words32;
253 }
254 function arrayBufferToWords32(buffer, endian) {
255 const words32 = Array((buffer.byteLength + 3) >>> 2);
256 const view = new Uint8Array(buffer);
257 for (let i = 0; i < words32.length; i++) {
258 words32[i] = wordAt(view, i * 4, endian);
259 }
260 return words32;
261 }
262 function byteAt(str, index) {
263 if (typeof str === 'string') {
264 return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
265 }
266 else {
267 return index >= str.byteLength ? 0 : str[index] & 0xff;
268 }
269 }
270 function wordAt(str, index, endian) {
271 let word = 0;
272 if (endian === Endian.Big) {
273 for (let i = 0; i < 4; i++) {
274 word += byteAt(str, index + i) << (24 - 8 * i);
275 }
276 }
277 else {
278 for (let i = 0; i < 4; i++) {
279 word += byteAt(str, index + i) << 8 * i;
280 }
281 }
282 return word;
283 }
284 function words32ToByteString(words32) {
285 return words32.reduce((str, word) => str + word32ToByteString(word), '');
286 }
287 function word32ToByteString(word) {
288 let str = '';
289 for (let i = 0; i < 4; i++) {
290 str += String.fromCharCode((word >>> 8 * (3 - i)) & 0xff);
291 }
292 return str;
293 }
294 function byteStringToHexString(str) {
295 let hex = '';
296 for (let i = 0; i < str.length; i++) {
297 const b = byteAt(str, i);
298 hex += (b >>> 4).toString(16) + (b & 0x0f).toString(16);
299 }
300 return hex.toLowerCase();
301 }
302
303 /**
304 * @license
305 * Copyright Google Inc. All Rights Reserved.
306 *
307 * Use of this source code is governed by an MIT-style license that can be
308 * found in the LICENSE file at https://angular.io/license
309 */
310 var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
311 return new (P || (P = Promise))(function (resolve, reject) {
312 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
313 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
314 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
315 step((generator = generator.apply(thisArg, _arguments || [])).next());
316 });
317 };
318 /**
319 * A group of assets that are cached in a `Cache` and managed by a given policy.
320 *
321 * Concrete classes derive from this base and specify the exact caching policy.
322 */
323 class AssetGroup {
324 constructor(scope, adapter, idle, config, hashes, db, prefix) {
325 this.scope = scope;
326 this.adapter = adapter;
327 this.idle = idle;
328 this.config = config;
329 this.hashes = hashes;
330 this.db = db;
331 this.prefix = prefix;
332 /**
333 * A deduplication cache, to make sure the SW never makes two network requests
334 * for the same resource at once. Managed by `fetchAndCacheOnce`.
335 */
336 this.inFlightRequests = new Map();
337 /**
338 * Regular expression patterns.
339 */
340 this.patterns = [];
341 this.name = config.name;
342 // Patterns in the config are regular expressions disguised as strings. Breathe life into them.
343 this.patterns = this.config.patterns.map(pattern => new RegExp(pattern));
344 // This is the primary cache, which holds all of the cached requests for this group. If a
345 // resource
346 // isn't in this cache, it hasn't been fetched yet.
347 this.cache = this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
348 // This is the metadata table, which holds specific information for each cached URL, such as
349 // the timestamp of when it was added to the cache.
350 this.metadata = this.db.open(`${this.prefix}:${this.config.name}:meta`);
351 // Determine the origin from the registration scope. This is used to differentiate between
352 // relative and absolute URLs.
353 this.origin =
354 this.adapter.parseUrl(this.scope.registration.scope, this.scope.registration.scope).origin;
355 }
356 cacheStatus(url) {
357 return __awaiter(this, void 0, void 0, function* () {
358 const cache = yield this.cache;
359 const meta = yield this.metadata;
360 const res = yield cache.match(this.adapter.newRequest(url));
361 if (res === undefined) {
362 return UpdateCacheStatus.NOT_CACHED;
363 }
364 try {
365 const data = yield meta.read(url);
366 if (!data.used) {
367 return UpdateCacheStatus.CACHED_BUT_UNUSED;
368 }
369 }
370 catch (_) {
371 // Error on the side of safety and assume cached.
372 }
373 return UpdateCacheStatus.CACHED;
374 });
375 }
376 /**
377 * Clean up all the cached data for this group.
378 */
379 cleanup() {
380 return __awaiter(this, void 0, void 0, function* () {
381 yield this.scope.caches.delete(`${this.prefix}:${this.config.name}:cache`);
382 yield this.db.delete(`${this.prefix}:${this.config.name}:meta`);
383 });
384 }
385 /**
386 * Process a request for a given resource and return it, or return null if it's not available.
387 */
388 handleFetch(req, ctx) {
389 return __awaiter(this, void 0, void 0, function* () {
390 const url = this.getConfigUrl(req.url);
391 // Either the request matches one of the known resource URLs, one of the patterns for
392 // dynamically matched URLs, or neither. Determine which is the case for this request in
393 // order to decide how to handle it.
394 if (this.config.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url))) {
395 // This URL matches a known resource. Either it's been cached already or it's missing, in
396 // which case it needs to be loaded from the network.
397 // Open the cache to check whether this resource is present.
398 const cache = yield this.cache;
399 // Look for a cached response. If one exists, it can be used to resolve the fetch
400 // operation.
401 const cachedResponse = yield cache.match(req);
402 if (cachedResponse !== undefined) {
403 // A response has already been cached (which presumably matches the hash for this
404 // resource). Check whether it's safe to serve this resource from cache.
405 if (this.hashes.has(url)) {
406 // This resource has a hash, and thus is versioned by the manifest. It's safe to return
407 // the response.
408 return cachedResponse;
409 }
410 else {
411 // This resource has no hash, and yet exists in the cache. Check how old this request is
412 // to make sure it's still usable.
413 if (yield this.needToRevalidate(req, cachedResponse)) {
414 this.idle.schedule(`revalidate(${this.prefix}, ${this.config.name}): ${req.url}`, () => __awaiter(this, void 0, void 0, function* () { yield this.fetchAndCacheOnce(req); }));
415 }
416 // In either case (revalidation or not), the cached response must be good.
417 return cachedResponse;
418 }
419 }
420 // No already-cached response exists, so attempt a fetch/cache operation. The original request
421 // may specify things like credential inclusion, but for assets these are not honored in order
422 // to avoid issues with opaque responses. The SW requests the data itself.
423 const res = yield this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
424 // If this is successful, the response needs to be cloned as it might be used to respond to
425 // multiple fetch operations at the same time.
426 return res.clone();
427 }
428 else {
429 return null;
430 }
431 });
432 }
433 getConfigUrl(url) {
434 // If the URL is relative to the SW's own origin, then only consider the path relative to
435 // the domain root. Determine this by checking the URL's origin against the SW's.
436 const parsed = this.adapter.parseUrl(url, this.scope.registration.scope);
437 if (parsed.origin === this.origin) {
438 // The URL is relative to the SW's origin domain.
439 return parsed.path;
440 }
441 else {
442 return url;
443 }
444 }
445 /**
446 * Some resources are cached without a hash, meaning that their expiration is controlled
447 * by HTTP caching headers. Check whether the given request/response pair is still valid
448 * per the caching headers.
449 */
450 needToRevalidate(req, res) {
451 return __awaiter(this, void 0, void 0, function* () {
452 // Three different strategies apply here:
453 // 1) The request has a Cache-Control header, and thus expiration needs to be based on its age.
454 // 2) The request has an Expires header, and expiration is based on the current timestamp.
455 // 3) The request has no applicable caching headers, and must be revalidated.
456 if (res.headers.has('Cache-Control')) {
457 // Figure out if there is a max-age directive in the Cache-Control header.
458 const cacheControl = res.headers.get('Cache-Control');
459 const cacheDirectives = cacheControl
460 // Directives are comma-separated within the Cache-Control header value.
461 .split(',')
462 // Make sure each directive doesn't have extraneous whitespace.
463 .map(v => v.trim())
464 // Some directives have values (like maxage and s-maxage)
465 .map(v => v.split('='));
466 // Lowercase all the directive names.
467 cacheDirectives.forEach(v => v[0] = v[0].toLowerCase());
468 // Find the max-age directive, if one exists.
469 const maxAgeDirective = cacheDirectives.find(v => v[0] === 'max-age');
470 const cacheAge = maxAgeDirective ? maxAgeDirective[1] : undefined;
471 if (!cacheAge) {
472 // No usable TTL defined. Must assume that the response is stale.
473 return true;
474 }
475 try {
476 const maxAge = 1000 * parseInt(cacheAge);
477 // Determine the origin time of this request. If the SW has metadata on the request (which
478 // it
479 // should), it will have the time the request was added to the cache. If it doesn't for some
480 // reason, the request may have a Date header which will serve the same purpose.
481 let ts;
482 try {
483 // Check the metadata table. If a timestamp is there, use it.
484 const metaTable = yield this.metadata;
485 ts = (yield metaTable.read(req.url)).ts;
486 }
487 catch (e) {
488 // Otherwise, look for a Date header.
489 const date = res.headers.get('Date');
490 if (date === null) {
491 // Unable to determine when this response was created. Assume that it's stale, and
492 // revalidate it.
493 return true;
494 }
495 ts = Date.parse(date);
496 }
497 const age = this.adapter.time - ts;
498 return age < 0 || age > maxAge;
499 }
500 catch (e) {
501 // Assume stale.
502 return true;
503 }
504 }
505 else if (res.headers.has('Expires')) {
506 // Determine if the expiration time has passed.
507 const expiresStr = res.headers.get('Expires');
508 try {
509 // The request needs to be revalidated if the current time is later than the expiration
510 // time, if it parses correctly.
511 return this.adapter.time > Date.parse(expiresStr);
512 }
513 catch (e) {
514 // The expiration date failed to parse, so revalidate as a precaution.
515 return true;
516 }
517 }
518 else {
519 // No way to evaluate staleness, so assume the response is already stale.
520 return true;
521 }
522 });
523 }
524 /**
525 * Fetch the complete state of a cached resource, or return null if it's not found.
526 */
527 fetchFromCacheOnly(url) {
528 return __awaiter(this, void 0, void 0, function* () {
529 const cache = yield this.cache;
530 const metaTable = yield this.metadata;
531 // Lookup the response in the cache.
532 const response = yield cache.match(this.adapter.newRequest(url));
533 if (response === undefined) {
534 // It's not found, return null.
535 return null;
536 }
537 // Next, lookup the cached metadata.
538 let metadata = undefined;
539 try {
540 metadata = yield metaTable.read(url);
541 }
542 catch (e) {
543 // Do nothing, not found. This shouldn't happen, but it can be handled.
544 }
545 // Return both the response and any available metadata.
546 return { response, metadata };
547 });
548 }
549 /**
550 * Lookup all resources currently stored in the cache which have no associated hash.
551 */
552 unhashedResources() {
553 return __awaiter(this, void 0, void 0, function* () {
554 const cache = yield this.cache;
555 // Start with the set of all cached URLs.
556 return (yield cache.keys())
557 .map(request => request.url)
558 // Exclude the URLs which have hashes.
559 .filter(url => !this.hashes.has(url));
560 });
561 }
562 /**
563 * Fetch the given resource from the network, and cache it if able.
564 */
565 fetchAndCacheOnce(req, used = true) {
566 return __awaiter(this, void 0, void 0, function* () {
567 // The `inFlightRequests` map holds information about which caching operations are currently
568 // underway for known resources. If this request appears there, another "thread" is already
569 // in the process of caching it, and this work should not be duplicated.
570 if (this.inFlightRequests.has(req.url)) {
571 // There is a caching operation already in progress for this request. Wait for it to
572 // complete, and hopefully it will have yielded a useful response.
573 return this.inFlightRequests.get(req.url);
574 }
575 // No other caching operation is being attempted for this resource, so it will be owned here.
576 // Go to the network and get the correct version.
577 const fetchOp = this.fetchFromNetwork(req);
578 // Save this operation in `inFlightRequests` so any other "thread" attempting to cache it
579 // will block on this chain instead of duplicating effort.
580 this.inFlightRequests.set(req.url, fetchOp);
581 // Make sure this attempt is cleaned up properly on failure.
582 try {
583 // Wait for a response. If this fails, the request will remain in `inFlightRequests`
584 // indefinitely.
585 const res = yield fetchOp;
586 // It's very important that only successful responses are cached. Unsuccessful responses
587 // should never be cached as this can completely break applications.
588 if (!res.ok) {
589 throw new Error(`Response not Ok (fetchAndCacheOnce): request for ${req.url} returned response ${res.status} ${res.statusText}`);
590 }
591 try {
592 // This response is safe to cache (as long as it's cloned). Wait until the cache operation
593 // is complete.
594 const cache = yield this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
595 yield cache.put(req, res.clone());
596 // If the request is not hashed, update its metadata, especially the timestamp. This is
597 // needed for future determination of whether this cached response is stale or not.
598 if (!this.hashes.has(req.url)) {
599 // Metadata is tracked for requests that are unhashed.
600 const meta = { ts: this.adapter.time, used };
601 const metaTable = yield this.metadata;
602 yield metaTable.write(req.url, meta);
603 }
604 return res;
605 }
606 catch (err) {
607 // Among other cases, this can happen when the user clears all data through the DevTools,
608 // but the SW is still running and serving another tab. In that case, trying to write to the
609 // caches throws an `Entry was not found` error.
610 // If this happens the SW can no longer work correctly. This situation is unrecoverable.
611 throw new SwCriticalError(`Failed to update the caches for request to '${req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
612 }
613 }
614 finally {
615 // Finally, it can be removed from `inFlightRequests`. This might result in a double-remove
616 // if some other chain was already making this request too, but that won't hurt anything.
617 this.inFlightRequests.delete(req.url);
618 }
619 });
620 }
621 fetchFromNetwork(req, redirectLimit = 3) {
622 return __awaiter(this, void 0, void 0, function* () {
623 // Make a cache-busted request for the resource.
624 const res = yield this.cacheBustedFetchFromNetwork(req);
625 // Check for redirected responses, and follow the redirects.
626 if (res['redirected'] && !!res.url) {
627 // If the redirect limit is exhausted, fail with an error.
628 if (redirectLimit === 0) {
629 throw new SwCriticalError(`Response hit redirect limit (fetchFromNetwork): request redirected too many times, next is ${res.url}`);
630 }
631 // Unwrap the redirect directly.
632 return this.fetchFromNetwork(this.adapter.newRequest(res.url), redirectLimit - 1);
633 }
634 return res;
635 });
636 }
637 /**
638 * Load a particular asset from the network, accounting for hash validation.
639 */
640 cacheBustedFetchFromNetwork(req) {
641 return __awaiter(this, void 0, void 0, function* () {
642 const url = this.getConfigUrl(req.url);
643 // If a hash is available for this resource, then compare the fetched version with the
644 // canonical hash. Otherwise, the network version will have to be trusted.
645 if (this.hashes.has(url)) {
646 // It turns out this resource does have a hash. Look it up. Unless the fetched version
647 // matches this hash, it's invalid and the whole manifest may need to be thrown out.
648 const canonicalHash = this.hashes.get(url);
649 // Ideally, the resource would be requested with cache-busting to guarantee the SW gets
650 // the freshest version. However, doing this would eliminate any chance of the response
651 // being in the HTTP cache. Given that the browser has recently actively loaded the page,
652 // it's likely that many of the responses the SW needs to cache are in the HTTP cache and
653 // are fresh enough to use. In the future, this could be done by setting cacheMode to
654 // *only* check the browser cache for a cached version of the resource, when cacheMode is
655 // fully supported. For now, the resource is fetched directly, without cache-busting, and
656 // if the hash test fails a cache-busted request is tried before concluding that the
657 // resource isn't correct. This gives the benefit of acceleration via the HTTP cache
658 // without the risk of stale data, at the expense of a duplicate request in the event of
659 // a stale response.
660 // Fetch the resource from the network (possibly hitting the HTTP cache).
661 const networkResult = yield this.safeFetch(req);
662 // Decide whether a cache-busted request is necessary. It might be for two independent
663 // reasons: either the non-cache-busted request failed (hopefully transiently) or if the
664 // hash of the content retrieved does not match the canonical hash from the manifest. It's
665 // only valid to access the content of the first response if the request was successful.
666 let makeCacheBustedRequest = networkResult.ok;
667 if (makeCacheBustedRequest) {
668 // The request was successful. A cache-busted request is only necessary if the hashes
669 // don't match. Compare them, making sure to clone the response so it can be used later
670 // if it proves to be valid.
671 const fetchedHash = sha1Binary(yield networkResult.clone().arrayBuffer());
672 makeCacheBustedRequest = (fetchedHash !== canonicalHash);
673 }
674 // Make a cache busted request to the network, if necessary.
675 if (makeCacheBustedRequest) {
676 // Hash failure, the version that was retrieved under the default URL did not have the
677 // hash expected. This could be because the HTTP cache got in the way and returned stale
678 // data, or because the version on the server really doesn't match. A cache-busting
679 // request will differentiate these two situations.
680 // TODO: handle case where the URL has parameters already (unlikely for assets).
681 const cacheBustReq = this.adapter.newRequest(this.cacheBust(req.url));
682 const cacheBustedResult = yield this.safeFetch(cacheBustReq);
683 // If the response was unsuccessful, there's nothing more that can be done.
684 if (!cacheBustedResult.ok) {
685 throw new SwCriticalError(`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${req.url} returned response ${cacheBustedResult.status} ${cacheBustedResult.statusText}`);
686 }
687 // Hash the contents.
688 const cacheBustedHash = sha1Binary(yield cacheBustedResult.clone().arrayBuffer());
689 // If the cache-busted version doesn't match, then the manifest is not an accurate
690 // representation of the server's current set of files, and the SW should give up.
691 if (canonicalHash !== cacheBustedHash) {
692 throw new SwCriticalError(`Hash mismatch (cacheBustedFetchFromNetwork): ${req.url}: expected ${canonicalHash}, got ${cacheBustedHash} (after cache busting)`);
693 }
694 // If it does match, then use the cache-busted result.
695 return cacheBustedResult;
696 }
697 // Excellent, the version from the network matched on the first try, with no need for
698 // cache-busting. Use it.
699 return networkResult;
700 }
701 else {
702 // This URL doesn't exist in our hash database, so it must be requested directly.
703 return this.safeFetch(req);
704 }
705 });
706 }
707 /**
708 * Possibly update a resource, if it's expired and needs to be updated. A no-op otherwise.
709 */
710 maybeUpdate(updateFrom, req, cache) {
711 return __awaiter(this, void 0, void 0, function* () {
712 const url = this.getConfigUrl(req.url);
713 const meta = yield this.metadata;
714 // Check if this resource is hashed and already exists in the cache of a prior version.
715 if (this.hashes.has(url)) {
716 const hash = this.hashes.get(url);
717 // Check the caches of prior versions, using the hash to ensure the correct version of
718 // the resource is loaded.
719 const res = yield updateFrom.lookupResourceWithHash(url, hash);
720 // If a previously cached version was available, copy it over to this cache.
721 if (res !== null) {
722 // Copy to this cache.
723 yield cache.put(req, res);
724 yield meta.write(req.url, { ts: this.adapter.time, used: false });
725 // No need to do anything further with this resource, it's now cached properly.
726 return true;
727 }
728 }
729 // No up-to-date version of this resource could be found.
730 return false;
731 });
732 }
733 /**
734 * Construct a cache-busting URL for a given URL.
735 */
736 cacheBust(url) {
737 return url + (url.indexOf('?') === -1 ? '?' : '&') + 'ngsw-cache-bust=' + Math.random();
738 }
739 safeFetch(req) {
740 return __awaiter(this, void 0, void 0, function* () {
741 try {
742 return yield this.scope.fetch(req);
743 }
744 catch (err) {
745 return this.adapter.newResponse('', {
746 status: 504,
747 statusText: 'Gateway Timeout',
748 });
749 }
750 });
751 }
752 }
753 /**
754 * An `AssetGroup` that prefetches all of its resources during initialization.
755 */
756 class PrefetchAssetGroup extends AssetGroup {
757 initializeFully(updateFrom) {
758 return __awaiter(this, void 0, void 0, function* () {
759 // Open the cache which actually holds requests.
760 const cache = yield this.cache;
761 // Cache all known resources serially. As this reduce proceeds, each Promise waits
762 // on the last before starting the fetch/cache operation for the next request. Any
763 // errors cause fall-through to the final Promise which rejects.
764 yield this.config.urls.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
765 // Wait on all previous operations to complete.
766 yield previous;
767 // Construct the Request for this url.
768 const req = this.adapter.newRequest(url);
769 // First, check the cache to see if there is already a copy of this resource.
770 const alreadyCached = (yield cache.match(req)) !== undefined;
771 // If the resource is in the cache already, it can be skipped.
772 if (alreadyCached) {
773 return;
774 }
775 // If an update source is available.
776 if (updateFrom !== undefined && (yield this.maybeUpdate(updateFrom, req, cache))) {
777 return;
778 }
779 // Otherwise, go to the network and hopefully cache the response (if successful).
780 yield this.fetchAndCacheOnce(req, false);
781 }), Promise.resolve());
782 // Handle updating of unknown (unhashed) resources. This is only possible if there's
783 // a source to update from.
784 if (updateFrom !== undefined) {
785 const metaTable = yield this.metadata;
786 // Select all of the previously cached resources. These are cached unhashed resources
787 // from previous versions of the app, in any asset group.
788 yield (yield updateFrom.previouslyCachedResources())
789 // First, narrow down the set of resources to those which are handled by this group.
790 // Either it's a known URL, or it matches a given pattern.
791 .filter(url => this.config.urls.some(cacheUrl => cacheUrl === url) ||
792 this.patterns.some(pattern => pattern.test(url)))
793 // Finally, process each resource in turn.
794 .reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
795 yield previous;
796 const req = this.adapter.newRequest(url);
797 // It's possible that the resource in question is already cached. If so,
798 // continue to the next one.
799 const alreadyCached = ((yield cache.match(req)) !== undefined);
800 if (alreadyCached) {
801 return;
802 }
803 // Get the most recent old version of the resource.
804 const res = yield updateFrom.lookupResourceWithoutHash(url);
805 if (res === null || res.metadata === undefined) {
806 // Unexpected, but not harmful.
807 return;
808 }
809 // Write it into the cache. It may already be expired, but it can still serve
810 // traffic until it's updated (stale-while-revalidate approach).
811 yield cache.put(req, res.response);
812 yield metaTable.write(url, Object.assign({}, res.metadata, { used: false }));
813 }), Promise.resolve());
814 }
815 });
816 }
817 }
818 class LazyAssetGroup extends AssetGroup {
819 initializeFully(updateFrom) {
820 return __awaiter(this, void 0, void 0, function* () {
821 // No action necessary if no update source is available - resources managed in this group
822 // are all lazily loaded, so there's nothing to initialize.
823 if (updateFrom === undefined) {
824 return;
825 }
826 // Open the cache which actually holds requests.
827 const cache = yield this.cache;
828 // Loop through the listed resources, caching any which are available.
829 yield this.config.urls.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
830 // Wait on all previous operations to complete.
831 yield previous;
832 // Construct the Request for this url.
833 const req = this.adapter.newRequest(url);
834 // First, check the cache to see if there is already a copy of this resource.
835 const alreadyCached = (yield cache.match(req)) !== undefined;
836 // If the resource is in the cache already, it can be skipped.
837 if (alreadyCached) {
838 return;
839 }
840 const updated = yield this.maybeUpdate(updateFrom, req, cache);
841 if (this.config.updateMode === 'prefetch' && !updated) {
842 // If the resource was not updated, either it was not cached before or
843 // the previously cached version didn't match the updated hash. In that
844 // case, prefetch update mode dictates that the resource will be updated,
845 // except if it was not previously utilized. Check the status of the
846 // cached resource to see.
847 const cacheStatus = yield updateFrom.recentCacheStatus(url);
848 // If the resource is not cached, or was cached but unused, then it will be
849 // loaded lazily.
850 if (cacheStatus !== UpdateCacheStatus.CACHED) {
851 return;
852 }
853 // Update from the network.
854 yield this.fetchAndCacheOnce(req, false);
855 }
856 }), Promise.resolve());
857 });
858 }
859 }
860
861 /**
862 * @license
863 * Copyright Google Inc. All Rights Reserved.
864 *
865 * Use of this source code is governed by an MIT-style license that can be
866 * found in the LICENSE file at https://angular.io/license
867 */
868 var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
869 return new (P || (P = Promise))(function (resolve, reject) {
870 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
871 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
872 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
873 step((generator = generator.apply(thisArg, _arguments || [])).next());
874 });
875 };
876 /**
877 * Manages an instance of `LruState` and moves URLs to the head of the
878 * chain when requested.
879 */
880 class LruList {
881 constructor(state) {
882 if (state === undefined) {
883 state = {
884 head: null,
885 tail: null,
886 map: {},
887 count: 0,
888 };
889 }
890 this.state = state;
891 }
892 /**
893 * The current count of URLs in the list.
894 */
895 get size() { return this.state.count; }
896 /**
897 * Remove the tail.
898 */
899 pop() {
900 // If there is no tail, return null.
901 if (this.state.tail === null) {
902 return null;
903 }
904 const url = this.state.tail;
905 this.remove(url);
906 // This URL has been successfully evicted.
907 return url;
908 }
909 remove(url) {
910 const node = this.state.map[url];
911 if (node === undefined) {
912 return false;
913 }
914 // Special case if removing the current head.
915 if (this.state.head === url) {
916 // The node is the current head. Special case the removal.
917 if (node.next === null) {
918 // This is the only node. Reset the cache to be empty.
919 this.state.head = null;
920 this.state.tail = null;
921 this.state.map = {};
922 this.state.count = 0;
923 return true;
924 }
925 // There is at least one other node. Make the next node the new head.
926 const next = this.state.map[node.next];
927 next.previous = null;
928 this.state.head = next.url;
929 node.next = null;
930 delete this.state.map[url];
931 this.state.count--;
932 return true;
933 }
934 // The node is not the head, so it has a previous. It may or may not be the tail.
935 // If it is not, then it has a next. First, grab the previous node.
936 const previous = this.state.map[node.previous];
937 // Fix the forward pointer to skip over node and go directly to node.next.
938 previous.next = node.next;
939 // node.next may or may not be set. If it is, fix the back pointer to skip over node.
940 // If it's not set, then this node happened to be the tail, and the tail needs to be
941 // updated to point to the previous node (removing the tail).
942 if (node.next !== null) {
943 // There is a next node, fix its back pointer to skip this node.
944 this.state.map[node.next].previous = node.previous;
945 }
946 else {
947 // There is no next node - the accessed node must be the tail. Move the tail pointer.
948 this.state.tail = node.previous;
949 }
950 node.next = null;
951 node.previous = null;
952 delete this.state.map[url];
953 // Count the removal.
954 this.state.count--;
955 return true;
956 }
957 accessed(url) {
958 // When a URL is accessed, its node needs to be moved to the head of the chain.
959 // This is accomplished in two steps:
960 //
961 // 1) remove the node from its position within the chain.
962 // 2) insert the node as the new head.
963 //
964 // Sometimes, a URL is accessed which has not been seen before. In this case, step 1 can
965 // be skipped completely (which will grow the chain by one). Of course, if the node is
966 // already the head, this whole operation can be skipped.
967 if (this.state.head === url) {
968 // The URL is already in the head position, accessing it is a no-op.
969 return;
970 }
971 // Look up the node in the map, and construct a new entry if it's
972 const node = this.state.map[url] || { url, next: null, previous: null };
973 // Step 1: remove the node from its position within the chain, if it is in the chain.
974 if (this.state.map[url] !== undefined) {
975 this.remove(url);
976 }
977 // Step 2: insert the node at the head of the chain.
978 // First, check if there's an existing head node. If there is, it has previous: null.
979 // Its previous pointer should be set to the node we're inserting.
980 if (this.state.head !== null) {
981 this.state.map[this.state.head].previous = url;
982 }
983 // The next pointer of the node being inserted gets set to the old head, before the head
984 // pointer is updated to this node.
985 node.next = this.state.head;
986 // The new head is the new node.
987 this.state.head = url;
988 // If there is no tail, then this is the first node, and is both the head and the tail.
989 if (this.state.tail === null) {
990 this.state.tail = url;
991 }
992 // Set the node in the map of nodes (if the URL has been seen before, this is a no-op)
993 // and count the insertion.
994 this.state.map[url] = node;
995 this.state.count++;
996 }
997 }
998 /**
999 * A group of cached resources determined by a set of URL patterns which follow a LRU policy
1000 * for caching.
1001 */
1002 class DataGroup {
1003 constructor(scope, adapter, config, db, prefix) {
1004 this.scope = scope;
1005 this.adapter = adapter;
1006 this.config = config;
1007 this.db = db;
1008 this.prefix = prefix;
1009 /**
1010 * Tracks the LRU state of resources in this cache.
1011 */
1012 this._lru = null;
1013 this.patterns = this.config.patterns.map(pattern => new RegExp(pattern));
1014 this.cache = this.scope.caches.open(`${this.prefix}:dynamic:${this.config.name}:cache`);
1015 this.lruTable = this.db.open(`${this.prefix}:dynamic:${this.config.name}:lru`);
1016 this.ageTable = this.db.open(`${this.prefix}:dynamic:${this.config.name}:age`);
1017 }
1018 /**
1019 * Lazily initialize/load the LRU chain.
1020 */
1021 lru() {
1022 return __awaiter$1(this, void 0, void 0, function* () {
1023 if (this._lru === null) {
1024 const table = yield this.lruTable;
1025 try {
1026 this._lru = new LruList(yield table.read('lru'));
1027 }
1028 catch (e) {
1029 this._lru = new LruList();
1030 }
1031 }
1032 return this._lru;
1033 });
1034 }
1035 /**
1036 * Sync the LRU chain to non-volatile storage.
1037 */
1038 syncLru() {
1039 return __awaiter$1(this, void 0, void 0, function* () {
1040 if (this._lru === null) {
1041 return;
1042 }
1043 const table = yield this.lruTable;
1044 return table.write('lru', this._lru.state);
1045 });
1046 }
1047 /**
1048 * Process a fetch event and return a `Response` if the resource is covered by this group,
1049 * or `null` otherwise.
1050 */
1051 handleFetch(req, ctx) {
1052 return __awaiter$1(this, void 0, void 0, function* () {
1053 // Do nothing
1054 if (!this.patterns.some(pattern => pattern.test(req.url))) {
1055 return null;
1056 }
1057 // Lazily initialize the LRU cache.
1058 const lru = yield this.lru();
1059 // The URL matches this cache. First, check whether this is a mutating request or not.
1060 switch (req.method) {
1061 case 'OPTIONS':
1062 // Don't try to cache this - it's non-mutating, but is part of a mutating request.
1063 // Most likely SWs don't even see this, but this guard is here just in case.
1064 return null;
1065 case 'GET':
1066 case 'HEAD':
1067 // Handle the request with whatever strategy was selected.
1068 switch (this.config.strategy) {
1069 case 'freshness':
1070 return this.handleFetchWithFreshness(req, ctx, lru);
1071 case 'performance':
1072 return this.handleFetchWithPerformance(req, ctx, lru);
1073 default:
1074 throw new Error(`Unknown strategy: ${this.config.strategy}`);
1075 }
1076 default:
1077 // This was a mutating request. Assume the cache for this URL is no longer valid.
1078 const wasCached = lru.remove(req.url);
1079 // If there was a cached entry, remove it.
1080 if (wasCached) {
1081 yield this.clearCacheForUrl(req.url);
1082 }
1083 // Sync the LRU chain to non-volatile storage.
1084 yield this.syncLru();
1085 // Finally, fall back on the network.
1086 return this.safeFetch(req);
1087 }
1088 });
1089 }
1090 handleFetchWithPerformance(req, ctx, lru) {
1091 return __awaiter$1(this, void 0, void 0, function* () {
1092 let res = null;
1093 // Check the cache first. If the resource exists there (and is not expired), the cached
1094 // version can be used.
1095 const fromCache = yield this.loadFromCache(req, lru);
1096 if (fromCache !== null) {
1097 res = fromCache.res;
1098 // Check the age of the resource.
1099 if (this.config.refreshAheadMs !== undefined && fromCache.age >= this.config.refreshAheadMs) {
1100 ctx.waitUntil(this.safeCacheResponse(req, this.safeFetch(req)));
1101 }
1102 }
1103 if (res !== null) {
1104 return res;
1105 }
1106 // No match from the cache. Go to the network. Note that this is not an 'await'
1107 // call, networkFetch is the actual Promise. This is due to timeout handling.
1108 const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
1109 res = yield timeoutFetch;
1110 // Since fetch() will always return a response, undefined indicates a timeout.
1111 if (res === undefined) {
1112 // The request timed out. Return a Gateway Timeout error.
1113 res = this.adapter.newResponse(null, { status: 504, statusText: 'Gateway Timeout' });
1114 // Cache the network response eventually.
1115 ctx.waitUntil(this.safeCacheResponse(req, networkFetch));
1116 }
1117 // The request completed in time, so cache it inline with the response flow.
1118 yield this.cacheResponse(req, res, lru);
1119 return res;
1120 });
1121 }
1122 handleFetchWithFreshness(req, ctx, lru) {
1123 return __awaiter$1(this, void 0, void 0, function* () {
1124 // Start with a network fetch.
1125 const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
1126 let res;
1127 // If that fetch errors, treat it as a timed out request.
1128 try {
1129 res = yield timeoutFetch;
1130 }
1131 catch (e) {
1132 res = undefined;
1133 }
1134 // If the network fetch times out or errors, fall back on the cache.
1135 if (res === undefined) {
1136 ctx.waitUntil(this.safeCacheResponse(req, networkFetch));
1137 // Ignore the age, the network response will be cached anyway due to the
1138 // behavior of freshness.
1139 const fromCache = yield this.loadFromCache(req, lru);
1140 res = (fromCache !== null) ? fromCache.res : null;
1141 }
1142 else {
1143 yield this.cacheResponse(req, res, lru, true);
1144 }
1145 // Either the network fetch didn't time out, or the cache yielded a usable response.
1146 // In either case, use it.
1147 if (res !== null) {
1148 return res;
1149 }
1150 // No response in the cache. No choice but to fall back on the full network fetch.
1151 res = yield networkFetch;
1152 yield this.cacheResponse(req, res, lru, true);
1153 return res;
1154 });
1155 }
1156 networkFetchWithTimeout(req) {
1157 // If there is a timeout configured, race a timeout Promise with the network fetch.
1158 // Otherwise, just fetch from the network directly.
1159 if (this.config.timeoutMs !== undefined) {
1160 const networkFetch = this.scope.fetch(req);
1161 const safeNetworkFetch = (() => __awaiter$1(this, void 0, void 0, function* () {
1162 try {
1163 return yield networkFetch;
1164 }
1165 catch (err) {
1166 return this.adapter.newResponse(null, {
1167 status: 504,
1168 statusText: 'Gateway Timeout',
1169 });
1170 }
1171 }))();
1172 const networkFetchUndefinedError = (() => __awaiter$1(this, void 0, void 0, function* () {
1173 try {
1174 return yield networkFetch;
1175 }
1176 catch (err) {
1177 return undefined;
1178 }
1179 }))();
1180 // Construct a Promise<undefined> for the timeout.
1181 const timeout = this.adapter.timeout(this.config.timeoutMs);
1182 // Race that with the network fetch. This will either be a Response, or `undefined`
1183 // in the event that the request errored or timed out.
1184 return [Promise.race([networkFetchUndefinedError, timeout]), safeNetworkFetch];
1185 }
1186 else {
1187 const networkFetch = this.safeFetch(req);
1188 // Do a plain fetch.
1189 return [networkFetch, networkFetch];
1190 }
1191 }
1192 safeCacheResponse(req, res) {
1193 return __awaiter$1(this, void 0, void 0, function* () {
1194 try {
1195 yield this.cacheResponse(req, yield res, yield this.lru());
1196 }
1197 catch (e) {
1198 // TODO: handle this error somehow?
1199 }
1200 });
1201 }
1202 loadFromCache(req, lru) {
1203 return __awaiter$1(this, void 0, void 0, function* () {
1204 // Look for a response in the cache. If one exists, return it.
1205 const cache = yield this.cache;
1206 let res = yield cache.match(req);
1207 if (res !== undefined) {
1208 // A response was found in the cache, but its age is not yet known. Look it up.
1209 try {
1210 const ageTable = yield this.ageTable;
1211 const age = this.adapter.time - (yield ageTable.read(req.url)).age;
1212 // If the response is young enough, use it.
1213 if (age <= this.config.maxAge) {
1214 // Successful match from the cache. Use the response, after marking it as having
1215 // been accessed.
1216 lru.accessed(req.url);
1217 return { res, age };
1218 }
1219 // Otherwise, or if there was an error, assume the response is expired, and evict it.
1220 }
1221 catch (e) {
1222 // Some error getting the age for the response. Assume it's expired.
1223 }
1224 lru.remove(req.url);
1225 yield this.clearCacheForUrl(req.url);
1226 // TODO: avoid duplicate in event of network timeout, maybe.
1227 yield this.syncLru();
1228 }
1229 return null;
1230 });
1231 }
1232 /**
1233 * Operation for caching the response from the server. This has to happen all
1234 * at once, so that the cache and LRU tracking remain in sync. If the network request
1235 * completes before the timeout, this logic will be run inline with the response flow.
1236 * If the request times out on the server, an error will be returned but the real network
1237 * request will still be running in the background, to be cached when it completes.
1238 */
1239 cacheResponse(req, res, lru, okToCacheOpaque = false) {
1240 return __awaiter$1(this, void 0, void 0, function* () {
1241 // Only cache successful responses.
1242 if (!res.ok || (okToCacheOpaque && res.type === 'opaque')) {
1243 return;
1244 }
1245 // If caching this response would make the cache exceed its maximum size, evict something
1246 // first.
1247 if (lru.size >= this.config.maxSize) {
1248 // The cache is too big, evict something.
1249 const evictedUrl = lru.pop();
1250 if (evictedUrl !== null) {
1251 yield this.clearCacheForUrl(evictedUrl);
1252 }
1253 }
1254 // TODO: evaluate for possible race conditions during flaky network periods.
1255 // Mark this resource as having been accessed recently. This ensures it won't be evicted
1256 // until enough other resources are requested that it falls off the end of the LRU chain.
1257 lru.accessed(req.url);
1258 // Store the response in the cache (cloning because the browser will consume
1259 // the body during the caching operation).
1260 yield (yield this.cache).put(req, res.clone());
1261 // Store the age of the cache.
1262 const ageTable = yield this.ageTable;
1263 yield ageTable.write(req.url, { age: this.adapter.time });
1264 // Sync the LRU chain to non-volatile storage.
1265 yield this.syncLru();
1266 });
1267 }
1268 /**
1269 * Delete all of the saved state which this group uses to track resources.
1270 */
1271 cleanup() {
1272 return __awaiter$1(this, void 0, void 0, function* () {
1273 // Remove both the cache and the database entries which track LRU stats.
1274 yield Promise.all([
1275 this.scope.caches.delete(`${this.prefix}:dynamic:${this.config.name}:cache`),
1276 this.db.delete(`${this.prefix}:dynamic:${this.config.name}:age`),
1277 this.db.delete(`${this.prefix}:dynamic:${this.config.name}:lru`),
1278 ]);
1279 });
1280 }
1281 /**
1282 * Clear the state of the cache for a particular resource.
1283 *
1284 * This doesn't remove the resource from the LRU table, that is assumed to have
1285 * been done already. This clears the GET and HEAD versions of the request from
1286 * the cache itself, as well as the metadata stored in the age table.
1287 */
1288 clearCacheForUrl(url) {
1289 return __awaiter$1(this, void 0, void 0, function* () {
1290 const [cache, ageTable] = yield Promise.all([this.cache, this.ageTable]);
1291 yield Promise.all([
1292 cache.delete(this.adapter.newRequest(url, { method: 'GET' })),
1293 cache.delete(this.adapter.newRequest(url, { method: 'HEAD' })),
1294 ageTable.delete(url),
1295 ]);
1296 });
1297 }
1298 safeFetch(req) {
1299 return __awaiter$1(this, void 0, void 0, function* () {
1300 try {
1301 return this.scope.fetch(req);
1302 }
1303 catch (err) {
1304 return this.adapter.newResponse(null, {
1305 status: 504,
1306 statusText: 'Gateway Timeout',
1307 });
1308 }
1309 });
1310 }
1311 }
1312
1313 /**
1314 * @license
1315 * Copyright Google Inc. All Rights Reserved.
1316 *
1317 * Use of this source code is governed by an MIT-style license that can be
1318 * found in the LICENSE file at https://angular.io/license
1319 */
1320 var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
1321 return new (P || (P = Promise))(function (resolve, reject) {
1322 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1323 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1324 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
1325 step((generator = generator.apply(thisArg, _arguments || [])).next());
1326 });
1327 };
1328 /**
1329 * A specific version of the application, identified by a unique manifest
1330 * as determined by its hash.
1331 *
1332 * Each `AppVersion` can be thought of as a published version of the app
1333 * that can be installed as an update to any previously installed versions.
1334 */
1335 class AppVersion {
1336 constructor(scope, adapter, database, idle, manifest, manifestHash) {
1337 this.scope = scope;
1338 this.adapter = adapter;
1339 this.database = database;
1340 this.idle = idle;
1341 this.manifest = manifest;
1342 this.manifestHash = manifestHash;
1343 /**
1344 * A Map of absolute URL paths (/foo.txt) to the known hash of their
1345 * contents (if available).
1346 */
1347 this.hashTable = new Map();
1348 /**
1349 * Tracks whether the manifest has encountered any inconsistencies.
1350 */
1351 this._okay = true;
1352 // The hashTable within the manifest is an Object - convert it to a Map for easier lookups.
1353 Object.keys(this.manifest.hashTable).forEach(url => {
1354 this.hashTable.set(url, this.manifest.hashTable[url]);
1355 });
1356 // Process each `AssetGroup` declared in the manifest. Each declared group gets an `AssetGroup`
1357 // instance
1358 // created for it, of a type that depends on the configuration mode.
1359 this.assetGroups = (manifest.assetGroups || []).map(config => {
1360 // Every asset group has a cache that's prefixed by the manifest hash and the name of the
1361 // group.
1362 const prefix = `ngsw:${this.manifestHash}:assets`;
1363 // Check the caching mode, which determines when resources will be fetched/updated.
1364 switch (config.installMode) {
1365 case 'prefetch':
1366 return new PrefetchAssetGroup(this.scope, this.adapter, this.idle, config, this.hashTable, this.database, prefix);
1367 case 'lazy':
1368 return new LazyAssetGroup(this.scope, this.adapter, this.idle, config, this.hashTable, this.database, prefix);
1369 }
1370 });
1371 // Process each `DataGroup` declared in the manifest.
1372 this.dataGroups = (manifest.dataGroups || [])
1373 .map(config => new DataGroup(this.scope, this.adapter, config, this.database, `ngsw:${config.version}:data`));
1374 // Create `include`/`exclude` RegExps for the `navigationUrls` declared in the manifest.
1375 const includeUrls = manifest.navigationUrls.filter(spec => spec.positive);
1376 const excludeUrls = manifest.navigationUrls.filter(spec => !spec.positive);
1377 this.navigationUrls = {
1378 include: includeUrls.map(spec => new RegExp(spec.regex)),
1379 exclude: excludeUrls.map(spec => new RegExp(spec.regex)),
1380 };
1381 }
1382 get okay() { return this._okay; }
1383 /**
1384 * Fully initialize this version of the application. If this Promise resolves successfully, all
1385 * required
1386 * data has been safely downloaded.
1387 */
1388 initializeFully(updateFrom) {
1389 return __awaiter$2(this, void 0, void 0, function* () {
1390 try {
1391 // Fully initialize each asset group, in series. Starts with an empty Promise,
1392 // and waits for the previous groups to have been initialized before initializing
1393 // the next one in turn.
1394 yield this.assetGroups.reduce((previous, group) => __awaiter$2(this, void 0, void 0, function* () {
1395 // Wait for the previous groups to complete initialization. If there is a
1396 // failure, this will throw, and each subsequent group will throw, until the
1397 // whole sequence fails.
1398 yield previous;
1399 // Initialize this group.
1400 return group.initializeFully(updateFrom);
1401 }), Promise.resolve());
1402 }
1403 catch (err) {
1404 this._okay = false;
1405 throw err;
1406 }
1407 });
1408 }
1409 handleFetch(req, context) {
1410 return __awaiter$2(this, void 0, void 0, function* () {
1411 // Check the request against each `AssetGroup` in sequence. If an `AssetGroup` can't handle the
1412 // request,
1413 // it will return `null`. Thus, the first non-null response is the SW's answer to the request.
1414 // So reduce
1415 // the group list, keeping track of a possible response. If there is one, it gets passed
1416 // through, and if
1417 // not the next group is consulted to produce a candidate response.
1418 const asset = yield this.assetGroups.reduce((potentialResponse, group) => __awaiter$2(this, void 0, void 0, function* () {
1419 // Wait on the previous potential response. If it's not null, it should just be passed
1420 // through.
1421 const resp = yield potentialResponse;
1422 if (resp !== null) {
1423 return resp;
1424 }
1425 // No response has been found yet. Maybe this group will have one.
1426 return group.handleFetch(req, context);
1427 }), Promise.resolve(null));
1428 // The result of the above is the asset response, if there is any, or null otherwise. Return the
1429 // asset
1430 // response if there was one. If not, check with the data caching groups.
1431 if (asset !== null) {
1432 return asset;
1433 }
1434 // Perform the same reduction operation as above, but this time processing
1435 // the data caching groups.
1436 const data = yield this.dataGroups.reduce((potentialResponse, group) => __awaiter$2(this, void 0, void 0, function* () {
1437 const resp = yield potentialResponse;
1438 if (resp !== null) {
1439 return resp;
1440 }
1441 return group.handleFetch(req, context);
1442 }), Promise.resolve(null));
1443 // If the data caching group returned a response, go with it.
1444 if (data !== null) {
1445 return data;
1446 }
1447 // Next, check if this is a navigation request for a route. Detect circular
1448 // navigations by checking if the request URL is the same as the index URL.
1449 if (req.url !== this.manifest.index && this.isNavigationRequest(req)) {
1450 // This was a navigation request. Re-enter `handleFetch` with a request for
1451 // the URL.
1452 return this.handleFetch(this.adapter.newRequest(this.manifest.index), context);
1453 }
1454 return null;
1455 });
1456 }
1457 /**
1458 * Determine whether the request is a navigation request.
1459 * Takes into account: Request mode, `Accept` header, `navigationUrls` patterns.
1460 */
1461 isNavigationRequest(req) {
1462 if (req.mode !== 'navigate') {
1463 return false;
1464 }
1465 if (!this.acceptsTextHtml(req)) {
1466 return false;
1467 }
1468 const urlPrefix = this.scope.registration.scope.replace(/\/$/, '');
1469 const url = req.url.startsWith(urlPrefix) ? req.url.substr(urlPrefix.length) : req.url;
1470 const urlWithoutQueryOrHash = url.replace(/[?#].*$/, '');
1471 return this.navigationUrls.include.some(regex => regex.test(urlWithoutQueryOrHash)) &&
1472 !this.navigationUrls.exclude.some(regex => regex.test(urlWithoutQueryOrHash));
1473 }
1474 /**
1475 * Check this version for a given resource with a particular hash.
1476 */
1477 lookupResourceWithHash(url, hash) {
1478 return __awaiter$2(this, void 0, void 0, function* () {
1479 // Verify that this version has the requested resource cached. If not,
1480 // there's no point in trying.
1481 if (!this.hashTable.has(url)) {
1482 return null;
1483 }
1484 // Next, check whether the resource has the correct hash. If not, any cached
1485 // response isn't usable.
1486 if (this.hashTable.get(url) !== hash) {
1487 return null;
1488 }
1489 const cacheState = yield this.lookupResourceWithoutHash(url);
1490 return cacheState && cacheState.response;
1491 });
1492 }
1493 /**
1494 * Check this version for a given resource regardless of its hash.
1495 */
1496 lookupResourceWithoutHash(url) {
1497 // Limit the search to asset groups, and only scan the cache, don't
1498 // load resources from the network.
1499 return this.assetGroups.reduce((potentialResponse, group) => __awaiter$2(this, void 0, void 0, function* () {
1500 const resp = yield potentialResponse;
1501 if (resp !== null) {
1502 return resp;
1503 }
1504 // fetchFromCacheOnly() avoids any network fetches, and returns the
1505 // full set of cache data, not just the Response.
1506 return group.fetchFromCacheOnly(url);
1507 }), Promise.resolve(null));
1508 }
1509 /**
1510 * List all unhashed resources from all asset groups.
1511 */
1512 previouslyCachedResources() {
1513 return this.assetGroups.reduce((resources, group) => __awaiter$2(this, void 0, void 0, function* () {
1514 return (yield resources).concat(yield group.unhashedResources());
1515 }), Promise.resolve([]));
1516 }
1517 recentCacheStatus(url) {
1518 return __awaiter$2(this, void 0, void 0, function* () {
1519 return this.assetGroups.reduce((current, group) => __awaiter$2(this, void 0, void 0, function* () {
1520 const status = yield current;
1521 if (status === UpdateCacheStatus.CACHED) {
1522 return status;
1523 }
1524 const groupStatus = yield group.cacheStatus(url);
1525 if (groupStatus === UpdateCacheStatus.NOT_CACHED) {
1526 return status;
1527 }
1528 return groupStatus;
1529 }), Promise.resolve(UpdateCacheStatus.NOT_CACHED));
1530 });
1531 }
1532 /**
1533 * Erase this application version, by cleaning up all the caches.
1534 */
1535 cleanup() {
1536 return __awaiter$2(this, void 0, void 0, function* () {
1537 yield Promise.all(this.assetGroups.map(group => group.cleanup()));
1538 yield Promise.all(this.dataGroups.map(group => group.cleanup()));
1539 });
1540 }
1541 /**
1542 * Get the opaque application data which was provided with the manifest.
1543 */
1544 get appData() { return this.manifest.appData || null; }
1545 /**
1546 * Check whether a request accepts `text/html` (based on the `Accept` header).
1547 */
1548 acceptsTextHtml(req) {
1549 const accept = req.headers.get('Accept');
1550 if (accept === null) {
1551 return false;
1552 }
1553 const values = accept.split(',');
1554 return values.some(value => value.trim().toLowerCase() === 'text/html');
1555 }
1556 }
1557
1558 /**
1559 * @license
1560 * Copyright Google Inc. All Rights Reserved.
1561 *
1562 * Use of this source code is governed by an MIT-style license that can be
1563 * found in the LICENSE file at https://angular.io/license
1564 */
1565 var __awaiter$3 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
1566 return new (P || (P = Promise))(function (resolve, reject) {
1567 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1568 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1569 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
1570 step((generator = generator.apply(thisArg, _arguments || [])).next());
1571 });
1572 };
1573 const DEBUG_LOG_BUFFER_SIZE = 100;
1574 class DebugHandler {
1575 constructor(driver, adapter) {
1576 this.driver = driver;
1577 this.adapter = adapter;
1578 // There are two debug log message arrays. debugLogA records new debugging messages.
1579 // Once it reaches DEBUG_LOG_BUFFER_SIZE, the array is moved to debugLogB and a new
1580 // array is assigned to debugLogA. This ensures that insertion to the debug log is
1581 // always O(1) no matter the number of logged messages, and that the total number
1582 // of messages in the log never exceeds 2 * DEBUG_LOG_BUFFER_SIZE.
1583 this.debugLogA = [];
1584 this.debugLogB = [];
1585 }
1586 handleFetch(req) {
1587 return __awaiter$3(this, void 0, void 0, function* () {
1588 const [state, versions, idle] = yield Promise.all([
1589 this.driver.debugState(),
1590 this.driver.debugVersions(),
1591 this.driver.debugIdleState(),
1592 ]);
1593 const msgState = `NGSW Debug Info:
1594
1595Driver state: ${state.state} (${state.why})
1596Latest manifest hash: ${state.latestHash || 'none'}
1597Last update check: ${this.since(state.lastUpdateCheck)}`;
1598 const msgVersions = versions
1599 .map(version => `=== Version ${version.hash} ===
1600
1601Clients: ${version.clients.join(', ')}`)
1602 .join('\n\n');
1603 const msgIdle = `=== Idle Task Queue ===
1604Last update tick: ${this.since(idle.lastTrigger)}
1605Last update run: ${this.since(idle.lastRun)}
1606Task queue:
1607${idle.queue.map(v => ' * ' + v).join('\n')}
1608
1609Debug log:
1610${this.formatDebugLog(this.debugLogB)}
1611${this.formatDebugLog(this.debugLogA)}
1612`;
1613 return this.adapter.newResponse(`${msgState}
1614
1615${msgVersions}
1616
1617${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }) });
1618 });
1619 }
1620 since(time) {
1621 if (time === null) {
1622 return 'never';
1623 }
1624 let age = this.adapter.time - time;
1625 const days = Math.floor(age / 86400000);
1626 age = age % 86400000;
1627 const hours = Math.floor(age / 3600000);
1628 age = age % 3600000;
1629 const minutes = Math.floor(age / 60000);
1630 age = age % 60000;
1631 const seconds = Math.floor(age / 1000);
1632 const millis = age % 1000;
1633 return '' + (days > 0 ? `${days}d` : '') + (hours > 0 ? `${hours}h` : '') +
1634 (minutes > 0 ? `${minutes}m` : '') + (seconds > 0 ? `${seconds}s` : '') +
1635 (millis > 0 ? `${millis}u` : '');
1636 }
1637 log(value, context = '') {
1638 // Rotate the buffers if debugLogA has grown too large.
1639 if (this.debugLogA.length === DEBUG_LOG_BUFFER_SIZE) {
1640 this.debugLogB = this.debugLogA;
1641 this.debugLogA = [];
1642 }
1643 // Convert errors to string for logging.
1644 if (typeof value !== 'string') {
1645 value = this.errorToString(value);
1646 }
1647 // Log the message.
1648 this.debugLogA.push({ value, time: this.adapter.time, context });
1649 }
1650 errorToString(err) { return `${err.name}(${err.message}, ${err.stack})`; }
1651 formatDebugLog(log) {
1652 return log.map(entry => `[${this.since(entry.time)}] ${entry.value} ${entry.context}`)
1653 .join('\n');
1654 }
1655 }
1656
1657 /**
1658 * @license
1659 * Copyright Google Inc. All Rights Reserved.
1660 *
1661 * Use of this source code is governed by an MIT-style license that can be
1662 * found in the LICENSE file at https://angular.io/license
1663 */
1664 var __awaiter$4 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
1665 return new (P || (P = Promise))(function (resolve, reject) {
1666 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1667 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1668 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
1669 step((generator = generator.apply(thisArg, _arguments || [])).next());
1670 });
1671 };
1672 class IdleScheduler {
1673 constructor(adapter, threshold, debug) {
1674 this.adapter = adapter;
1675 this.threshold = threshold;
1676 this.debug = debug;
1677 this.queue = [];
1678 this.scheduled = null;
1679 this.empty = Promise.resolve();
1680 this.emptyResolve = null;
1681 this.lastTrigger = null;
1682 this.lastRun = null;
1683 }
1684 trigger() {
1685 return __awaiter$4(this, void 0, void 0, function* () {
1686 this.lastTrigger = this.adapter.time;
1687 if (this.queue.length === 0) {
1688 return;
1689 }
1690 if (this.scheduled !== null) {
1691 this.scheduled.cancel = true;
1692 }
1693 const scheduled = {
1694 cancel: false,
1695 };
1696 this.scheduled = scheduled;
1697 yield this.adapter.timeout(this.threshold);
1698 if (scheduled.cancel) {
1699 return;
1700 }
1701 this.scheduled = null;
1702 yield this.execute();
1703 });
1704 }
1705 execute() {
1706 return __awaiter$4(this, void 0, void 0, function* () {
1707 this.lastRun = this.adapter.time;
1708 while (this.queue.length > 0) {
1709 const queue = this.queue;
1710 this.queue = [];
1711 yield queue.reduce((previous, task) => __awaiter$4(this, void 0, void 0, function* () {
1712 yield previous;
1713 try {
1714 yield task.run();
1715 }
1716 catch (err) {
1717 this.debug.log(err, `while running idle task ${task.desc}`);
1718 }
1719 }), Promise.resolve());
1720 }
1721 if (this.emptyResolve !== null) {
1722 this.emptyResolve();
1723 this.emptyResolve = null;
1724 }
1725 this.empty = Promise.resolve();
1726 });
1727 }
1728 schedule(desc, run) {
1729 this.queue.push({ desc, run });
1730 if (this.emptyResolve === null) {
1731 this.empty = new Promise(resolve => { this.emptyResolve = resolve; });
1732 }
1733 }
1734 get size() { return this.queue.length; }
1735 get taskDescriptions() { return this.queue.map(task => task.desc); }
1736 }
1737
1738 /**
1739 * @license
1740 * Copyright Google Inc. All Rights Reserved.
1741 *
1742 * Use of this source code is governed by an MIT-style license that can be
1743 * found in the LICENSE file at https://angular.io/license
1744 */
1745 function hashManifest(manifest) {
1746 return sha1(JSON.stringify(manifest));
1747 }
1748
1749 /**
1750 * @license
1751 * Copyright Google Inc. All Rights Reserved.
1752 *
1753 * Use of this source code is governed by an MIT-style license that can be
1754 * found in the LICENSE file at https://angular.io/license
1755 */
1756 function isMsgCheckForUpdates(msg) {
1757 return msg.action === 'CHECK_FOR_UPDATES';
1758 }
1759 function isMsgActivateUpdate(msg) {
1760 return msg.action === 'ACTIVATE_UPDATE';
1761 }
1762
1763 /**
1764 * @license
1765 * Copyright Google Inc. All Rights Reserved.
1766 *
1767 * Use of this source code is governed by an MIT-style license that can be
1768 * found in the LICENSE file at https://angular.io/license
1769 */
1770 var __awaiter$5 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
1771 return new (P || (P = Promise))(function (resolve, reject) {
1772 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1773 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1774 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
1775 step((generator = generator.apply(thisArg, _arguments || [])).next());
1776 });
1777 };
1778 const IDLE_THRESHOLD = 5000;
1779 const SUPPORTED_CONFIG_VERSION = 1;
1780 const NOTIFICATION_OPTION_NAMES = [
1781 'actions', 'badge', 'body', 'data', 'dir', 'icon', 'image', 'lang', 'renotify',
1782 'requireInteraction', 'silent', 'tag', 'timestamp', 'title', 'vibrate'
1783 ];
1784 var DriverReadyState;
1785 (function (DriverReadyState) {
1786 // The SW is operating in a normal mode, responding to all traffic.
1787 DriverReadyState[DriverReadyState["NORMAL"] = 0] = "NORMAL";
1788 // The SW does not have a clean installation of the latest version of the app, but older
1789 // cached versions are safe to use so long as they don't try to fetch new dependencies.
1790 // This is a degraded state.
1791 DriverReadyState[DriverReadyState["EXISTING_CLIENTS_ONLY"] = 1] = "EXISTING_CLIENTS_ONLY";
1792 // The SW has decided that caching is completely unreliable, and is forgoing request
1793 // handling until the next restart.
1794 DriverReadyState[DriverReadyState["SAFE_MODE"] = 2] = "SAFE_MODE";
1795 })(DriverReadyState || (DriverReadyState = {}));
1796 class Driver {
1797 constructor(scope, adapter, db) {
1798 // Set up all the event handlers that the SW needs.
1799 this.scope = scope;
1800 this.adapter = adapter;
1801 this.db = db;
1802 /**
1803 * Tracks the current readiness condition under which the SW is operating. This controls
1804 * whether the SW attempts to respond to some or all requests.
1805 */
1806 this.state = DriverReadyState.NORMAL;
1807 this.stateMessage = '(nominal)';
1808 /**
1809 * Tracks whether the SW is in an initialized state or not. Before initialization,
1810 * it's not legal to respond to requests.
1811 */
1812 this.initialized = null;
1813 /**
1814 * Maps client IDs to the manifest hash of the application version being used to serve
1815 * them. If a client ID is not present here, it has not yet been assigned a version.
1816 *
1817 * If a ManifestHash appears here, it is also present in the `versions` map below.
1818 */
1819 this.clientVersionMap = new Map();
1820 /**
1821 * Maps manifest hashes to instances of `AppVersion` for those manifests.
1822 */
1823 this.versions = new Map();
1824 /**
1825 * The latest version fetched from the server.
1826 *
1827 * Valid after initialization has completed.
1828 */
1829 this.latestHash = null;
1830 this.lastUpdateCheck = null;
1831 /**
1832 * Whether there is a check for updates currently scheduled due to navigation.
1833 */
1834 this.scheduledNavUpdateCheck = false;
1835 /**
1836 * Keep track of whether we have logged an invalid `only-if-cached` request.
1837 * (See `.onFetch()` for details.)
1838 */
1839 this.loggedInvalidOnlyIfCachedRequest = false;
1840 // The install event is triggered when the service worker is first installed.
1841 this.scope.addEventListener('install', (event) => {
1842 // SW code updates are separate from application updates, so code updates are
1843 // almost as straightforward as restarting the SW. Because of this, it's always
1844 // safe to skip waiting until application tabs are closed, and activate the new
1845 // SW version immediately.
1846 event.waitUntil(this.scope.skipWaiting());
1847 });
1848 // The activate event is triggered when this version of the service worker is
1849 // first activated.
1850 this.scope.addEventListener('activate', (event) => {
1851 event.waitUntil((() => __awaiter$5(this, void 0, void 0, function* () {
1852 // As above, it's safe to take over from existing clients immediately, since the new SW
1853 // version will continue to serve the old application.
1854 yield this.scope.clients.claim();
1855 // Once all clients have been taken over, we can delete caches used by old versions of
1856 // `@angular/service-worker`, which are no longer needed. This can happen in the background.
1857 this.idle.schedule('activate: cleanup-old-sw-caches', () => __awaiter$5(this, void 0, void 0, function* () {
1858 try {
1859 yield this.cleanupOldSwCaches();
1860 }
1861 catch (err) {
1862 // Nothing to do - cleanup failed. Just log it.
1863 this.debugger.log(err, 'cleanupOldSwCaches @ activate: cleanup-old-sw-caches');
1864 }
1865 }));
1866 }))());
1867 // Rather than wait for the first fetch event, which may not arrive until
1868 // the next time the application is loaded, the SW takes advantage of the
1869 // activation event to schedule initialization. However, if this were run
1870 // in the context of the 'activate' event, waitUntil() here would cause fetch
1871 // events to block until initialization completed. Thus, the SW does a
1872 // postMessage() to itself, to schedule a new event loop iteration with an
1873 // entirely separate event context. The SW will be kept alive by waitUntil()
1874 // within that separate context while initialization proceeds, while at the
1875 // same time the activation event is allowed to resolve and traffic starts
1876 // being served.
1877 if (this.scope.registration.active !== null) {
1878 this.scope.registration.active.postMessage({ action: 'INITIALIZE' });
1879 }
1880 });
1881 // Handle the fetch, message, and push events.
1882 this.scope.addEventListener('fetch', (event) => this.onFetch(event));
1883 this.scope.addEventListener('message', (event) => this.onMessage(event));
1884 this.scope.addEventListener('push', (event) => this.onPush(event));
1885 this.scope.addEventListener('notificationclick', (event) => this.onClick(event));
1886 // The debugger generates debug pages in response to debugging requests.
1887 this.debugger = new DebugHandler(this, this.adapter);
1888 // The IdleScheduler will execute idle tasks after a given delay.
1889 this.idle = new IdleScheduler(this.adapter, IDLE_THRESHOLD, this.debugger);
1890 }
1891 /**
1892 * The handler for fetch events.
1893 *
1894 * This is the transition point between the synchronous event handler and the
1895 * asynchronous execution that eventually resolves for respondWith() and waitUntil().
1896 */
1897 onFetch(event) {
1898 const req = event.request;
1899 // The only thing that is served unconditionally is the debug page.
1900 if (this.adapter.parseUrl(req.url, this.scope.registration.scope).path === '/ngsw/state') {
1901 // Allow the debugger to handle the request, but don't affect SW state in any
1902 // other way.
1903 event.respondWith(this.debugger.handleFetch(req));
1904 return;
1905 }
1906 // If the SW is in a broken state where it's not safe to handle requests at all,
1907 // returning causes the request to fall back on the network. This is preferred over
1908 // `respondWith(fetch(req))` because the latter still shows in DevTools that the
1909 // request was handled by the SW.
1910 // TODO: try to handle DriverReadyState.EXISTING_CLIENTS_ONLY here.
1911 if (this.state === DriverReadyState.SAFE_MODE) {
1912 // Even though the worker is in safe mode, idle tasks still need to happen so
1913 // things like update checks, etc. can take place.
1914 event.waitUntil(this.idle.trigger());
1915 return;
1916 }
1917 // When opening DevTools in Chrome, a request is made for the current URL (and possibly related
1918 // resources, e.g. scripts) with `cache: 'only-if-cached'` and `mode: 'no-cors'`. These request
1919 // will eventually fail, because `only-if-cached` is only allowed to be used with
1920 // `mode: 'same-origin'`.
1921 // This is likely a bug in Chrome DevTools. Avoid handling such requests.
1922 // (See also https://github.com/angular/angular/issues/22362.)
1923 // TODO(gkalpak): Remove once no longer necessary (i.e. fixed in Chrome DevTools).
1924 if (req.cache === 'only-if-cached' && req.mode !== 'same-origin') {
1925 // Log the incident only the first time it happens, to avoid spamming the logs.
1926 if (!this.loggedInvalidOnlyIfCachedRequest) {
1927 this.loggedInvalidOnlyIfCachedRequest = true;
1928 this.debugger.log(`Ignoring invalid request: 'only-if-cached' can be set only with 'same-origin' mode`, `Driver.fetch(${req.url}, cache: ${req.cache}, mode: ${req.mode})`);
1929 }
1930 return;
1931 }
1932 // Past this point, the SW commits to handling the request itself. This could still
1933 // fail (and result in `state` being set to `SAFE_MODE`), but even in that case the
1934 // SW will still deliver a response.
1935 event.respondWith(this.handleFetch(event));
1936 }
1937 /**
1938 * The handler for message events.
1939 */
1940 onMessage(event) {
1941 // Ignore message events when the SW is in safe mode, for now.
1942 if (this.state === DriverReadyState.SAFE_MODE) {
1943 return;
1944 }
1945 // If the message doesn't have the expected signature, ignore it.
1946 const data = event.data;
1947 if (!data || !data.action) {
1948 return;
1949 }
1950 // Initialization is the only event which is sent directly from the SW to itself,
1951 // and thus `event.source` is not a Client. Handle it here, before the check
1952 // for Client sources.
1953 if (data.action === 'INITIALIZE') {
1954 // Only initialize if not already initialized (or initializing).
1955 if (this.initialized === null) {
1956 // Initialize the SW.
1957 this.initialized = this.initialize();
1958 // Wait until initialization is properly scheduled, then trigger idle
1959 // events to allow it to complete (assuming the SW is idle).
1960 event.waitUntil((() => __awaiter$5(this, void 0, void 0, function* () {
1961 yield this.initialized;
1962 yield this.idle.trigger();
1963 }))());
1964 }
1965 return;
1966 }
1967 // Only messages from true clients are accepted past this point (this is essentially
1968 // a typecast).
1969 if (!this.adapter.isClient(event.source)) {
1970 return;
1971 }
1972 // Handle the message and keep the SW alive until it's handled.
1973 event.waitUntil(this.handleMessage(data, event.source));
1974 }
1975 onPush(msg) {
1976 // Push notifications without data have no effect.
1977 if (!msg.data) {
1978 return;
1979 }
1980 // Handle the push and keep the SW alive until it's handled.
1981 msg.waitUntil(this.handlePush(msg.data.json()));
1982 }
1983 onClick(event) {
1984 // Handle the click event and keep the SW alive until it's handled.
1985 event.waitUntil(this.handleClick(event.notification, event.action));
1986 }
1987 handleMessage(msg, from) {
1988 return __awaiter$5(this, void 0, void 0, function* () {
1989 if (isMsgCheckForUpdates(msg)) {
1990 const action = (() => __awaiter$5(this, void 0, void 0, function* () { yield this.checkForUpdate(); }))();
1991 yield this.reportStatus(from, action, msg.statusNonce);
1992 }
1993 else if (isMsgActivateUpdate(msg)) {
1994 yield this.reportStatus(from, this.updateClient(from), msg.statusNonce);
1995 }
1996 });
1997 }
1998 handlePush(data) {
1999 return __awaiter$5(this, void 0, void 0, function* () {
2000 yield this.broadcast({
2001 type: 'PUSH',
2002 data,
2003 });
2004 if (!data.notification || !data.notification.title) {
2005 return;
2006 }
2007 const desc = data.notification;
2008 let options = {};
2009 NOTIFICATION_OPTION_NAMES.filter(name => desc.hasOwnProperty(name))
2010 .forEach(name => options[name] = desc[name]);
2011 yield this.scope.registration.showNotification(desc['title'], options);
2012 });
2013 }
2014 handleClick(notification, action) {
2015 return __awaiter$5(this, void 0, void 0, function* () {
2016 notification.close();
2017 const options = {};
2018 // The filter uses `name in notification` because the properties are on the prototype so
2019 // hasOwnProperty does not work here
2020 NOTIFICATION_OPTION_NAMES.filter(name => name in notification)
2021 .forEach(name => options[name] = notification[name]);
2022 yield this.broadcast({
2023 type: 'NOTIFICATION_CLICK',
2024 data: { action, notification: options },
2025 });
2026 });
2027 }
2028 reportStatus(client, promise, nonce) {
2029 return __awaiter$5(this, void 0, void 0, function* () {
2030 const response = { type: 'STATUS', nonce, status: true };
2031 try {
2032 yield promise;
2033 client.postMessage(response);
2034 }
2035 catch (e) {
2036 client.postMessage(Object.assign({}, response, { status: false, error: e.toString() }));
2037 }
2038 });
2039 }
2040 updateClient(client) {
2041 return __awaiter$5(this, void 0, void 0, function* () {
2042 // Figure out which version the client is on. If it's not on the latest,
2043 // it needs to be moved.
2044 const existing = this.clientVersionMap.get(client.id);
2045 if (existing === this.latestHash) {
2046 // Nothing to do, this client is already on the latest version.
2047 return;
2048 }
2049 // Switch the client over.
2050 let previous = undefined;
2051 // Look up the application data associated with the existing version. If there
2052 // isn't any, fall back on using the hash.
2053 if (existing !== undefined) {
2054 const existingVersion = this.versions.get(existing);
2055 previous = this.mergeHashWithAppData(existingVersion.manifest, existing);
2056 }
2057 // Set the current version used by the client, and sync the mapping to disk.
2058 this.clientVersionMap.set(client.id, this.latestHash);
2059 yield this.sync();
2060 // Notify the client about this activation.
2061 const current = this.versions.get(this.latestHash);
2062 const notice = {
2063 type: 'UPDATE_ACTIVATED',
2064 previous,
2065 current: this.mergeHashWithAppData(current.manifest, this.latestHash),
2066 };
2067 client.postMessage(notice);
2068 });
2069 }
2070 handleFetch(event) {
2071 return __awaiter$5(this, void 0, void 0, function* () {
2072 // Since the SW may have just been started, it may or may not have been initialized already.
2073 // this.initialized will be `null` if initialization has not yet been attempted, or will be a
2074 // Promise which will resolve (successfully or unsuccessfully) if it has.
2075 if (this.initialized === null) {
2076 // Initialization has not yet been attempted, so attempt it. This should only ever happen once
2077 // per SW instantiation.
2078 this.initialized = this.initialize();
2079 }
2080 // If initialization fails, the SW needs to enter a safe state, where it declines to respond to
2081 // network requests.
2082 try {
2083 // Wait for initialization.
2084 yield this.initialized;
2085 }
2086 catch (e) {
2087 // Initialization failed. Enter a safe state.
2088 this.state = DriverReadyState.SAFE_MODE;
2089 this.stateMessage = `Initialization failed due to error: ${errorToString(e)}`;
2090 // Even though the driver entered safe mode, background tasks still need to happen.
2091 event.waitUntil(this.idle.trigger());
2092 // Since the SW is already committed to responding to the currently active request,
2093 // respond with a network fetch.
2094 return this.safeFetch(event.request);
2095 }
2096 // On navigation requests, check for new updates.
2097 if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
2098 this.scheduledNavUpdateCheck = true;
2099 this.idle.schedule('check-updates-on-navigation', () => __awaiter$5(this, void 0, void 0, function* () {
2100 this.scheduledNavUpdateCheck = false;
2101 yield this.checkForUpdate();
2102 }));
2103 }
2104 // Decide which version of the app to use to serve this request. This is asynchronous as in
2105 // some cases, a record will need to be written to disk about the assignment that is made.
2106 const appVersion = yield this.assignVersion(event);
2107 // Bail out
2108 if (appVersion === null) {
2109 event.waitUntil(this.idle.trigger());
2110 return this.safeFetch(event.request);
2111 }
2112 let res = null;
2113 try {
2114 // Handle the request. First try the AppVersion. If that doesn't work, fall back on the
2115 // network.
2116 res = yield appVersion.handleFetch(event.request, event);
2117 }
2118 catch (err) {
2119 if (err.isCritical) {
2120 // Something went wrong with the activation of this version.
2121 yield this.versionFailed(appVersion, err, this.latestHash === appVersion.manifestHash);
2122 event.waitUntil(this.idle.trigger());
2123 return this.safeFetch(event.request);
2124 }
2125 throw err;
2126 }
2127 // The AppVersion will only return null if the manifest doesn't specify what to do about this
2128 // request. In that case, just fall back on the network.
2129 if (res === null) {
2130 event.waitUntil(this.idle.trigger());
2131 return this.safeFetch(event.request);
2132 }
2133 // Trigger the idle scheduling system. The Promise returned by trigger() will resolve after
2134 // a specific amount of time has passed. If trigger() hasn't been called again by then (e.g.
2135 // on a subsequent request), the idle task queue will be drained and the Promise won't resolve
2136 // until that operation is complete as well.
2137 event.waitUntil(this.idle.trigger());
2138 // The AppVersion returned a usable response, so return it.
2139 return res;
2140 });
2141 }
2142 /**
2143 * Attempt to quickly reach a state where it's safe to serve responses.
2144 */
2145 initialize() {
2146 return __awaiter$5(this, void 0, void 0, function* () {
2147 // On initialization, all of the serialized state is read out of the 'control'
2148 // table. This includes:
2149 // - map of hashes to manifests of currently loaded application versions
2150 // - map of client IDs to their pinned versions
2151 // - record of the most recently fetched manifest hash
2152 //
2153 // If these values don't exist in the DB, then this is the either the first time
2154 // the SW has run or the DB state has been wiped or is inconsistent. In that case,
2155 // load a fresh copy of the manifest and reset the state from scratch.
2156 // Open up the DB table.
2157 const table = yield this.db.open('control');
2158 // Attempt to load the needed state from the DB. If this fails, the catch {} block
2159 // will populate these variables with freshly constructed values.
2160 let manifests, assignments, latest;
2161 try {
2162 // Read them from the DB simultaneously.
2163 [manifests, assignments, latest] = yield Promise.all([
2164 table.read('manifests'),
2165 table.read('assignments'),
2166 table.read('latest'),
2167 ]);
2168 // Successfully loaded from saved state. This implies a manifest exists, so
2169 // the update check needs to happen in the background.
2170 this.idle.schedule('init post-load (update, cleanup)', () => __awaiter$5(this, void 0, void 0, function* () {
2171 yield this.checkForUpdate();
2172 try {
2173 yield this.cleanupCaches();
2174 }
2175 catch (err) {
2176 // Nothing to do - cleanup failed. Just log it.
2177 this.debugger.log(err, 'cleanupCaches @ init post-load');
2178 }
2179 }));
2180 }
2181 catch (_) {
2182 // Something went wrong. Try to start over by fetching a new manifest from the
2183 // server and building up an empty initial state.
2184 const manifest = yield this.fetchLatestManifest();
2185 const hash = hashManifest(manifest);
2186 manifests = {};
2187 manifests[hash] = manifest;
2188 assignments = {};
2189 latest = { latest: hash };
2190 // Save the initial state to the DB.
2191 yield Promise.all([
2192 table.write('manifests', manifests),
2193 table.write('assignments', assignments),
2194 table.write('latest', latest),
2195 ]);
2196 }
2197 // At this point, either the state has been loaded successfully, or fresh state
2198 // with a new copy of the manifest has been produced. At this point, the `Driver`
2199 // can have its internals hydrated from the state.
2200 // Initialize the `versions` map by setting each hash to a new `AppVersion` instance
2201 // for that manifest.
2202 Object.keys(manifests).forEach((hash) => {
2203 const manifest = manifests[hash];
2204 // If the manifest is newly initialized, an AppVersion may have already been
2205 // created for it.
2206 if (!this.versions.has(hash)) {
2207 this.versions.set(hash, new AppVersion(this.scope, this.adapter, this.db, this.idle, manifest, hash));
2208 }
2209 });
2210 // Map each client ID to its associated hash. Along the way, verify that the hash
2211 // is still valid for that client ID. It should not be possible for a client to
2212 // still be associated with a hash that was since removed from the state.
2213 Object.keys(assignments).forEach((clientId) => {
2214 const hash = assignments[clientId];
2215 if (this.versions.has(hash)) {
2216 this.clientVersionMap.set(clientId, hash);
2217 }
2218 else {
2219 this.clientVersionMap.set(clientId, latest.latest);
2220 this.debugger.log(`Unknown version ${hash} mapped for client ${clientId}, using latest instead`, `initialize: map assignments`);
2221 }
2222 });
2223 // Set the latest version.
2224 this.latestHash = latest.latest;
2225 // Finally, assert that the latest version is in fact loaded.
2226 if (!this.versions.has(latest.latest)) {
2227 throw new Error(`Invariant violated (initialize): latest hash ${latest.latest} has no known manifest`);
2228 }
2229 // Finally, wait for the scheduling of initialization of all versions in the
2230 // manifest. Ordinarily this just schedules the initializations to happen during
2231 // the next idle period, but in development mode this might actually wait for the
2232 // full initialization.
2233 // If any of these initializations fail, versionFailed() will be called either
2234 // synchronously or asynchronously to handle the failure and re-map clients.
2235 yield Promise.all(Object.keys(manifests).map((hash) => __awaiter$5(this, void 0, void 0, function* () {
2236 try {
2237 // Attempt to schedule or initialize this version. If this operation is
2238 // successful, then initialization either succeeded or was scheduled. If
2239 // it fails, then full initialization was attempted and failed.
2240 yield this.scheduleInitialization(this.versions.get(hash), this.latestHash === hash);
2241 }
2242 catch (err) {
2243 this.debugger.log(err, `initialize: schedule init of ${hash}`);
2244 return false;
2245 }
2246 })));
2247 });
2248 }
2249 lookupVersionByHash(hash, debugName = 'lookupVersionByHash') {
2250 // The version should exist, but check just in case.
2251 if (!this.versions.has(hash)) {
2252 throw new Error(`Invariant violated (${debugName}): want AppVersion for ${hash} but not loaded`);
2253 }
2254 return this.versions.get(hash);
2255 }
2256 /**
2257 * Decide which version of the manifest to use for the event.
2258 */
2259 assignVersion(event) {
2260 return __awaiter$5(this, void 0, void 0, function* () {
2261 // First, check whether the event has a (non empty) client ID. If it does, the version may
2262 // already be associated.
2263 const clientId = event.clientId;
2264 if (clientId) {
2265 // Check if there is an assigned client id.
2266 if (this.clientVersionMap.has(clientId)) {
2267 // There is an assignment for this client already.
2268 const hash = this.clientVersionMap.get(clientId);
2269 let appVersion = this.lookupVersionByHash(hash, 'assignVersion');
2270 // Ordinarily, this client would be served from its assigned version. But, if this
2271 // request is a navigation request, this client can be updated to the latest
2272 // version immediately.
2273 if (this.state === DriverReadyState.NORMAL && hash !== this.latestHash &&
2274 appVersion.isNavigationRequest(event.request)) {
2275 // Update this client to the latest version immediately.
2276 if (this.latestHash === null) {
2277 throw new Error(`Invariant violated (assignVersion): latestHash was null`);
2278 }
2279 const client = yield this.scope.clients.get(clientId);
2280 yield this.updateClient(client);
2281 appVersion = this.lookupVersionByHash(this.latestHash, 'assignVersion');
2282 }
2283 // TODO: make sure the version is valid.
2284 return appVersion;
2285 }
2286 else {
2287 // This is the first time this client ID has been seen. Whether the SW is in a
2288 // state to handle new clients depends on the current readiness state, so check
2289 // that first.
2290 if (this.state !== DriverReadyState.NORMAL) {
2291 // It's not safe to serve new clients in the current state. It's possible that
2292 // this is an existing client which has not been mapped yet (see below) but
2293 // even if that is the case, it's invalid to make an assignment to a known
2294 // invalid version, even if that assignment was previously implicit. Return
2295 // undefined here to let the caller know that no assignment is possible at
2296 // this time.
2297 return null;
2298 }
2299 // It's safe to handle this request. Two cases apply. Either:
2300 // 1) the browser assigned a client ID at the time of the navigation request, and
2301 // this is truly the first time seeing this client, or
2302 // 2) a navigation request came previously from the same client, but with no client
2303 // ID attached. Browsers do this to avoid creating a client under the origin in
2304 // the event the navigation request is just redirected.
2305 //
2306 // In case 1, the latest version can safely be used.
2307 // In case 2, the latest version can be used, with the assumption that the previous
2308 // navigation request was answered under the same version. This assumption relies
2309 // on the fact that it's unlikely an update will come in between the navigation
2310 // request and requests for subsequent resources on that page.
2311 // First validate the current state.
2312 if (this.latestHash === null) {
2313 throw new Error(`Invariant violated (assignVersion): latestHash was null`);
2314 }
2315 // Pin this client ID to the current latest version, indefinitely.
2316 this.clientVersionMap.set(clientId, this.latestHash);
2317 yield this.sync();
2318 // Return the latest `AppVersion`.
2319 return this.lookupVersionByHash(this.latestHash, 'assignVersion');
2320 }
2321 }
2322 else {
2323 // No client ID was associated with the request. This must be a navigation request
2324 // for a new client. First check that the SW is accepting new clients.
2325 if (this.state !== DriverReadyState.NORMAL) {
2326 return null;
2327 }
2328 // Serve it with the latest version, and assume that the client will actually get
2329 // associated with that version on the next request.
2330 // First validate the current state.
2331 if (this.latestHash === null) {
2332 throw new Error(`Invariant violated (assignVersion): latestHash was null`);
2333 }
2334 // Return the latest `AppVersion`.
2335 return this.lookupVersionByHash(this.latestHash, 'assignVersion');
2336 }
2337 });
2338 }
2339 fetchLatestManifest(ignoreOfflineError = false) {
2340 return __awaiter$5(this, void 0, void 0, function* () {
2341 const res = yield this.safeFetch(this.adapter.newRequest('ngsw.json?ngsw-cache-bust=' + Math.random()));
2342 if (!res.ok) {
2343 if (res.status === 404) {
2344 yield this.deleteAllCaches();
2345 yield this.scope.registration.unregister();
2346 }
2347 else if (res.status === 504 && ignoreOfflineError) {
2348 return null;
2349 }
2350 throw new Error(`Manifest fetch failed! (status: ${res.status})`);
2351 }
2352 this.lastUpdateCheck = this.adapter.time;
2353 return res.json();
2354 });
2355 }
2356 deleteAllCaches() {
2357 return __awaiter$5(this, void 0, void 0, function* () {
2358 yield (yield this.scope.caches.keys())
2359 .filter(key => key.startsWith('ngsw:'))
2360 .reduce((previous, key) => __awaiter$5(this, void 0, void 0, function* () {
2361 yield Promise.all([
2362 previous,
2363 this.scope.caches.delete(key),
2364 ]);
2365 }), Promise.resolve());
2366 });
2367 }
2368 /**
2369 * Schedule the SW's attempt to reach a fully prefetched state for the given AppVersion
2370 * when the SW is not busy and has connectivity. This returns a Promise which must be
2371 * awaited, as under some conditions the AppVersion might be initialized immediately.
2372 */
2373 scheduleInitialization(appVersion, latest) {
2374 return __awaiter$5(this, void 0, void 0, function* () {
2375 const initialize = () => __awaiter$5(this, void 0, void 0, function* () {
2376 try {
2377 yield appVersion.initializeFully();
2378 }
2379 catch (err) {
2380 this.debugger.log(err, `initializeFully for ${appVersion.manifestHash}`);
2381 yield this.versionFailed(appVersion, err, latest);
2382 }
2383 });
2384 // TODO: better logic for detecting localhost.
2385 if (this.scope.registration.scope.indexOf('://localhost') > -1) {
2386 return initialize();
2387 }
2388 this.idle.schedule(`initialization(${appVersion.manifestHash})`, initialize);
2389 });
2390 }
2391 versionFailed(appVersion, err, latest) {
2392 return __awaiter$5(this, void 0, void 0, function* () {
2393 // This particular AppVersion is broken. First, find the manifest hash.
2394 const broken = Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
2395 if (broken === undefined) {
2396 // This version is no longer in use anyway, so nobody cares.
2397 return;
2398 }
2399 const brokenHash = broken[0];
2400 // TODO: notify affected apps.
2401 // The action taken depends on whether the broken manifest is the active (latest) or not.
2402 // If so, the SW cannot accept new clients, but can continue to service old ones.
2403 if (this.latestHash === brokenHash || latest) {
2404 // The latest manifest is broken. This means that new clients are at the mercy of the
2405 // network, but caches continue to be valid for previous versions. This is
2406 // unfortunate but unavoidable.
2407 this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
2408 this.stateMessage = `Degraded due to: ${errorToString(err)}`;
2409 // Cancel the binding for these clients.
2410 Array.from(this.clientVersionMap.keys())
2411 .forEach(clientId => this.clientVersionMap.delete(clientId));
2412 }
2413 else {
2414 // The current version is viable, but this older version isn't. The only
2415 // possible remedy is to stop serving the older version and go to the network.
2416 // Figure out which clients are affected and put them on the latest.
2417 const affectedClients = Array.from(this.clientVersionMap.keys())
2418 .filter(clientId => this.clientVersionMap.get(clientId) === brokenHash);
2419 // Push the affected clients onto the latest version.
2420 affectedClients.forEach(clientId => this.clientVersionMap.set(clientId, this.latestHash));
2421 }
2422 try {
2423 yield this.sync();
2424 }
2425 catch (err2) {
2426 // We are already in a bad state. No need to make things worse.
2427 // Just log the error and move on.
2428 this.debugger.log(err2, `Driver.versionFailed(${err.message || err})`);
2429 }
2430 });
2431 }
2432 setupUpdate(manifest, hash) {
2433 return __awaiter$5(this, void 0, void 0, function* () {
2434 const newVersion = new AppVersion(this.scope, this.adapter, this.db, this.idle, manifest, hash);
2435 // Firstly, check if the manifest version is correct.
2436 if (manifest.configVersion !== SUPPORTED_CONFIG_VERSION) {
2437 yield this.deleteAllCaches();
2438 yield this.scope.registration.unregister();
2439 throw new Error(`Invalid config version: expected ${SUPPORTED_CONFIG_VERSION}, got ${manifest.configVersion}.`);
2440 }
2441 // Cause the new version to become fully initialized. If this fails, then the
2442 // version will not be available for use.
2443 yield newVersion.initializeFully(this);
2444 // Install this as an active version of the app.
2445 this.versions.set(hash, newVersion);
2446 // Future new clients will use this hash as the latest version.
2447 this.latestHash = hash;
2448 yield this.sync();
2449 yield this.notifyClientsAboutUpdate();
2450 });
2451 }
2452 checkForUpdate() {
2453 return __awaiter$5(this, void 0, void 0, function* () {
2454 let hash = '(unknown)';
2455 try {
2456 const manifest = yield this.fetchLatestManifest(true);
2457 if (manifest === null) {
2458 // Client or server offline. Unable to check for updates at this time.
2459 // Continue to service clients (existing and new).
2460 this.debugger.log('Check for update aborted. (Client or server offline.)');
2461 return false;
2462 }
2463 hash = hashManifest(manifest);
2464 // Check whether this is really an update.
2465 if (this.versions.has(hash)) {
2466 return false;
2467 }
2468 yield this.setupUpdate(manifest, hash);
2469 return true;
2470 }
2471 catch (err) {
2472 this.debugger.log(err, `Error occurred while updating to manifest ${hash}`);
2473 this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
2474 this.stateMessage = `Degraded due to failed initialization: ${errorToString(err)}`;
2475 return false;
2476 }
2477 });
2478 }
2479 /**
2480 * Synchronize the existing state to the underlying database.
2481 */
2482 sync() {
2483 return __awaiter$5(this, void 0, void 0, function* () {
2484 // Open up the DB table.
2485 const table = yield this.db.open('control');
2486 // Construct a serializable map of hashes to manifests.
2487 const manifests = {};
2488 this.versions.forEach((version, hash) => { manifests[hash] = version.manifest; });
2489 // Construct a serializable map of client ids to version hashes.
2490 const assignments = {};
2491 this.clientVersionMap.forEach((hash, clientId) => { assignments[clientId] = hash; });
2492 // Record the latest entry. Since this is a sync which is necessarily happening after
2493 // initialization, latestHash should always be valid.
2494 const latest = {
2495 latest: this.latestHash,
2496 };
2497 // Synchronize all of these.
2498 yield Promise.all([
2499 table.write('manifests', manifests),
2500 table.write('assignments', assignments),
2501 table.write('latest', latest),
2502 ]);
2503 });
2504 }
2505 cleanupCaches() {
2506 return __awaiter$5(this, void 0, void 0, function* () {
2507 // Query for all currently active clients, and list the client ids. This may skip
2508 // some clients in the browser back-forward cache, but not much can be done about
2509 // that.
2510 const activeClients = (yield this.scope.clients.matchAll()).map(client => client.id);
2511 // A simple list of client ids that the SW has kept track of. Subtracting
2512 // activeClients from this list will result in the set of client ids which are
2513 // being tracked but are no longer used in the browser, and thus can be cleaned up.
2514 const knownClients = Array.from(this.clientVersionMap.keys());
2515 // Remove clients in the clientVersionMap that are no longer active.
2516 knownClients.filter(id => activeClients.indexOf(id) === -1)
2517 .forEach(id => this.clientVersionMap.delete(id));
2518 // Next, determine the set of versions which are still used. All others can be
2519 // removed.
2520 const usedVersions = new Set();
2521 this.clientVersionMap.forEach((version, _) => usedVersions.add(version));
2522 // Collect all obsolete versions by filtering out used versions from the set of all versions.
2523 const obsoleteVersions = Array.from(this.versions.keys())
2524 .filter(version => !usedVersions.has(version) && version !== this.latestHash);
2525 // Remove all the versions which are no longer used.
2526 yield obsoleteVersions.reduce((previous, version) => __awaiter$5(this, void 0, void 0, function* () {
2527 // Wait for the other cleanup operations to complete.
2528 yield previous;
2529 // Try to get past the failure of one particular version to clean up (this
2530 // shouldn't happen, but handle it just in case).
2531 try {
2532 // Get ahold of the AppVersion for this particular hash.
2533 const instance = this.versions.get(version);
2534 // Delete it from the canonical map.
2535 this.versions.delete(version);
2536 // Clean it up.
2537 yield instance.cleanup();
2538 }
2539 catch (err) {
2540 // Oh well? Not much that can be done here. These caches will be removed when
2541 // the SW revs its format version, which happens from time to time.
2542 this.debugger.log(err, `cleanupCaches - cleanup ${version}`);
2543 }
2544 }), Promise.resolve());
2545 // Commit all the changes to the saved state.
2546 yield this.sync();
2547 });
2548 }
2549 /**
2550 * Delete caches that were used by older versions of `@angular/service-worker` to avoid running
2551 * into storage quota limitations imposed by browsers.
2552 * (Since at this point the SW has claimed all clients, it is safe to remove those caches.)
2553 */
2554 cleanupOldSwCaches() {
2555 return __awaiter$5(this, void 0, void 0, function* () {
2556 const cacheNames = yield this.scope.caches.keys();
2557 const oldSwCacheNames = cacheNames.filter(name => /^ngsw:(?:active|staged|manifest:.+)$/.test(name));
2558 yield Promise.all(oldSwCacheNames.map(name => this.scope.caches.delete(name)));
2559 });
2560 }
2561 /**
2562 * Determine if a specific version of the given resource is cached anywhere within the SW,
2563 * and fetch it if so.
2564 */
2565 lookupResourceWithHash(url, hash) {
2566 return Array
2567 // Scan through the set of all cached versions, valid or otherwise. It's safe to do such
2568 // lookups even for invalid versions as the cached version of a resource will have the
2569 // same hash regardless.
2570 .from(this.versions.values())
2571 // Reduce the set of versions to a single potential result. At any point along the
2572 // reduction, if a response has already been identified, then pass it through, as no
2573 // future operation could change the response. If no response has been found yet, keep
2574 // checking versions until one is or until all versions have been exhausted.
2575 .reduce((prev, version) => __awaiter$5(this, void 0, void 0, function* () {
2576 // First, check the previous result. If a non-null result has been found already, just
2577 // return it.
2578 if ((yield prev) !== null) {
2579 return prev;
2580 }
2581 // No result has been found yet. Try the next `AppVersion`.
2582 return version.lookupResourceWithHash(url, hash);
2583 }), Promise.resolve(null));
2584 }
2585 lookupResourceWithoutHash(url) {
2586 return __awaiter$5(this, void 0, void 0, function* () {
2587 yield this.initialized;
2588 const version = this.versions.get(this.latestHash);
2589 return version.lookupResourceWithoutHash(url);
2590 });
2591 }
2592 previouslyCachedResources() {
2593 return __awaiter$5(this, void 0, void 0, function* () {
2594 yield this.initialized;
2595 const version = this.versions.get(this.latestHash);
2596 return version.previouslyCachedResources();
2597 });
2598 }
2599 recentCacheStatus(url) {
2600 const version = this.versions.get(this.latestHash);
2601 return version.recentCacheStatus(url);
2602 }
2603 mergeHashWithAppData(manifest, hash) {
2604 return {
2605 hash,
2606 appData: manifest.appData,
2607 };
2608 }
2609 notifyClientsAboutUpdate() {
2610 return __awaiter$5(this, void 0, void 0, function* () {
2611 yield this.initialized;
2612 const clients = yield this.scope.clients.matchAll();
2613 const next = this.versions.get(this.latestHash);
2614 yield clients.reduce((previous, client) => __awaiter$5(this, void 0, void 0, function* () {
2615 yield previous;
2616 // Firstly, determine which version this client is on.
2617 const version = this.clientVersionMap.get(client.id);
2618 if (version === undefined) {
2619 // Unmapped client - assume it's the latest.
2620 return;
2621 }
2622 if (version === this.latestHash) {
2623 // Client is already on the latest version, no need for a notification.
2624 return;
2625 }
2626 const current = this.versions.get(version);
2627 // Send a notice.
2628 const notice = {
2629 type: 'UPDATE_AVAILABLE',
2630 current: this.mergeHashWithAppData(current.manifest, version),
2631 available: this.mergeHashWithAppData(next.manifest, this.latestHash),
2632 };
2633 client.postMessage(notice);
2634 }), Promise.resolve());
2635 });
2636 }
2637 broadcast(msg) {
2638 return __awaiter$5(this, void 0, void 0, function* () {
2639 const clients = yield this.scope.clients.matchAll();
2640 clients.forEach(client => { client.postMessage(msg); });
2641 });
2642 }
2643 debugState() {
2644 return __awaiter$5(this, void 0, void 0, function* () {
2645 return {
2646 state: DriverReadyState[this.state],
2647 why: this.stateMessage,
2648 latestHash: this.latestHash,
2649 lastUpdateCheck: this.lastUpdateCheck,
2650 };
2651 });
2652 }
2653 debugVersions() {
2654 return __awaiter$5(this, void 0, void 0, function* () {
2655 // Build list of versions.
2656 return Array.from(this.versions.keys()).map(hash => {
2657 const version = this.versions.get(hash);
2658 const clients = Array.from(this.clientVersionMap.entries())
2659 .filter(([clientId, version]) => version === hash)
2660 .map(([clientId, version]) => clientId);
2661 return {
2662 hash,
2663 manifest: version.manifest, clients,
2664 status: '',
2665 };
2666 });
2667 });
2668 }
2669 debugIdleState() {
2670 return __awaiter$5(this, void 0, void 0, function* () {
2671 return {
2672 queue: this.idle.taskDescriptions,
2673 lastTrigger: this.idle.lastTrigger,
2674 lastRun: this.idle.lastRun,
2675 };
2676 });
2677 }
2678 safeFetch(req) {
2679 return __awaiter$5(this, void 0, void 0, function* () {
2680 try {
2681 return yield this.scope.fetch(req);
2682 }
2683 catch (err) {
2684 this.debugger.log(err, `Driver.fetch(${req.url})`);
2685 return this.adapter.newResponse(null, {
2686 status: 504,
2687 statusText: 'Gateway Timeout',
2688 });
2689 }
2690 });
2691 }
2692 }
2693
2694 /**
2695 * @license
2696 * Copyright Google Inc. All Rights Reserved.
2697 *
2698 * Use of this source code is governed by an MIT-style license that can be
2699 * found in the LICENSE file at https://angular.io/license
2700 */
2701 const scope = self;
2702 const adapter = new Adapter();
2703 const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));
2704
2705}());