1 | (function () {
|
2 | ;
|
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 |
|
1696 | Driver state: ${state.state} (${state.why})
|
1697 | Latest manifest hash: ${state.latestHash || 'none'}
|
1698 | Last update check: ${this.since(state.lastUpdateCheck)}`;
|
1699 | const msgVersions = versions
|
1700 | .map(version => `=== Version ${version.hash} ===
|
1701 |
|
1702 | Clients: ${version.clients.join(', ')}`)
|
1703 | .join('\n\n');
|
1704 | const msgIdle = `=== Idle Task Queue ===
|
1705 | Last update tick: ${this.since(idle.lastTrigger)}
|
1706 | Last update run: ${this.since(idle.lastRun)}
|
1707 | Task queue:
|
1708 | ${idle.queue.map(v => ' * ' + v).join('\n')}
|
1709 |
|
1710 | Debug 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 | }());
|