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