UNPKG

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