UNPKG

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