dealposbarcode 1.1.7 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/barcode/269.3d862488bed00387.js +1 -0
- package/barcode/3rdpartylicenses.txt +1 -4
- package/barcode/index.html +23 -24
- package/barcode/main.582657251d4457f0.js +1 -0
- package/barcode/ngsw-worker.js +1496 -1532
- package/barcode/ngsw.json +13 -11
- package/barcode/polyfills.3eeaafac055ac6b7.js +1 -0
- package/barcode/runtime.d51cecd29bc19a00.js +1 -0
- package/barcode/safety-worker.js +8 -2
- package/barcode/styles.43c2c34807124c9d.css +6 -0
- package/barcode/worker-basic.min.js +8 -2
- package/package.json +1 -1
- package/barcode/4.be4a7b6ecc537fbdaa10.js +0 -1
- package/barcode/main.f1b04443f4ff9ab38e78.js +0 -1
- package/barcode/polyfills.94daefd414b8355106ab.js +0 -1
- package/barcode/runtime.a39762fd0a739b314827.js +0 -1
- package/barcode/styles.0bf721a144e37840b164.css +0 -22
package/barcode/ngsw-worker.js
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
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
|
+
* A wrapper around `CacheStorage` to allow interacting with caches more easily and consistently by:
|
|
13
|
+
* - Adding a `name` property to all opened caches, which can be used to easily perform other
|
|
14
|
+
* operations that require the cache name.
|
|
15
|
+
* - Name-spacing cache names to avoid conflicts with other caches on the same domain.
|
|
16
|
+
*/
|
|
17
|
+
class NamedCacheStorage {
|
|
18
|
+
constructor(original, cacheNamePrefix) {
|
|
19
|
+
this.original = original;
|
|
20
|
+
this.cacheNamePrefix = cacheNamePrefix;
|
|
21
|
+
}
|
|
22
|
+
delete(cacheName) {
|
|
23
|
+
return this.original.delete(`${this.cacheNamePrefix}:${cacheName}`);
|
|
24
|
+
}
|
|
25
|
+
has(cacheName) {
|
|
26
|
+
return this.original.has(`${this.cacheNamePrefix}:${cacheName}`);
|
|
27
|
+
}
|
|
28
|
+
async keys() {
|
|
29
|
+
const prefix = `${this.cacheNamePrefix}:`;
|
|
30
|
+
const allCacheNames = await this.original.keys();
|
|
31
|
+
const ownCacheNames = allCacheNames.filter(name => name.startsWith(prefix));
|
|
32
|
+
return ownCacheNames.map(name => name.slice(prefix.length));
|
|
33
|
+
}
|
|
34
|
+
match(request, options) {
|
|
35
|
+
return this.original.match(request, options);
|
|
36
|
+
}
|
|
37
|
+
async open(cacheName) {
|
|
38
|
+
const cache = await this.original.open(`${this.cacheNamePrefix}:${cacheName}`);
|
|
39
|
+
return Object.assign(cache, { name: cacheName });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
4
43
|
/**
|
|
5
44
|
* @license
|
|
6
45
|
* Copyright Google LLC All Rights Reserved.
|
|
@@ -15,15 +54,15 @@
|
|
|
15
54
|
* from the global scope.
|
|
16
55
|
*/
|
|
17
56
|
class Adapter {
|
|
18
|
-
constructor(scopeUrl) {
|
|
57
|
+
constructor(scopeUrl, caches) {
|
|
19
58
|
this.scopeUrl = scopeUrl;
|
|
20
59
|
const parsedScopeUrl = this.parseUrl(this.scopeUrl);
|
|
21
60
|
// Determine the origin from the registration scope. This is used to differentiate between
|
|
22
61
|
// relative and absolute URLs.
|
|
23
62
|
this.origin = parsedScopeUrl.origin;
|
|
24
|
-
//
|
|
25
|
-
// scopes on the same domain.
|
|
26
|
-
this.
|
|
63
|
+
// Use the baseHref in the cache name prefix to avoid clash of cache names for SWs with
|
|
64
|
+
// different scopes on the same domain.
|
|
65
|
+
this.caches = new NamedCacheStorage(caches, `ngsw:${parsedScopeUrl.path}`);
|
|
27
66
|
}
|
|
28
67
|
/**
|
|
29
68
|
* Wrapper around the `Request` constructor.
|
|
@@ -120,24 +159,29 @@
|
|
|
120
159
|
* state within mock `Response` objects.
|
|
121
160
|
*/
|
|
122
161
|
class CacheDatabase {
|
|
123
|
-
constructor(
|
|
124
|
-
this.scope = scope;
|
|
162
|
+
constructor(adapter) {
|
|
125
163
|
this.adapter = adapter;
|
|
164
|
+
this.cacheNamePrefix = 'db';
|
|
126
165
|
this.tables = new Map();
|
|
127
166
|
}
|
|
128
167
|
'delete'(name) {
|
|
129
168
|
if (this.tables.has(name)) {
|
|
130
169
|
this.tables.delete(name);
|
|
131
170
|
}
|
|
132
|
-
return this.
|
|
171
|
+
return this.adapter.caches.delete(`${this.cacheNamePrefix}:${name}`);
|
|
133
172
|
}
|
|
134
|
-
list() {
|
|
135
|
-
|
|
173
|
+
async list() {
|
|
174
|
+
const prefix = `${this.cacheNamePrefix}:`;
|
|
175
|
+
const allCacheNames = await this.adapter.caches.keys();
|
|
176
|
+
const dbCacheNames = allCacheNames.filter(name => name.startsWith(prefix));
|
|
177
|
+
// Return the un-prefixed table names, so they can be used with other `CacheDatabase` methods
|
|
178
|
+
// (for example, for opening/deleting a table).
|
|
179
|
+
return dbCacheNames.map(name => name.slice(prefix.length));
|
|
136
180
|
}
|
|
137
|
-
open(name, cacheQueryOptions) {
|
|
181
|
+
async open(name, cacheQueryOptions) {
|
|
138
182
|
if (!this.tables.has(name)) {
|
|
139
|
-
const
|
|
140
|
-
|
|
183
|
+
const cache = await this.adapter.caches.open(`${this.cacheNamePrefix}:${name}`);
|
|
184
|
+
const table = new CacheTable(name, cache, this.adapter, cacheQueryOptions);
|
|
141
185
|
this.tables.set(name, table);
|
|
142
186
|
}
|
|
143
187
|
return this.tables.get(name);
|
|
@@ -147,11 +191,12 @@
|
|
|
147
191
|
* A `Table` backed by a `Cache`.
|
|
148
192
|
*/
|
|
149
193
|
class CacheTable {
|
|
150
|
-
constructor(
|
|
151
|
-
this.
|
|
194
|
+
constructor(name, cache, adapter, cacheQueryOptions) {
|
|
195
|
+
this.name = name;
|
|
152
196
|
this.cache = cache;
|
|
153
197
|
this.adapter = adapter;
|
|
154
198
|
this.cacheQueryOptions = cacheQueryOptions;
|
|
199
|
+
this.cacheName = this.cache.name;
|
|
155
200
|
}
|
|
156
201
|
request(key) {
|
|
157
202
|
return this.adapter.newRequest('/' + key);
|
|
@@ -165,7 +210,7 @@
|
|
|
165
210
|
read(key) {
|
|
166
211
|
return this.cache.match(this.request(key), this.cacheQueryOptions).then(res => {
|
|
167
212
|
if (res === undefined) {
|
|
168
|
-
return Promise.reject(new NotFound(this.
|
|
213
|
+
return Promise.reject(new NotFound(this.name, key));
|
|
169
214
|
}
|
|
170
215
|
return res.json();
|
|
171
216
|
});
|
|
@@ -175,40 +220,6 @@
|
|
|
175
220
|
}
|
|
176
221
|
}
|
|
177
222
|
|
|
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
223
|
/**
|
|
213
224
|
* @license
|
|
214
225
|
* Copyright Google LLC All Rights Reserved.
|
|
@@ -399,14 +410,13 @@
|
|
|
399
410
|
* Concrete classes derive from this base and specify the exact caching policy.
|
|
400
411
|
*/
|
|
401
412
|
class AssetGroup {
|
|
402
|
-
constructor(scope, adapter, idle, config, hashes, db,
|
|
413
|
+
constructor(scope, adapter, idle, config, hashes, db, cacheNamePrefix) {
|
|
403
414
|
this.scope = scope;
|
|
404
415
|
this.adapter = adapter;
|
|
405
416
|
this.idle = idle;
|
|
406
417
|
this.config = config;
|
|
407
418
|
this.hashes = hashes;
|
|
408
419
|
this.db = db;
|
|
409
|
-
this.prefix = prefix;
|
|
410
420
|
/**
|
|
411
421
|
* A deduplication cache, to make sure the SW never makes two network requests
|
|
412
422
|
* for the same resource at once. Managed by `fetchAndCacheOnce`.
|
|
@@ -426,384 +436,365 @@
|
|
|
426
436
|
// Patterns in the config are regular expressions disguised as strings. Breathe life into them.
|
|
427
437
|
this.patterns = config.patterns.map(pattern => new RegExp(pattern));
|
|
428
438
|
// This is the primary cache, which holds all of the cached requests for this group. If a
|
|
429
|
-
// resource
|
|
430
|
-
|
|
431
|
-
this.cache = scope.caches.open(`${this.prefix}:${config.name}:cache`);
|
|
439
|
+
// resource isn't in this cache, it hasn't been fetched yet.
|
|
440
|
+
this.cache = adapter.caches.open(`${cacheNamePrefix}:${config.name}:cache`);
|
|
432
441
|
// This is the metadata table, which holds specific information for each cached URL, such as
|
|
433
442
|
// the timestamp of when it was added to the cache.
|
|
434
|
-
this.metadata =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
catch (_) {
|
|
452
|
-
// Error on the side of safety and assume cached.
|
|
443
|
+
this.metadata =
|
|
444
|
+
this.db.open(`${cacheNamePrefix}:${config.name}:meta`, config.cacheQueryOptions);
|
|
445
|
+
}
|
|
446
|
+
async cacheStatus(url) {
|
|
447
|
+
const cache = await this.cache;
|
|
448
|
+
const meta = await this.metadata;
|
|
449
|
+
const req = this.adapter.newRequest(url);
|
|
450
|
+
const res = await cache.match(req, this.config.cacheQueryOptions);
|
|
451
|
+
if (res === undefined) {
|
|
452
|
+
return UpdateCacheStatus.NOT_CACHED;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const data = await meta.read(req.url);
|
|
456
|
+
if (!data.used) {
|
|
457
|
+
return UpdateCacheStatus.CACHED_BUT_UNUSED;
|
|
453
458
|
}
|
|
454
|
-
|
|
455
|
-
|
|
459
|
+
}
|
|
460
|
+
catch (_) {
|
|
461
|
+
// Error on the side of safety and assume cached.
|
|
462
|
+
}
|
|
463
|
+
return UpdateCacheStatus.CACHED;
|
|
456
464
|
}
|
|
457
465
|
/**
|
|
458
|
-
*
|
|
466
|
+
* Return a list of the names of all caches used by this group.
|
|
459
467
|
*/
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
468
|
+
async getCacheNames() {
|
|
469
|
+
const [cache, metadata] = await Promise.all([
|
|
470
|
+
this.cache,
|
|
471
|
+
this.metadata,
|
|
472
|
+
]);
|
|
473
|
+
return [cache.name, metadata.cacheName];
|
|
465
474
|
}
|
|
466
475
|
/**
|
|
467
476
|
* Process a request for a given resource and return it, or return null if it's not available.
|
|
468
477
|
*/
|
|
469
|
-
handleFetch(req,
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
this.
|
|
496
|
-
|
|
497
|
-
}));
|
|
498
|
-
}
|
|
499
|
-
// In either case (revalidation or not), the cached response must be good.
|
|
500
|
-
return cachedResponse;
|
|
478
|
+
async handleFetch(req, _event) {
|
|
479
|
+
const url = this.adapter.normalizeUrl(req.url);
|
|
480
|
+
// Either the request matches one of the known resource URLs, one of the patterns for
|
|
481
|
+
// dynamically matched URLs, or neither. Determine which is the case for this request in
|
|
482
|
+
// order to decide how to handle it.
|
|
483
|
+
if (this.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url))) {
|
|
484
|
+
// This URL matches a known resource. Either it's been cached already or it's missing, in
|
|
485
|
+
// which case it needs to be loaded from the network.
|
|
486
|
+
// Open the cache to check whether this resource is present.
|
|
487
|
+
const cache = await this.cache;
|
|
488
|
+
// Look for a cached response. If one exists, it can be used to resolve the fetch
|
|
489
|
+
// operation.
|
|
490
|
+
const cachedResponse = await cache.match(req, this.config.cacheQueryOptions);
|
|
491
|
+
if (cachedResponse !== undefined) {
|
|
492
|
+
// A response has already been cached (which presumably matches the hash for this
|
|
493
|
+
// resource). Check whether it's safe to serve this resource from cache.
|
|
494
|
+
if (this.hashes.has(url)) {
|
|
495
|
+
// This resource has a hash, and thus is versioned by the manifest. It's safe to return
|
|
496
|
+
// the response.
|
|
497
|
+
return cachedResponse;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// This resource has no hash, and yet exists in the cache. Check how old this request is
|
|
501
|
+
// to make sure it's still usable.
|
|
502
|
+
if (await this.needToRevalidate(req, cachedResponse)) {
|
|
503
|
+
this.idle.schedule(`revalidate(${cache.name}): ${req.url}`, async () => {
|
|
504
|
+
await this.fetchAndCacheOnce(req);
|
|
505
|
+
});
|
|
501
506
|
}
|
|
507
|
+
// In either case (revalidation or not), the cached response must be good.
|
|
508
|
+
return cachedResponse;
|
|
502
509
|
}
|
|
503
|
-
// No already-cached response exists, so attempt a fetch/cache operation. The original request
|
|
504
|
-
// may specify things like credential inclusion, but for assets these are not honored in order
|
|
505
|
-
// to avoid issues with opaque responses. The SW requests the data itself.
|
|
506
|
-
const res = yield this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
|
|
507
|
-
// If this is successful, the response needs to be cloned as it might be used to respond to
|
|
508
|
-
// multiple fetch operations at the same time.
|
|
509
|
-
return res.clone();
|
|
510
|
-
}
|
|
511
|
-
else {
|
|
512
|
-
return null;
|
|
513
510
|
}
|
|
514
|
-
|
|
511
|
+
// No already-cached response exists, so attempt a fetch/cache operation. The original request
|
|
512
|
+
// may specify things like credential inclusion, but for assets these are not honored in order
|
|
513
|
+
// to avoid issues with opaque responses. The SW requests the data itself.
|
|
514
|
+
const res = await this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
|
|
515
|
+
// If this is successful, the response needs to be cloned as it might be used to respond to
|
|
516
|
+
// multiple fetch operations at the same time.
|
|
517
|
+
return res.clone();
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
515
522
|
}
|
|
516
523
|
/**
|
|
517
524
|
* Some resources are cached without a hash, meaning that their expiration is controlled
|
|
518
525
|
* by HTTP caching headers. Check whether the given request/response pair is still valid
|
|
519
526
|
* per the caching headers.
|
|
520
527
|
*/
|
|
521
|
-
needToRevalidate(req, res) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
const maxAge = 1000 * parseInt(cacheAge);
|
|
548
|
-
// Determine the origin time of this request. If the SW has metadata on the request (which
|
|
549
|
-
// it
|
|
550
|
-
// should), it will have the time the request was added to the cache. If it doesn't for some
|
|
551
|
-
// reason, the request may have a Date header which will serve the same purpose.
|
|
552
|
-
let ts;
|
|
553
|
-
try {
|
|
554
|
-
// Check the metadata table. If a timestamp is there, use it.
|
|
555
|
-
const metaTable = yield this.metadata;
|
|
556
|
-
ts = (yield metaTable.read(req.url)).ts;
|
|
557
|
-
}
|
|
558
|
-
catch (_a) {
|
|
559
|
-
// Otherwise, look for a Date header.
|
|
560
|
-
const date = res.headers.get('Date');
|
|
561
|
-
if (date === null) {
|
|
562
|
-
// Unable to determine when this response was created. Assume that it's stale, and
|
|
563
|
-
// revalidate it.
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
ts = Date.parse(date);
|
|
567
|
-
}
|
|
568
|
-
const age = this.adapter.time - ts;
|
|
569
|
-
return age < 0 || age > maxAge;
|
|
570
|
-
}
|
|
571
|
-
catch (_b) {
|
|
572
|
-
// Assume stale.
|
|
573
|
-
return true;
|
|
574
|
-
}
|
|
528
|
+
async needToRevalidate(req, res) {
|
|
529
|
+
// Three different strategies apply here:
|
|
530
|
+
// 1) The request has a Cache-Control header, and thus expiration needs to be based on its age.
|
|
531
|
+
// 2) The request has an Expires header, and expiration is based on the current timestamp.
|
|
532
|
+
// 3) The request has no applicable caching headers, and must be revalidated.
|
|
533
|
+
if (res.headers.has('Cache-Control')) {
|
|
534
|
+
// Figure out if there is a max-age directive in the Cache-Control header.
|
|
535
|
+
const cacheControl = res.headers.get('Cache-Control');
|
|
536
|
+
const cacheDirectives = cacheControl
|
|
537
|
+
// Directives are comma-separated within the Cache-Control header value.
|
|
538
|
+
.split(',')
|
|
539
|
+
// Make sure each directive doesn't have extraneous whitespace.
|
|
540
|
+
.map(v => v.trim())
|
|
541
|
+
// Some directives have values (like maxage and s-maxage)
|
|
542
|
+
.map(v => v.split('='));
|
|
543
|
+
// Lowercase all the directive names.
|
|
544
|
+
cacheDirectives.forEach(v => v[0] = v[0].toLowerCase());
|
|
545
|
+
// Find the max-age directive, if one exists.
|
|
546
|
+
const maxAgeDirective = cacheDirectives.find(v => v[0] === 'max-age');
|
|
547
|
+
const cacheAge = maxAgeDirective ? maxAgeDirective[1] : undefined;
|
|
548
|
+
if (!cacheAge) {
|
|
549
|
+
// No usable TTL defined. Must assume that the response is stale.
|
|
550
|
+
return true;
|
|
575
551
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
552
|
+
try {
|
|
553
|
+
const maxAge = 1000 * parseInt(cacheAge);
|
|
554
|
+
// Determine the origin time of this request. If the SW has metadata on the request (which
|
|
555
|
+
// it
|
|
556
|
+
// should), it will have the time the request was added to the cache. If it doesn't for some
|
|
557
|
+
// reason, the request may have a Date header which will serve the same purpose.
|
|
558
|
+
let ts;
|
|
579
559
|
try {
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
560
|
+
// Check the metadata table. If a timestamp is there, use it.
|
|
561
|
+
const metaTable = await this.metadata;
|
|
562
|
+
ts = (await metaTable.read(req.url)).ts;
|
|
583
563
|
}
|
|
584
|
-
catch
|
|
585
|
-
//
|
|
586
|
-
|
|
564
|
+
catch {
|
|
565
|
+
// Otherwise, look for a Date header.
|
|
566
|
+
const date = res.headers.get('Date');
|
|
567
|
+
if (date === null) {
|
|
568
|
+
// Unable to determine when this response was created. Assume that it's stale, and
|
|
569
|
+
// revalidate it.
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
ts = Date.parse(date);
|
|
587
573
|
}
|
|
574
|
+
const age = this.adapter.time - ts;
|
|
575
|
+
return age < 0 || age > maxAge;
|
|
588
576
|
}
|
|
589
|
-
|
|
590
|
-
//
|
|
577
|
+
catch {
|
|
578
|
+
// Assume stale.
|
|
591
579
|
return true;
|
|
592
580
|
}
|
|
593
|
-
}
|
|
581
|
+
}
|
|
582
|
+
else if (res.headers.has('Expires')) {
|
|
583
|
+
// Determine if the expiration time has passed.
|
|
584
|
+
const expiresStr = res.headers.get('Expires');
|
|
585
|
+
try {
|
|
586
|
+
// The request needs to be revalidated if the current time is later than the expiration
|
|
587
|
+
// time, if it parses correctly.
|
|
588
|
+
return this.adapter.time > Date.parse(expiresStr);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// The expiration date failed to parse, so revalidate as a precaution.
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// No way to evaluate staleness, so assume the response is already stale.
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
594
599
|
}
|
|
595
600
|
/**
|
|
596
601
|
* Fetch the complete state of a cached resource, or return null if it's not found.
|
|
597
602
|
*/
|
|
598
|
-
fetchFromCacheOnly(url) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return { response, metadata };
|
|
619
|
-
});
|
|
603
|
+
async fetchFromCacheOnly(url) {
|
|
604
|
+
const cache = await this.cache;
|
|
605
|
+
const metaTable = await this.metadata;
|
|
606
|
+
// Lookup the response in the cache.
|
|
607
|
+
const request = this.adapter.newRequest(url);
|
|
608
|
+
const response = await cache.match(request, this.config.cacheQueryOptions);
|
|
609
|
+
if (response === undefined) {
|
|
610
|
+
// It's not found, return null.
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
// Next, lookup the cached metadata.
|
|
614
|
+
let metadata = undefined;
|
|
615
|
+
try {
|
|
616
|
+
metadata = await metaTable.read(request.url);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
// Do nothing, not found. This shouldn't happen, but it can be handled.
|
|
620
|
+
}
|
|
621
|
+
// Return both the response and any available metadata.
|
|
622
|
+
return { response, metadata };
|
|
620
623
|
}
|
|
621
624
|
/**
|
|
622
625
|
* Lookup all resources currently stored in the cache which have no associated hash.
|
|
623
626
|
*/
|
|
624
|
-
unhashedResources() {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
.filter(url => !this.hashes.has(url));
|
|
633
|
-
});
|
|
627
|
+
async unhashedResources() {
|
|
628
|
+
const cache = await this.cache;
|
|
629
|
+
// Start with the set of all cached requests.
|
|
630
|
+
return (await cache.keys())
|
|
631
|
+
// Normalize their URLs.
|
|
632
|
+
.map(request => this.adapter.normalizeUrl(request.url))
|
|
633
|
+
// Exclude the URLs which have hashes.
|
|
634
|
+
.filter(url => !this.hashes.has(url));
|
|
634
635
|
}
|
|
635
636
|
/**
|
|
636
637
|
* Fetch the given resource from the network, and cache it if able.
|
|
637
638
|
*/
|
|
638
|
-
fetchAndCacheOnce(req, used = true) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
639
|
+
async fetchAndCacheOnce(req, used = true) {
|
|
640
|
+
// The `inFlightRequests` map holds information about which caching operations are currently
|
|
641
|
+
// underway for known resources. If this request appears there, another "thread" is already
|
|
642
|
+
// in the process of caching it, and this work should not be duplicated.
|
|
643
|
+
if (this.inFlightRequests.has(req.url)) {
|
|
644
|
+
// There is a caching operation already in progress for this request. Wait for it to
|
|
645
|
+
// complete, and hopefully it will have yielded a useful response.
|
|
646
|
+
return this.inFlightRequests.get(req.url);
|
|
647
|
+
}
|
|
648
|
+
// No other caching operation is being attempted for this resource, so it will be owned here.
|
|
649
|
+
// Go to the network and get the correct version.
|
|
650
|
+
const fetchOp = this.fetchFromNetwork(req);
|
|
651
|
+
// Save this operation in `inFlightRequests` so any other "thread" attempting to cache it
|
|
652
|
+
// will block on this chain instead of duplicating effort.
|
|
653
|
+
this.inFlightRequests.set(req.url, fetchOp);
|
|
654
|
+
// Make sure this attempt is cleaned up properly on failure.
|
|
655
|
+
try {
|
|
656
|
+
// Wait for a response. If this fails, the request will remain in `inFlightRequests`
|
|
657
|
+
// indefinitely.
|
|
658
|
+
const res = await fetchOp;
|
|
659
|
+
// It's very important that only successful responses are cached. Unsuccessful responses
|
|
660
|
+
// should never be cached as this can completely break applications.
|
|
661
|
+
if (!res.ok) {
|
|
662
|
+
throw new Error(`Response not Ok (fetchAndCacheOnce): request for ${req.url} returned response ${res.status} ${res.statusText}`);
|
|
663
|
+
}
|
|
655
664
|
try {
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const cache = yield this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
|
|
668
|
-
yield cache.put(req, res.clone());
|
|
669
|
-
// If the request is not hashed, update its metadata, especially the timestamp. This is
|
|
670
|
-
// needed for future determination of whether this cached response is stale or not.
|
|
671
|
-
if (!this.hashes.has(this.adapter.normalizeUrl(req.url))) {
|
|
672
|
-
// Metadata is tracked for requests that are unhashed.
|
|
673
|
-
const meta = { ts: this.adapter.time, used };
|
|
674
|
-
const metaTable = yield this.metadata;
|
|
675
|
-
yield metaTable.write(req.url, meta);
|
|
676
|
-
}
|
|
677
|
-
return res;
|
|
678
|
-
}
|
|
679
|
-
catch (err) {
|
|
680
|
-
// Among other cases, this can happen when the user clears all data through the DevTools,
|
|
681
|
-
// but the SW is still running and serving another tab. In that case, trying to write to the
|
|
682
|
-
// caches throws an `Entry was not found` error.
|
|
683
|
-
// If this happens the SW can no longer work correctly. This situation is unrecoverable.
|
|
684
|
-
throw new SwCriticalError(`Failed to update the caches for request to '${req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
|
|
665
|
+
// This response is safe to cache (as long as it's cloned). Wait until the cache operation
|
|
666
|
+
// is complete.
|
|
667
|
+
const cache = await this.cache;
|
|
668
|
+
await cache.put(req, res.clone());
|
|
669
|
+
// If the request is not hashed, update its metadata, especially the timestamp. This is
|
|
670
|
+
// needed for future determination of whether this cached response is stale or not.
|
|
671
|
+
if (!this.hashes.has(this.adapter.normalizeUrl(req.url))) {
|
|
672
|
+
// Metadata is tracked for requests that are unhashed.
|
|
673
|
+
const meta = { ts: this.adapter.time, used };
|
|
674
|
+
const metaTable = await this.metadata;
|
|
675
|
+
await metaTable.write(req.url, meta);
|
|
685
676
|
}
|
|
677
|
+
return res;
|
|
686
678
|
}
|
|
687
|
-
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
679
|
+
catch (err) {
|
|
680
|
+
// Among other cases, this can happen when the user clears all data through the DevTools,
|
|
681
|
+
// but the SW is still running and serving another tab. In that case, trying to write to the
|
|
682
|
+
// caches throws an `Entry was not found` error.
|
|
683
|
+
// If this happens the SW can no longer work correctly. This situation is unrecoverable.
|
|
684
|
+
throw new SwCriticalError(`Failed to update the caches for request to '${req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
|
|
691
685
|
}
|
|
692
|
-
}
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
// Finally, it can be removed from `inFlightRequests`. This might result in a double-remove
|
|
689
|
+
// if some other chain was already making this request too, but that won't hurt anything.
|
|
690
|
+
this.inFlightRequests.delete(req.url);
|
|
691
|
+
}
|
|
693
692
|
}
|
|
694
|
-
fetchFromNetwork(req, redirectLimit = 3) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
return res;
|
|
708
|
-
});
|
|
693
|
+
async fetchFromNetwork(req, redirectLimit = 3) {
|
|
694
|
+
// Make a cache-busted request for the resource.
|
|
695
|
+
const res = await this.cacheBustedFetchFromNetwork(req);
|
|
696
|
+
// Check for redirected responses, and follow the redirects.
|
|
697
|
+
if (res['redirected'] && !!res.url) {
|
|
698
|
+
// If the redirect limit is exhausted, fail with an error.
|
|
699
|
+
if (redirectLimit === 0) {
|
|
700
|
+
throw new SwCriticalError(`Response hit redirect limit (fetchFromNetwork): request redirected too many times, next is ${res.url}`);
|
|
701
|
+
}
|
|
702
|
+
// Unwrap the redirect directly.
|
|
703
|
+
return this.fetchFromNetwork(this.adapter.newRequest(res.url), redirectLimit - 1);
|
|
704
|
+
}
|
|
705
|
+
return res;
|
|
709
706
|
}
|
|
710
707
|
/**
|
|
711
708
|
* Load a particular asset from the network, accounting for hash validation.
|
|
712
709
|
*/
|
|
713
|
-
cacheBustedFetchFromNetwork(req) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if
|
|
740
|
-
|
|
741
|
-
|
|
710
|
+
async cacheBustedFetchFromNetwork(req) {
|
|
711
|
+
const url = this.adapter.normalizeUrl(req.url);
|
|
712
|
+
// If a hash is available for this resource, then compare the fetched version with the
|
|
713
|
+
// canonical hash. Otherwise, the network version will have to be trusted.
|
|
714
|
+
if (this.hashes.has(url)) {
|
|
715
|
+
// It turns out this resource does have a hash. Look it up. Unless the fetched version
|
|
716
|
+
// matches this hash, it's invalid and the whole manifest may need to be thrown out.
|
|
717
|
+
const canonicalHash = this.hashes.get(url);
|
|
718
|
+
// Ideally, the resource would be requested with cache-busting to guarantee the SW gets
|
|
719
|
+
// the freshest version. However, doing this would eliminate any chance of the response
|
|
720
|
+
// being in the HTTP cache. Given that the browser has recently actively loaded the page,
|
|
721
|
+
// it's likely that many of the responses the SW needs to cache are in the HTTP cache and
|
|
722
|
+
// are fresh enough to use. In the future, this could be done by setting cacheMode to
|
|
723
|
+
// *only* check the browser cache for a cached version of the resource, when cacheMode is
|
|
724
|
+
// fully supported. For now, the resource is fetched directly, without cache-busting, and
|
|
725
|
+
// if the hash test fails a cache-busted request is tried before concluding that the
|
|
726
|
+
// resource isn't correct. This gives the benefit of acceleration via the HTTP cache
|
|
727
|
+
// without the risk of stale data, at the expense of a duplicate request in the event of
|
|
728
|
+
// a stale response.
|
|
729
|
+
// Fetch the resource from the network (possibly hitting the HTTP cache).
|
|
730
|
+
let response = await this.safeFetch(req);
|
|
731
|
+
// Decide whether a cache-busted request is necessary. A cache-busted request is necessary
|
|
732
|
+
// only if the request was successful but the hash of the retrieved contents does not match
|
|
733
|
+
// the canonical hash from the manifest.
|
|
734
|
+
let makeCacheBustedRequest = response.ok;
|
|
735
|
+
if (makeCacheBustedRequest) {
|
|
736
|
+
// The request was successful. A cache-busted request is only necessary if the hashes
|
|
737
|
+
// don't match.
|
|
738
|
+
// (Make sure to clone the response so it can be used later if it proves to be valid.)
|
|
739
|
+
const fetchedHash = sha1Binary(await response.clone().arrayBuffer());
|
|
740
|
+
makeCacheBustedRequest = (fetchedHash !== canonicalHash);
|
|
741
|
+
}
|
|
742
|
+
// Make a cache busted request to the network, if necessary.
|
|
743
|
+
if (makeCacheBustedRequest) {
|
|
744
|
+
// Hash failure, the version that was retrieved under the default URL did not have the
|
|
745
|
+
// hash expected. This could be because the HTTP cache got in the way and returned stale
|
|
746
|
+
// data, or because the version on the server really doesn't match. A cache-busting
|
|
747
|
+
// request will differentiate these two situations.
|
|
748
|
+
// TODO: handle case where the URL has parameters already (unlikely for assets).
|
|
749
|
+
const cacheBustReq = this.adapter.newRequest(this.cacheBust(req.url));
|
|
750
|
+
response = await this.safeFetch(cacheBustReq);
|
|
751
|
+
// If the response was successful, check the contents against the canonical hash.
|
|
752
|
+
if (response.ok) {
|
|
753
|
+
// Hash the contents.
|
|
742
754
|
// (Make sure to clone the response so it can be used later if it proves to be valid.)
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
// Hash failure, the version that was retrieved under the default URL did not have the
|
|
749
|
-
// hash expected. This could be because the HTTP cache got in the way and returned stale
|
|
750
|
-
// data, or because the version on the server really doesn't match. A cache-busting
|
|
751
|
-
// request will differentiate these two situations.
|
|
752
|
-
// TODO: handle case where the URL has parameters already (unlikely for assets).
|
|
753
|
-
const cacheBustReq = this.adapter.newRequest(this.cacheBust(req.url));
|
|
754
|
-
response = yield this.safeFetch(cacheBustReq);
|
|
755
|
-
// If the response was successful, check the contents against the canonical hash.
|
|
756
|
-
if (response.ok) {
|
|
757
|
-
// Hash the contents.
|
|
758
|
-
// (Make sure to clone the response so it can be used later if it proves to be valid.)
|
|
759
|
-
const cacheBustedHash = sha1Binary(yield response.clone().arrayBuffer());
|
|
760
|
-
// If the cache-busted version doesn't match, then the manifest is not an accurate
|
|
761
|
-
// representation of the server's current set of files, and the SW should give up.
|
|
762
|
-
if (canonicalHash !== cacheBustedHash) {
|
|
763
|
-
throw new SwCriticalError(`Hash mismatch (cacheBustedFetchFromNetwork): ${req.url}: expected ${canonicalHash}, got ${cacheBustedHash} (after cache busting)`);
|
|
764
|
-
}
|
|
755
|
+
const cacheBustedHash = sha1Binary(await response.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)`);
|
|
765
760
|
}
|
|
766
761
|
}
|
|
767
|
-
// At this point, `response` is either successful with a matching hash or is unsuccessful.
|
|
768
|
-
// Before returning it, check whether it failed with a 404 status. This would signify an
|
|
769
|
-
// unrecoverable state.
|
|
770
|
-
if (!response.ok && (response.status === 404)) {
|
|
771
|
-
throw new SwUnrecoverableStateError(`Failed to retrieve hashed resource from the server. (AssetGroup: ${this.config.name} | URL: ${url})`);
|
|
772
|
-
}
|
|
773
|
-
// Return the response (successful or unsuccessful).
|
|
774
|
-
return response;
|
|
775
762
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
763
|
+
// At this point, `response` is either successful with a matching hash or is unsuccessful.
|
|
764
|
+
// Before returning it, check whether it failed with a 404 status. This would signify an
|
|
765
|
+
// unrecoverable state.
|
|
766
|
+
if (!response.ok && (response.status === 404)) {
|
|
767
|
+
throw new SwUnrecoverableStateError(`Failed to retrieve hashed resource from the server. (AssetGroup: ${this.config.name} | URL: ${url})`);
|
|
779
768
|
}
|
|
780
|
-
|
|
769
|
+
// Return the response (successful or unsuccessful).
|
|
770
|
+
return response;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
// This URL doesn't exist in our hash database, so it must be requested directly.
|
|
774
|
+
return this.safeFetch(req);
|
|
775
|
+
}
|
|
781
776
|
}
|
|
782
777
|
/**
|
|
783
778
|
* Possibly update a resource, if it's expired and needs to be updated. A no-op otherwise.
|
|
784
779
|
*/
|
|
785
|
-
maybeUpdate(updateFrom, req, cache) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
yield meta.write(req.url, { ts: this.adapter.time, used: false });
|
|
800
|
-
// No need to do anything further with this resource, it's now cached properly.
|
|
801
|
-
return true;
|
|
802
|
-
}
|
|
780
|
+
async maybeUpdate(updateFrom, req, cache) {
|
|
781
|
+
const url = this.adapter.normalizeUrl(req.url);
|
|
782
|
+
// Check if this resource is hashed and already exists in the cache of a prior version.
|
|
783
|
+
if (this.hashes.has(url)) {
|
|
784
|
+
const hash = this.hashes.get(url);
|
|
785
|
+
// Check the caches of prior versions, using the hash to ensure the correct version of
|
|
786
|
+
// the resource is loaded.
|
|
787
|
+
const res = await updateFrom.lookupResourceWithHash(url, hash);
|
|
788
|
+
// If a previously cached version was available, copy it over to this cache.
|
|
789
|
+
if (res !== null) {
|
|
790
|
+
// Copy to this cache.
|
|
791
|
+
await cache.put(req, res);
|
|
792
|
+
// No need to do anything further with this resource, it's now cached properly.
|
|
793
|
+
return true;
|
|
803
794
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
795
|
+
}
|
|
796
|
+
// No up-to-date version of this resource could be found.
|
|
797
|
+
return false;
|
|
807
798
|
}
|
|
808
799
|
/**
|
|
809
800
|
* Construct a cache-busting URL for a given URL.
|
|
@@ -811,124 +802,118 @@
|
|
|
811
802
|
cacheBust(url) {
|
|
812
803
|
return url + (url.indexOf('?') === -1 ? '?' : '&') + 'ngsw-cache-bust=' + Math.random();
|
|
813
804
|
}
|
|
814
|
-
safeFetch(req) {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
});
|
|
805
|
+
async safeFetch(req) {
|
|
806
|
+
try {
|
|
807
|
+
return await this.scope.fetch(req);
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
return this.adapter.newResponse('', {
|
|
811
|
+
status: 504,
|
|
812
|
+
statusText: 'Gateway Timeout',
|
|
813
|
+
});
|
|
814
|
+
}
|
|
826
815
|
}
|
|
827
816
|
}
|
|
828
817
|
/**
|
|
829
818
|
* An `AssetGroup` that prefetches all of its resources during initialization.
|
|
830
819
|
*/
|
|
831
820
|
class PrefetchAssetGroup extends AssetGroup {
|
|
832
|
-
initializeFully(updateFrom) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
821
|
+
async initializeFully(updateFrom) {
|
|
822
|
+
// Open the cache which actually holds requests.
|
|
823
|
+
const cache = await this.cache;
|
|
824
|
+
// Cache all known resources serially. As this reduce proceeds, each Promise waits
|
|
825
|
+
// on the last before starting the fetch/cache operation for the next request. Any
|
|
826
|
+
// errors cause fall-through to the final Promise which rejects.
|
|
827
|
+
await this.urls.reduce(async (previous, url) => {
|
|
828
|
+
// Wait on all previous operations to complete.
|
|
829
|
+
await previous;
|
|
830
|
+
// Construct the Request for this url.
|
|
831
|
+
const req = this.adapter.newRequest(url);
|
|
832
|
+
// First, check the cache to see if there is already a copy of this resource.
|
|
833
|
+
const alreadyCached = (await cache.match(req, this.config.cacheQueryOptions)) !== undefined;
|
|
834
|
+
// If the resource is in the cache already, it can be skipped.
|
|
835
|
+
if (alreadyCached) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
// If an update source is available.
|
|
839
|
+
if (updateFrom !== undefined && await this.maybeUpdate(updateFrom, req, cache)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// Otherwise, go to the network and hopefully cache the response (if successful).
|
|
843
|
+
await this.fetchAndCacheOnce(req, false);
|
|
844
|
+
}, Promise.resolve());
|
|
845
|
+
// Handle updating of unknown (unhashed) resources. This is only possible if there's
|
|
846
|
+
// a source to update from.
|
|
847
|
+
if (updateFrom !== undefined) {
|
|
848
|
+
const metaTable = await this.metadata;
|
|
849
|
+
// Select all of the previously cached resources. These are cached unhashed resources
|
|
850
|
+
// from previous versions of the app, in any asset group.
|
|
851
|
+
await (await updateFrom.previouslyCachedResources())
|
|
852
|
+
// First, narrow down the set of resources to those which are handled by this group.
|
|
853
|
+
// Either it's a known URL, or it matches a given pattern.
|
|
854
|
+
.filter(url => this.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url)))
|
|
855
|
+
// Finally, process each resource in turn.
|
|
856
|
+
.reduce(async (previous, url) => {
|
|
857
|
+
await previous;
|
|
843
858
|
const req = this.adapter.newRequest(url);
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
|
|
859
|
+
// It's possible that the resource in question is already cached. If so,
|
|
860
|
+
// continue to the next one.
|
|
861
|
+
const alreadyCached = (await cache.match(req, this.config.cacheQueryOptions) !== undefined);
|
|
847
862
|
if (alreadyCached) {
|
|
848
863
|
return;
|
|
849
864
|
}
|
|
850
|
-
//
|
|
851
|
-
|
|
865
|
+
// Get the most recent old version of the resource.
|
|
866
|
+
const res = await updateFrom.lookupResourceWithoutHash(url);
|
|
867
|
+
if (res === null || res.metadata === undefined) {
|
|
868
|
+
// Unexpected, but not harmful.
|
|
852
869
|
return;
|
|
853
870
|
}
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const metaTable = yield this.metadata;
|
|
861
|
-
// Select all of the previously cached resources. These are cached unhashed resources
|
|
862
|
-
// from previous versions of the app, in any asset group.
|
|
863
|
-
yield (yield updateFrom.previouslyCachedResources())
|
|
864
|
-
// First, narrow down the set of resources to those which are handled by this group.
|
|
865
|
-
// Either it's a known URL, or it matches a given pattern.
|
|
866
|
-
.filter(url => this.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url)))
|
|
867
|
-
// Finally, process each resource in turn.
|
|
868
|
-
.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
|
|
869
|
-
yield previous;
|
|
870
|
-
const req = this.adapter.newRequest(url);
|
|
871
|
-
// It's possible that the resource in question is already cached. If so,
|
|
872
|
-
// continue to the next one.
|
|
873
|
-
const alreadyCached = ((yield cache.match(req, this.config.cacheQueryOptions)) !== undefined);
|
|
874
|
-
if (alreadyCached) {
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
// Get the most recent old version of the resource.
|
|
878
|
-
const res = yield updateFrom.lookupResourceWithoutHash(url);
|
|
879
|
-
if (res === null || res.metadata === undefined) {
|
|
880
|
-
// Unexpected, but not harmful.
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
// Write it into the cache. It may already be expired, but it can still serve
|
|
884
|
-
// traffic until it's updated (stale-while-revalidate approach).
|
|
885
|
-
yield cache.put(req, res.response);
|
|
886
|
-
yield metaTable.write(req.url, Object.assign(Object.assign({}, res.metadata), { used: false }));
|
|
887
|
-
}), Promise.resolve());
|
|
888
|
-
}
|
|
889
|
-
});
|
|
871
|
+
// Write it into the cache. It may already be expired, but it can still serve
|
|
872
|
+
// traffic until it's updated (stale-while-revalidate approach).
|
|
873
|
+
await cache.put(req, res.response);
|
|
874
|
+
await metaTable.write(req.url, { ...res.metadata, used: false });
|
|
875
|
+
}, Promise.resolve());
|
|
876
|
+
}
|
|
890
877
|
}
|
|
891
878
|
}
|
|
892
879
|
class LazyAssetGroup extends AssetGroup {
|
|
893
|
-
initializeFully(updateFrom) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
880
|
+
async initializeFully(updateFrom) {
|
|
881
|
+
// No action necessary if no update source is available - resources managed in this group
|
|
882
|
+
// are all lazily loaded, so there's nothing to initialize.
|
|
883
|
+
if (updateFrom === undefined) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
// Open the cache which actually holds requests.
|
|
887
|
+
const cache = await this.cache;
|
|
888
|
+
// Loop through the listed resources, caching any which are available.
|
|
889
|
+
await this.urls.reduce(async (previous, url) => {
|
|
890
|
+
// Wait on all previous operations to complete.
|
|
891
|
+
await previous;
|
|
892
|
+
// Construct the Request for this url.
|
|
893
|
+
const req = this.adapter.newRequest(url);
|
|
894
|
+
// First, check the cache to see if there is already a copy of this resource.
|
|
895
|
+
const alreadyCached = (await cache.match(req, this.config.cacheQueryOptions)) !== undefined;
|
|
896
|
+
// If the resource is in the cache already, it can be skipped.
|
|
897
|
+
if (alreadyCached) {
|
|
898
898
|
return;
|
|
899
899
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
//
|
|
907
|
-
const
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (alreadyCached) {
|
|
900
|
+
const updated = await this.maybeUpdate(updateFrom, req, cache);
|
|
901
|
+
if (this.config.updateMode === 'prefetch' && !updated) {
|
|
902
|
+
// If the resource was not updated, either it was not cached before or
|
|
903
|
+
// the previously cached version didn't match the updated hash. In that
|
|
904
|
+
// case, prefetch update mode dictates that the resource will be updated,
|
|
905
|
+
// except if it was not previously utilized. Check the status of the
|
|
906
|
+
// cached resource to see.
|
|
907
|
+
const cacheStatus = await updateFrom.recentCacheStatus(url);
|
|
908
|
+
// If the resource is not cached, or was cached but unused, then it will be
|
|
909
|
+
// loaded lazily.
|
|
910
|
+
if (cacheStatus !== UpdateCacheStatus.CACHED) {
|
|
912
911
|
return;
|
|
913
912
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
// case, prefetch update mode dictates that the resource will be updated,
|
|
919
|
-
// except if it was not previously utilized. Check the status of the
|
|
920
|
-
// cached resource to see.
|
|
921
|
-
const cacheStatus = yield updateFrom.recentCacheStatus(url);
|
|
922
|
-
// If the resource is not cached, or was cached but unused, then it will be
|
|
923
|
-
// loaded lazily.
|
|
924
|
-
if (cacheStatus !== UpdateCacheStatus.CACHED) {
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
// Update from the network.
|
|
928
|
-
yield this.fetchAndCacheOnce(req, false);
|
|
929
|
-
}
|
|
930
|
-
}), Promise.resolve());
|
|
931
|
-
});
|
|
913
|
+
// Update from the network.
|
|
914
|
+
await this.fetchAndCacheOnce(req, false);
|
|
915
|
+
}
|
|
916
|
+
}, Promise.resolve());
|
|
932
917
|
}
|
|
933
918
|
}
|
|
934
919
|
|
|
@@ -1068,193 +1053,182 @@
|
|
|
1068
1053
|
* for caching.
|
|
1069
1054
|
*/
|
|
1070
1055
|
class DataGroup {
|
|
1071
|
-
constructor(scope, adapter, config, db, debugHandler,
|
|
1056
|
+
constructor(scope, adapter, config, db, debugHandler, cacheNamePrefix) {
|
|
1072
1057
|
this.scope = scope;
|
|
1073
1058
|
this.adapter = adapter;
|
|
1074
1059
|
this.config = config;
|
|
1075
1060
|
this.db = db;
|
|
1076
1061
|
this.debugHandler = debugHandler;
|
|
1077
|
-
this.prefix = prefix;
|
|
1078
1062
|
/**
|
|
1079
1063
|
* Tracks the LRU state of resources in this cache.
|
|
1080
1064
|
*/
|
|
1081
1065
|
this._lru = null;
|
|
1082
|
-
this.patterns =
|
|
1083
|
-
this.cache =
|
|
1084
|
-
this.lruTable = this.db.open(`${
|
|
1085
|
-
this.ageTable = this.db.open(`${
|
|
1066
|
+
this.patterns = config.patterns.map(pattern => new RegExp(pattern));
|
|
1067
|
+
this.cache = adapter.caches.open(`${cacheNamePrefix}:${config.name}:cache`);
|
|
1068
|
+
this.lruTable = this.db.open(`${cacheNamePrefix}:${config.name}:lru`, config.cacheQueryOptions);
|
|
1069
|
+
this.ageTable = this.db.open(`${cacheNamePrefix}:${config.name}:age`, config.cacheQueryOptions);
|
|
1086
1070
|
}
|
|
1087
1071
|
/**
|
|
1088
1072
|
* Lazily initialize/load the LRU chain.
|
|
1089
1073
|
*/
|
|
1090
|
-
lru() {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
this._lru = new LruList(yield table.read('lru'));
|
|
1096
|
-
}
|
|
1097
|
-
catch (_a) {
|
|
1098
|
-
this._lru = new LruList();
|
|
1099
|
-
}
|
|
1074
|
+
async lru() {
|
|
1075
|
+
if (this._lru === null) {
|
|
1076
|
+
const table = await this.lruTable;
|
|
1077
|
+
try {
|
|
1078
|
+
this._lru = new LruList(await table.read('lru'));
|
|
1100
1079
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1080
|
+
catch {
|
|
1081
|
+
this._lru = new LruList();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return this._lru;
|
|
1103
1085
|
}
|
|
1104
1086
|
/**
|
|
1105
1087
|
* Sync the LRU chain to non-volatile storage.
|
|
1106
1088
|
*/
|
|
1107
|
-
syncLru() {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
});
|
|
1089
|
+
async syncLru() {
|
|
1090
|
+
if (this._lru === null) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const table = await this.lruTable;
|
|
1094
|
+
try {
|
|
1095
|
+
return table.write('lru', this._lru.state);
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
// Writing lru cache table failed. This could be a result of a full storage.
|
|
1099
|
+
// Continue serving clients as usual.
|
|
1100
|
+
this.debugHandler.log(err, `DataGroup(${this.config.name}@${this.config.version}).syncLru()`);
|
|
1101
|
+
// TODO: Better detect/handle full storage; e.g. using
|
|
1102
|
+
// [navigator.storage](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorStorage/storage).
|
|
1103
|
+
}
|
|
1124
1104
|
}
|
|
1125
1105
|
/**
|
|
1126
1106
|
* Process a fetch event and return a `Response` if the resource is covered by this group,
|
|
1127
1107
|
* or `null` otherwise.
|
|
1128
1108
|
*/
|
|
1129
|
-
handleFetch(req,
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1109
|
+
async handleFetch(req, event) {
|
|
1110
|
+
// Do nothing
|
|
1111
|
+
if (!this.patterns.some(pattern => pattern.test(req.url))) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
// Lazily initialize the LRU cache.
|
|
1115
|
+
const lru = await this.lru();
|
|
1116
|
+
// The URL matches this cache. First, check whether this is a mutating request or not.
|
|
1117
|
+
switch (req.method) {
|
|
1118
|
+
case 'OPTIONS':
|
|
1119
|
+
// Don't try to cache this - it's non-mutating, but is part of a mutating request.
|
|
1120
|
+
// Most likely SWs don't even see this, but this guard is here just in case.
|
|
1133
1121
|
return null;
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
case 'HEAD':
|
|
1145
|
-
// Handle the request with whatever strategy was selected.
|
|
1146
|
-
switch (this.config.strategy) {
|
|
1147
|
-
case 'freshness':
|
|
1148
|
-
return this.handleFetchWithFreshness(req, ctx, lru);
|
|
1149
|
-
case 'performance':
|
|
1150
|
-
return this.handleFetchWithPerformance(req, ctx, lru);
|
|
1151
|
-
default:
|
|
1152
|
-
throw new Error(`Unknown strategy: ${this.config.strategy}`);
|
|
1153
|
-
}
|
|
1154
|
-
default:
|
|
1155
|
-
// This was a mutating request. Assume the cache for this URL is no longer valid.
|
|
1156
|
-
const wasCached = lru.remove(req.url);
|
|
1157
|
-
// If there was a cached entry, remove it.
|
|
1158
|
-
if (wasCached) {
|
|
1159
|
-
yield this.clearCacheForUrl(req.url);
|
|
1160
|
-
}
|
|
1161
|
-
// Sync the LRU chain to non-volatile storage.
|
|
1162
|
-
yield this.syncLru();
|
|
1163
|
-
// Finally, fall back on the network.
|
|
1164
|
-
return this.safeFetch(req);
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
handleFetchWithPerformance(req, ctx, lru) {
|
|
1169
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1170
|
-
let res = null;
|
|
1171
|
-
// Check the cache first. If the resource exists there (and is not expired), the cached
|
|
1172
|
-
// version can be used.
|
|
1173
|
-
const fromCache = yield this.loadFromCache(req, lru);
|
|
1174
|
-
if (fromCache !== null) {
|
|
1175
|
-
res = fromCache.res;
|
|
1176
|
-
// Check the age of the resource.
|
|
1177
|
-
if (this.config.refreshAheadMs !== undefined && fromCache.age >= this.config.refreshAheadMs) {
|
|
1178
|
-
ctx.waitUntil(this.safeCacheResponse(req, this.safeFetch(req), lru));
|
|
1122
|
+
case 'GET':
|
|
1123
|
+
case 'HEAD':
|
|
1124
|
+
// Handle the request with whatever strategy was selected.
|
|
1125
|
+
switch (this.config.strategy) {
|
|
1126
|
+
case 'freshness':
|
|
1127
|
+
return this.handleFetchWithFreshness(req, event, lru);
|
|
1128
|
+
case 'performance':
|
|
1129
|
+
return this.handleFetchWithPerformance(req, event, lru);
|
|
1130
|
+
default:
|
|
1131
|
+
throw new Error(`Unknown strategy: ${this.config.strategy}`);
|
|
1179
1132
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
// Cache the network response eventually.
|
|
1193
|
-
ctx.waitUntil(this.safeCacheResponse(req, networkFetch, lru));
|
|
1194
|
-
}
|
|
1195
|
-
else {
|
|
1196
|
-
// The request completed in time, so cache it inline with the response flow.
|
|
1197
|
-
yield this.safeCacheResponse(req, res, lru);
|
|
1198
|
-
}
|
|
1199
|
-
return res;
|
|
1200
|
-
});
|
|
1133
|
+
default:
|
|
1134
|
+
// This was a mutating request. Assume the cache for this URL is no longer valid.
|
|
1135
|
+
const wasCached = lru.remove(req.url);
|
|
1136
|
+
// If there was a cached entry, remove it.
|
|
1137
|
+
if (wasCached) {
|
|
1138
|
+
await this.clearCacheForUrl(req.url);
|
|
1139
|
+
}
|
|
1140
|
+
// Sync the LRU chain to non-volatile storage.
|
|
1141
|
+
await this.syncLru();
|
|
1142
|
+
// Finally, fall back on the network.
|
|
1143
|
+
return this.safeFetch(req);
|
|
1144
|
+
}
|
|
1201
1145
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
res = undefined;
|
|
1213
|
-
}
|
|
1214
|
-
// If the network fetch times out or errors, fall back on the cache.
|
|
1215
|
-
if (res === undefined) {
|
|
1216
|
-
ctx.waitUntil(this.safeCacheResponse(req, networkFetch, lru, true));
|
|
1217
|
-
// Ignore the age, the network response will be cached anyway due to the
|
|
1218
|
-
// behavior of freshness.
|
|
1219
|
-
const fromCache = yield this.loadFromCache(req, lru);
|
|
1220
|
-
res = (fromCache !== null) ? fromCache.res : null;
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
yield this.safeCacheResponse(req, res, lru, true);
|
|
1224
|
-
}
|
|
1225
|
-
// Either the network fetch didn't time out, or the cache yielded a usable response.
|
|
1226
|
-
// In either case, use it.
|
|
1227
|
-
if (res !== null) {
|
|
1228
|
-
return res;
|
|
1146
|
+
async handleFetchWithPerformance(req, event, lru) {
|
|
1147
|
+
let res = null;
|
|
1148
|
+
// Check the cache first. If the resource exists there (and is not expired), the cached
|
|
1149
|
+
// version can be used.
|
|
1150
|
+
const fromCache = await this.loadFromCache(req, lru);
|
|
1151
|
+
if (fromCache !== null) {
|
|
1152
|
+
res = fromCache.res;
|
|
1153
|
+
// Check the age of the resource.
|
|
1154
|
+
if (this.config.refreshAheadMs !== undefined && fromCache.age >= this.config.refreshAheadMs) {
|
|
1155
|
+
event.waitUntil(this.safeCacheResponse(req, this.safeFetch(req), lru));
|
|
1229
1156
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1157
|
+
}
|
|
1158
|
+
if (res !== null) {
|
|
1159
|
+
return res;
|
|
1160
|
+
}
|
|
1161
|
+
// No match from the cache. Go to the network. Note that this is not an 'await'
|
|
1162
|
+
// call, networkFetch is the actual Promise. This is due to timeout handling.
|
|
1163
|
+
const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
|
|
1164
|
+
res = await timeoutFetch;
|
|
1165
|
+
// Since fetch() will always return a response, undefined indicates a timeout.
|
|
1166
|
+
if (res === undefined) {
|
|
1167
|
+
// The request timed out. Return a Gateway Timeout error.
|
|
1168
|
+
res = this.adapter.newResponse(null, { status: 504, statusText: 'Gateway Timeout' });
|
|
1169
|
+
// Cache the network response eventually.
|
|
1170
|
+
event.waitUntil(this.safeCacheResponse(req, networkFetch, lru));
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
// The request completed in time, so cache it inline with the response flow.
|
|
1174
|
+
await this.safeCacheResponse(req, res, lru);
|
|
1175
|
+
}
|
|
1176
|
+
return res;
|
|
1177
|
+
}
|
|
1178
|
+
async handleFetchWithFreshness(req, event, lru) {
|
|
1179
|
+
// Start with a network fetch.
|
|
1180
|
+
const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
|
|
1181
|
+
let res;
|
|
1182
|
+
// If that fetch errors, treat it as a timed out request.
|
|
1183
|
+
try {
|
|
1184
|
+
res = await timeoutFetch;
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
res = undefined;
|
|
1188
|
+
}
|
|
1189
|
+
// If the network fetch times out or errors, fall back on the cache.
|
|
1190
|
+
if (res === undefined) {
|
|
1191
|
+
event.waitUntil(this.safeCacheResponse(req, networkFetch, lru, true));
|
|
1192
|
+
// Ignore the age, the network response will be cached anyway due to the
|
|
1193
|
+
// behavior of freshness.
|
|
1194
|
+
const fromCache = await this.loadFromCache(req, lru);
|
|
1195
|
+
res = (fromCache !== null) ? fromCache.res : null;
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
await this.safeCacheResponse(req, res, lru, true);
|
|
1199
|
+
}
|
|
1200
|
+
// Either the network fetch didn't time out, or the cache yielded a usable response.
|
|
1201
|
+
// In either case, use it.
|
|
1202
|
+
if (res !== null) {
|
|
1203
|
+
return res;
|
|
1204
|
+
}
|
|
1205
|
+
// No response in the cache. No choice but to fall back on the full network fetch.
|
|
1206
|
+
return networkFetch;
|
|
1233
1207
|
}
|
|
1234
1208
|
networkFetchWithTimeout(req) {
|
|
1235
1209
|
// If there is a timeout configured, race a timeout Promise with the network fetch.
|
|
1236
1210
|
// Otherwise, just fetch from the network directly.
|
|
1237
1211
|
if (this.config.timeoutMs !== undefined) {
|
|
1238
1212
|
const networkFetch = this.scope.fetch(req);
|
|
1239
|
-
const safeNetworkFetch = (() =>
|
|
1213
|
+
const safeNetworkFetch = (async () => {
|
|
1240
1214
|
try {
|
|
1241
|
-
return
|
|
1215
|
+
return await networkFetch;
|
|
1242
1216
|
}
|
|
1243
|
-
catch
|
|
1217
|
+
catch {
|
|
1244
1218
|
return this.adapter.newResponse(null, {
|
|
1245
1219
|
status: 504,
|
|
1246
1220
|
statusText: 'Gateway Timeout',
|
|
1247
1221
|
});
|
|
1248
1222
|
}
|
|
1249
|
-
})
|
|
1250
|
-
const networkFetchUndefinedError = (() =>
|
|
1223
|
+
})();
|
|
1224
|
+
const networkFetchUndefinedError = (async () => {
|
|
1251
1225
|
try {
|
|
1252
|
-
return
|
|
1226
|
+
return await networkFetch;
|
|
1253
1227
|
}
|
|
1254
|
-
catch
|
|
1228
|
+
catch {
|
|
1255
1229
|
return undefined;
|
|
1256
1230
|
}
|
|
1257
|
-
})
|
|
1231
|
+
})();
|
|
1258
1232
|
// Construct a Promise<undefined> for the timeout.
|
|
1259
1233
|
const timeout = this.adapter.timeout(this.config.timeoutMs);
|
|
1260
1234
|
// Race that with the network fetch. This will either be a Response, or `undefined`
|
|
@@ -1267,56 +1241,52 @@
|
|
|
1267
1241
|
return [networkFetch, networkFetch];
|
|
1268
1242
|
}
|
|
1269
1243
|
}
|
|
1270
|
-
safeCacheResponse(req, resOrPromise, lru, okToCacheOpaque) {
|
|
1271
|
-
|
|
1244
|
+
async safeCacheResponse(req, resOrPromise, lru, okToCacheOpaque) {
|
|
1245
|
+
try {
|
|
1246
|
+
const res = await resOrPromise;
|
|
1272
1247
|
try {
|
|
1273
|
-
|
|
1274
|
-
try {
|
|
1275
|
-
yield this.cacheResponse(req, res, lru, okToCacheOpaque);
|
|
1276
|
-
}
|
|
1277
|
-
catch (err) {
|
|
1278
|
-
// Saving the API response failed. This could be a result of a full storage.
|
|
1279
|
-
// Since this data is cached lazily and temporarily, continue serving clients as usual.
|
|
1280
|
-
this.debugHandler.log(err, `DataGroup(${this.config.name}@${this.config.version}).safeCacheResponse(${req.url}, status: ${res.status})`);
|
|
1281
|
-
// TODO: Better detect/handle full storage; e.g. using
|
|
1282
|
-
// [navigator.storage](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorStorage/storage).
|
|
1283
|
-
}
|
|
1248
|
+
await this.cacheResponse(req, res, lru, okToCacheOpaque);
|
|
1284
1249
|
}
|
|
1285
|
-
catch (
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1250
|
+
catch (err) {
|
|
1251
|
+
// Saving the API response failed. This could be a result of a full storage.
|
|
1252
|
+
// Since this data is cached lazily and temporarily, continue serving clients as usual.
|
|
1253
|
+
this.debugHandler.log(err, `DataGroup(${this.config.name}@${this.config.version}).safeCacheResponse(${req.url}, status: ${res.status})`);
|
|
1254
|
+
// TODO: Better detect/handle full storage; e.g. using
|
|
1255
|
+
// [navigator.storage](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorStorage/storage).
|
|
1288
1256
|
}
|
|
1289
|
-
}
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
// Request failed
|
|
1260
|
+
// TODO: Handle this error somehow?
|
|
1261
|
+
}
|
|
1290
1262
|
}
|
|
1291
|
-
loadFromCache(req, lru) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
return { res, age };
|
|
1307
|
-
}
|
|
1308
|
-
// Otherwise, or if there was an error, assume the response is expired, and evict it.
|
|
1309
|
-
}
|
|
1310
|
-
catch (_a) {
|
|
1311
|
-
// Some error getting the age for the response. Assume it's expired.
|
|
1263
|
+
async loadFromCache(req, lru) {
|
|
1264
|
+
// Look for a response in the cache. If one exists, return it.
|
|
1265
|
+
const cache = await this.cache;
|
|
1266
|
+
let res = await cache.match(req, this.config.cacheQueryOptions);
|
|
1267
|
+
if (res !== undefined) {
|
|
1268
|
+
// A response was found in the cache, but its age is not yet known. Look it up.
|
|
1269
|
+
try {
|
|
1270
|
+
const ageTable = await this.ageTable;
|
|
1271
|
+
const age = this.adapter.time - (await ageTable.read(req.url)).age;
|
|
1272
|
+
// If the response is young enough, use it.
|
|
1273
|
+
if (age <= this.config.maxAge) {
|
|
1274
|
+
// Successful match from the cache. Use the response, after marking it as having
|
|
1275
|
+
// been accessed.
|
|
1276
|
+
lru.accessed(req.url);
|
|
1277
|
+
return { res, age };
|
|
1312
1278
|
}
|
|
1313
|
-
|
|
1314
|
-
yield this.clearCacheForUrl(req.url);
|
|
1315
|
-
// TODO: avoid duplicate in event of network timeout, maybe.
|
|
1316
|
-
yield this.syncLru();
|
|
1279
|
+
// Otherwise, or if there was an error, assume the response is expired, and evict it.
|
|
1317
1280
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1281
|
+
catch {
|
|
1282
|
+
// Some error getting the age for the response. Assume it's expired.
|
|
1283
|
+
}
|
|
1284
|
+
lru.remove(req.url);
|
|
1285
|
+
await this.clearCacheForUrl(req.url);
|
|
1286
|
+
// TODO: avoid duplicate in event of network timeout, maybe.
|
|
1287
|
+
await this.syncLru();
|
|
1288
|
+
}
|
|
1289
|
+
return null;
|
|
1320
1290
|
}
|
|
1321
1291
|
/**
|
|
1322
1292
|
* Operation for caching the response from the server. This has to happen all
|
|
@@ -1325,47 +1295,54 @@
|
|
|
1325
1295
|
* If the request times out on the server, an error will be returned but the real network
|
|
1326
1296
|
* request will still be running in the background, to be cached when it completes.
|
|
1327
1297
|
*/
|
|
1328
|
-
cacheResponse(req, res, lru, okToCacheOpaque = false) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
yield this.clearCacheForUrl(evictedUrl);
|
|
1341
|
-
}
|
|
1298
|
+
async cacheResponse(req, res, lru, okToCacheOpaque = false) {
|
|
1299
|
+
// Only cache successful responses.
|
|
1300
|
+
if (!(res.ok || (okToCacheOpaque && res.type === 'opaque'))) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
// If caching this response would make the cache exceed its maximum size, evict something
|
|
1304
|
+
// first.
|
|
1305
|
+
if (lru.size >= this.config.maxSize) {
|
|
1306
|
+
// The cache is too big, evict something.
|
|
1307
|
+
const evictedUrl = lru.pop();
|
|
1308
|
+
if (evictedUrl !== null) {
|
|
1309
|
+
await this.clearCacheForUrl(evictedUrl);
|
|
1342
1310
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1311
|
+
}
|
|
1312
|
+
// TODO: evaluate for possible race conditions during flaky network periods.
|
|
1313
|
+
// Mark this resource as having been accessed recently. This ensures it won't be evicted
|
|
1314
|
+
// until enough other resources are requested that it falls off the end of the LRU chain.
|
|
1315
|
+
lru.accessed(req.url);
|
|
1316
|
+
// Store the response in the cache (cloning because the browser will consume
|
|
1317
|
+
// the body during the caching operation).
|
|
1318
|
+
await (await this.cache).put(req, res.clone());
|
|
1319
|
+
// Store the age of the cache.
|
|
1320
|
+
const ageTable = await this.ageTable;
|
|
1321
|
+
await ageTable.write(req.url, { age: this.adapter.time });
|
|
1322
|
+
// Sync the LRU chain to non-volatile storage.
|
|
1323
|
+
await this.syncLru();
|
|
1356
1324
|
}
|
|
1357
1325
|
/**
|
|
1358
1326
|
* Delete all of the saved state which this group uses to track resources.
|
|
1359
1327
|
*/
|
|
1360
|
-
cleanup() {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1328
|
+
async cleanup() {
|
|
1329
|
+
// Remove both the cache and the database entries which track LRU stats.
|
|
1330
|
+
await Promise.all([
|
|
1331
|
+
this.cache.then(cache => this.adapter.caches.delete(cache.name)),
|
|
1332
|
+
this.ageTable.then(table => this.db.delete(table.name)),
|
|
1333
|
+
this.lruTable.then(table => this.db.delete(table.name)),
|
|
1334
|
+
]);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Return a list of the names of all caches used by this group.
|
|
1338
|
+
*/
|
|
1339
|
+
async getCacheNames() {
|
|
1340
|
+
const [cache, ageTable, lruTable] = await Promise.all([
|
|
1341
|
+
this.cache,
|
|
1342
|
+
this.ageTable,
|
|
1343
|
+
this.lruTable,
|
|
1344
|
+
]);
|
|
1345
|
+
return [cache.name, ageTable.cacheName, lruTable.cacheName];
|
|
1369
1346
|
}
|
|
1370
1347
|
/**
|
|
1371
1348
|
* Clear the state of the cache for a particular resource.
|
|
@@ -1374,28 +1351,24 @@
|
|
|
1374
1351
|
* been done already. This clears the GET and HEAD versions of the request from
|
|
1375
1352
|
* the cache itself, as well as the metadata stored in the age table.
|
|
1376
1353
|
*/
|
|
1377
|
-
clearCacheForUrl(url) {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
statusText: 'Gateway Timeout',
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
});
|
|
1354
|
+
async clearCacheForUrl(url) {
|
|
1355
|
+
const [cache, ageTable] = await Promise.all([this.cache, this.ageTable]);
|
|
1356
|
+
await Promise.all([
|
|
1357
|
+
cache.delete(this.adapter.newRequest(url, { method: 'GET' }), this.config.cacheQueryOptions),
|
|
1358
|
+
cache.delete(this.adapter.newRequest(url, { method: 'HEAD' }), this.config.cacheQueryOptions),
|
|
1359
|
+
ageTable.delete(url),
|
|
1360
|
+
]);
|
|
1361
|
+
}
|
|
1362
|
+
async safeFetch(req) {
|
|
1363
|
+
try {
|
|
1364
|
+
return this.scope.fetch(req);
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
return this.adapter.newResponse(null, {
|
|
1368
|
+
status: 504,
|
|
1369
|
+
statusText: 'Gateway Timeout',
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1399
1372
|
}
|
|
1400
1373
|
}
|
|
1401
1374
|
|
|
@@ -1423,7 +1396,6 @@
|
|
|
1423
1396
|
this.scope = scope;
|
|
1424
1397
|
this.adapter = adapter;
|
|
1425
1398
|
this.database = database;
|
|
1426
|
-
this.idle = idle;
|
|
1427
1399
|
this.debugHandler = debugHandler;
|
|
1428
1400
|
this.manifest = manifest;
|
|
1429
1401
|
this.manifestHash = manifestHash;
|
|
@@ -1441,28 +1413,25 @@
|
|
|
1441
1413
|
*/
|
|
1442
1414
|
this._okay = true;
|
|
1443
1415
|
// The hashTable within the manifest is an Object - convert it to a Map for easier lookups.
|
|
1444
|
-
Object.keys(
|
|
1445
|
-
this.hashTable.set(adapter.normalizeUrl(url),
|
|
1416
|
+
Object.keys(manifest.hashTable).forEach(url => {
|
|
1417
|
+
this.hashTable.set(adapter.normalizeUrl(url), manifest.hashTable[url]);
|
|
1446
1418
|
});
|
|
1447
1419
|
// Process each `AssetGroup` declared in the manifest. Each declared group gets an `AssetGroup`
|
|
1448
|
-
// instance
|
|
1449
|
-
|
|
1420
|
+
// instance created for it, of a type that depends on the configuration mode.
|
|
1421
|
+
const assetCacheNamePrefix = `${manifestHash}:assets`;
|
|
1450
1422
|
this.assetGroups = (manifest.assetGroups || []).map(config => {
|
|
1451
|
-
// Every asset group has a cache that's prefixed by the manifest hash and the name of the
|
|
1452
|
-
// group.
|
|
1453
|
-
const prefix = `${adapter.cacheNamePrefix}:${this.manifestHash}:assets`;
|
|
1454
1423
|
// Check the caching mode, which determines when resources will be fetched/updated.
|
|
1455
1424
|
switch (config.installMode) {
|
|
1456
1425
|
case 'prefetch':
|
|
1457
|
-
return new PrefetchAssetGroup(
|
|
1426
|
+
return new PrefetchAssetGroup(scope, adapter, idle, config, this.hashTable, database, assetCacheNamePrefix);
|
|
1458
1427
|
case 'lazy':
|
|
1459
|
-
return new LazyAssetGroup(
|
|
1428
|
+
return new LazyAssetGroup(scope, adapter, idle, config, this.hashTable, database, assetCacheNamePrefix);
|
|
1460
1429
|
}
|
|
1461
1430
|
});
|
|
1462
1431
|
// Process each `DataGroup` declared in the manifest.
|
|
1463
1432
|
this.dataGroups =
|
|
1464
1433
|
(manifest.dataGroups || [])
|
|
1465
|
-
.map(config => new DataGroup(
|
|
1434
|
+
.map(config => new DataGroup(scope, adapter, config, database, debugHandler, `${config.version}:data`));
|
|
1466
1435
|
// This keeps backwards compatibility with app versions without navigation urls.
|
|
1467
1436
|
// Fix: https://github.com/angular/angular/issues/27209
|
|
1468
1437
|
manifest.navigationUrls = manifest.navigationUrls || BACKWARDS_COMPATIBILITY_NAVIGATION_URLS;
|
|
@@ -1482,86 +1451,82 @@
|
|
|
1482
1451
|
* required
|
|
1483
1452
|
* data has been safely downloaded.
|
|
1484
1453
|
*/
|
|
1485
|
-
initializeFully(updateFrom) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
});
|
|
1454
|
+
async initializeFully(updateFrom) {
|
|
1455
|
+
try {
|
|
1456
|
+
// Fully initialize each asset group, in series. Starts with an empty Promise,
|
|
1457
|
+
// and waits for the previous groups to have been initialized before initializing
|
|
1458
|
+
// the next one in turn.
|
|
1459
|
+
await this.assetGroups.reduce(async (previous, group) => {
|
|
1460
|
+
// Wait for the previous groups to complete initialization. If there is a
|
|
1461
|
+
// failure, this will throw, and each subsequent group will throw, until the
|
|
1462
|
+
// whole sequence fails.
|
|
1463
|
+
await previous;
|
|
1464
|
+
// Initialize this group.
|
|
1465
|
+
return group.initializeFully(updateFrom);
|
|
1466
|
+
}, Promise.resolve());
|
|
1467
|
+
}
|
|
1468
|
+
catch (err) {
|
|
1469
|
+
this._okay = false;
|
|
1470
|
+
throw err;
|
|
1471
|
+
}
|
|
1505
1472
|
}
|
|
1506
|
-
handleFetch(req,
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1473
|
+
async handleFetch(req, event) {
|
|
1474
|
+
// Check the request against each `AssetGroup` in sequence. If an `AssetGroup` can't handle the
|
|
1475
|
+
// request,
|
|
1476
|
+
// it will return `null`. Thus, the first non-null response is the SW's answer to the request.
|
|
1477
|
+
// So reduce
|
|
1478
|
+
// the group list, keeping track of a possible response. If there is one, it gets passed
|
|
1479
|
+
// through, and if
|
|
1480
|
+
// not the next group is consulted to produce a candidate response.
|
|
1481
|
+
const asset = await this.assetGroups.reduce(async (potentialResponse, group) => {
|
|
1482
|
+
// Wait on the previous potential response. If it's not null, it should just be passed
|
|
1483
|
+
// through.
|
|
1484
|
+
const resp = await potentialResponse;
|
|
1485
|
+
if (resp !== null) {
|
|
1486
|
+
return resp;
|
|
1487
|
+
}
|
|
1488
|
+
// No response has been found yet. Maybe this group will have one.
|
|
1489
|
+
return group.handleFetch(req, event);
|
|
1490
|
+
}, Promise.resolve(null));
|
|
1491
|
+
// The result of the above is the asset response, if there is any, or null otherwise. Return the
|
|
1492
|
+
// asset
|
|
1493
|
+
// response if there was one. If not, check with the data caching groups.
|
|
1494
|
+
if (asset !== null) {
|
|
1495
|
+
return asset;
|
|
1496
|
+
}
|
|
1497
|
+
// Perform the same reduction operation as above, but this time processing
|
|
1498
|
+
// the data caching groups.
|
|
1499
|
+
const data = await this.dataGroups.reduce(async (potentialResponse, group) => {
|
|
1500
|
+
const resp = await potentialResponse;
|
|
1501
|
+
if (resp !== null) {
|
|
1502
|
+
return resp;
|
|
1503
|
+
}
|
|
1504
|
+
return group.handleFetch(req, event);
|
|
1505
|
+
}, Promise.resolve(null));
|
|
1506
|
+
// If the data caching group returned a response, go with it.
|
|
1507
|
+
if (data !== null) {
|
|
1508
|
+
return data;
|
|
1509
|
+
}
|
|
1510
|
+
// Next, check if this is a navigation request for a route. Detect circular
|
|
1511
|
+
// navigations by checking if the request URL is the same as the index URL.
|
|
1512
|
+
if (this.adapter.normalizeUrl(req.url) !== this.indexUrl && this.isNavigationRequest(req)) {
|
|
1513
|
+
if (this.manifest.navigationRequestStrategy === 'freshness') {
|
|
1514
|
+
// For navigation requests the freshness was configured. The request will always go trough
|
|
1515
|
+
// the network and fallback to default `handleFetch` behavior in case of failure.
|
|
1516
|
+
try {
|
|
1517
|
+
return await this.scope.fetch(req);
|
|
1521
1518
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
// asset
|
|
1527
|
-
// response if there was one. If not, check with the data caching groups.
|
|
1528
|
-
if (asset !== null) {
|
|
1529
|
-
return asset;
|
|
1530
|
-
}
|
|
1531
|
-
// Perform the same reduction operation as above, but this time processing
|
|
1532
|
-
// the data caching groups.
|
|
1533
|
-
const data = yield this.dataGroups.reduce((potentialResponse, group) => __awaiter(this, void 0, void 0, function* () {
|
|
1534
|
-
const resp = yield potentialResponse;
|
|
1535
|
-
if (resp !== null) {
|
|
1536
|
-
return resp;
|
|
1519
|
+
catch {
|
|
1520
|
+
// Navigation request failed - application is likely offline.
|
|
1521
|
+
// Proceed forward to the default `handleFetch` behavior, where
|
|
1522
|
+
// `indexUrl` will be requested and it should be available in the cache.
|
|
1537
1523
|
}
|
|
1538
|
-
return group.handleFetch(req, context);
|
|
1539
|
-
}), Promise.resolve(null));
|
|
1540
|
-
// If the data caching group returned a response, go with it.
|
|
1541
|
-
if (data !== null) {
|
|
1542
|
-
return data;
|
|
1543
|
-
}
|
|
1544
|
-
// Next, check if this is a navigation request for a route. Detect circular
|
|
1545
|
-
// navigations by checking if the request URL is the same as the index URL.
|
|
1546
|
-
if (this.adapter.normalizeUrl(req.url) !== this.indexUrl && this.isNavigationRequest(req)) {
|
|
1547
|
-
if (this.manifest.navigationRequestStrategy === 'freshness') {
|
|
1548
|
-
// For navigation requests the freshness was configured. The request will always go trough
|
|
1549
|
-
// the network and fallback to default `handleFetch` behavior in case of failure.
|
|
1550
|
-
try {
|
|
1551
|
-
return yield this.scope.fetch(req);
|
|
1552
|
-
}
|
|
1553
|
-
catch (_a) {
|
|
1554
|
-
// Navigation request failed - application is likely offline.
|
|
1555
|
-
// Proceed forward to the default `handleFetch` behavior, where
|
|
1556
|
-
// `indexUrl` will be requested and it should be available in the cache.
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
// This was a navigation request. Re-enter `handleFetch` with a request for
|
|
1560
|
-
// the URL.
|
|
1561
|
-
return this.handleFetch(this.adapter.newRequest(this.indexUrl), context);
|
|
1562
1524
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1525
|
+
// This was a navigation request. Re-enter `handleFetch` with a request for
|
|
1526
|
+
// the URL.
|
|
1527
|
+
return this.handleFetch(this.adapter.newRequest(this.indexUrl), event);
|
|
1528
|
+
}
|
|
1529
|
+
return null;
|
|
1565
1530
|
}
|
|
1566
1531
|
/**
|
|
1567
1532
|
* Determine whether the request is a navigation request.
|
|
@@ -1583,21 +1548,19 @@
|
|
|
1583
1548
|
/**
|
|
1584
1549
|
* Check this version for a given resource with a particular hash.
|
|
1585
1550
|
*/
|
|
1586
|
-
lookupResourceWithHash(url, hash) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
return cacheState && cacheState.response;
|
|
1600
|
-
});
|
|
1551
|
+
async lookupResourceWithHash(url, hash) {
|
|
1552
|
+
// Verify that this version has the requested resource cached. If not,
|
|
1553
|
+
// there's no point in trying.
|
|
1554
|
+
if (!this.hashTable.has(url)) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
// Next, check whether the resource has the correct hash. If not, any cached
|
|
1558
|
+
// response isn't usable.
|
|
1559
|
+
if (this.hashTable.get(url) !== hash) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
const cacheState = await this.lookupResourceWithoutHash(url);
|
|
1563
|
+
return cacheState && cacheState.response;
|
|
1601
1564
|
}
|
|
1602
1565
|
/**
|
|
1603
1566
|
* Check this version for a given resource regardless of its hash.
|
|
@@ -1605,45 +1568,44 @@
|
|
|
1605
1568
|
lookupResourceWithoutHash(url) {
|
|
1606
1569
|
// Limit the search to asset groups, and only scan the cache, don't
|
|
1607
1570
|
// load resources from the network.
|
|
1608
|
-
return this.assetGroups.reduce((potentialResponse, group) =>
|
|
1609
|
-
const resp =
|
|
1571
|
+
return this.assetGroups.reduce(async (potentialResponse, group) => {
|
|
1572
|
+
const resp = await potentialResponse;
|
|
1610
1573
|
if (resp !== null) {
|
|
1611
1574
|
return resp;
|
|
1612
1575
|
}
|
|
1613
1576
|
// fetchFromCacheOnly() avoids any network fetches, and returns the
|
|
1614
1577
|
// full set of cache data, not just the Response.
|
|
1615
1578
|
return group.fetchFromCacheOnly(url);
|
|
1616
|
-
}
|
|
1579
|
+
}, Promise.resolve(null));
|
|
1617
1580
|
}
|
|
1618
1581
|
/**
|
|
1619
1582
|
* List all unhashed resources from all asset groups.
|
|
1620
1583
|
*/
|
|
1621
1584
|
previouslyCachedResources() {
|
|
1622
|
-
return this.assetGroups.reduce((resources, group) =>
|
|
1623
|
-
}
|
|
1624
|
-
recentCacheStatus(url) {
|
|
1625
|
-
return
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}), Promise.resolve(UpdateCacheStatus.NOT_CACHED));
|
|
1637
|
-
});
|
|
1585
|
+
return this.assetGroups.reduce(async (resources, group) => (await resources).concat(await group.unhashedResources()), Promise.resolve([]));
|
|
1586
|
+
}
|
|
1587
|
+
async recentCacheStatus(url) {
|
|
1588
|
+
return this.assetGroups.reduce(async (current, group) => {
|
|
1589
|
+
const status = await current;
|
|
1590
|
+
if (status === UpdateCacheStatus.CACHED) {
|
|
1591
|
+
return status;
|
|
1592
|
+
}
|
|
1593
|
+
const groupStatus = await group.cacheStatus(url);
|
|
1594
|
+
if (groupStatus === UpdateCacheStatus.NOT_CACHED) {
|
|
1595
|
+
return status;
|
|
1596
|
+
}
|
|
1597
|
+
return groupStatus;
|
|
1598
|
+
}, Promise.resolve(UpdateCacheStatus.NOT_CACHED));
|
|
1638
1599
|
}
|
|
1639
1600
|
/**
|
|
1640
|
-
*
|
|
1601
|
+
* Return a list of the names of all caches used by this version.
|
|
1641
1602
|
*/
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1603
|
+
async getCacheNames() {
|
|
1604
|
+
const allGroupCacheNames = await Promise.all([
|
|
1605
|
+
...this.assetGroups.map(group => group.getCacheNames()),
|
|
1606
|
+
...this.dataGroups.map(group => group.getCacheNames()),
|
|
1607
|
+
]);
|
|
1608
|
+
return [].concat(...allGroupCacheNames);
|
|
1647
1609
|
}
|
|
1648
1610
|
/**
|
|
1649
1611
|
* Get the opaque application data which was provided with the manifest.
|
|
@@ -1671,6 +1633,7 @@
|
|
|
1671
1633
|
* Use of this source code is governed by an MIT-style license that can be
|
|
1672
1634
|
* found in the LICENSE file at https://angular.io/license
|
|
1673
1635
|
*/
|
|
1636
|
+
const SW_VERSION = '13.0.2';
|
|
1674
1637
|
const DEBUG_LOG_BUFFER_SIZE = 100;
|
|
1675
1638
|
class DebugHandler {
|
|
1676
1639
|
constructor(driver, adapter) {
|
|
@@ -1684,24 +1647,24 @@
|
|
|
1684
1647
|
this.debugLogA = [];
|
|
1685
1648
|
this.debugLogB = [];
|
|
1686
1649
|
}
|
|
1687
|
-
handleFetch(req) {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
const msgState = `NGSW Debug Info:
|
|
1650
|
+
async handleFetch(req) {
|
|
1651
|
+
const [state, versions, idle] = await Promise.all([
|
|
1652
|
+
this.driver.debugState(),
|
|
1653
|
+
this.driver.debugVersions(),
|
|
1654
|
+
this.driver.debugIdleState(),
|
|
1655
|
+
]);
|
|
1656
|
+
const msgState = `NGSW Debug Info:
|
|
1695
1657
|
|
|
1658
|
+
Driver version: ${SW_VERSION}
|
|
1696
1659
|
Driver state: ${state.state} (${state.why})
|
|
1697
1660
|
Latest manifest hash: ${state.latestHash || 'none'}
|
|
1698
1661
|
Last update check: ${this.since(state.lastUpdateCheck)}`;
|
|
1699
|
-
|
|
1700
|
-
|
|
1662
|
+
const msgVersions = versions
|
|
1663
|
+
.map(version => `=== Version ${version.hash} ===
|
|
1701
1664
|
|
|
1702
1665
|
Clients: ${version.clients.join(', ')}`)
|
|
1703
|
-
|
|
1704
|
-
|
|
1666
|
+
.join('\n\n');
|
|
1667
|
+
const msgIdle = `=== Idle Task Queue ===
|
|
1705
1668
|
Last update tick: ${this.since(idle.lastTrigger)}
|
|
1706
1669
|
Last update run: ${this.since(idle.lastRun)}
|
|
1707
1670
|
Task queue:
|
|
@@ -1711,12 +1674,11 @@ Debug log:
|
|
|
1711
1674
|
${this.formatDebugLog(this.debugLogB)}
|
|
1712
1675
|
${this.formatDebugLog(this.debugLogA)}
|
|
1713
1676
|
`;
|
|
1714
|
-
|
|
1677
|
+
return this.adapter.newResponse(`${msgState}
|
|
1715
1678
|
|
|
1716
1679
|
${msgVersions}
|
|
1717
1680
|
|
|
1718
1681
|
${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }) });
|
|
1719
|
-
});
|
|
1720
1682
|
}
|
|
1721
1683
|
since(time) {
|
|
1722
1684
|
if (time === null) {
|
|
@@ -1778,55 +1740,50 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
1778
1740
|
this.lastRun = null;
|
|
1779
1741
|
this.oldestScheduledAt = null;
|
|
1780
1742
|
}
|
|
1781
|
-
trigger() {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
this.emptyResolve = null;
|
|
1826
|
-
}
|
|
1827
|
-
this.empty = Promise.resolve();
|
|
1828
|
-
this.oldestScheduledAt = null;
|
|
1829
|
-
});
|
|
1743
|
+
async trigger() {
|
|
1744
|
+
this.lastTrigger = this.adapter.time;
|
|
1745
|
+
if (this.queue.length === 0) {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (this.scheduled !== null) {
|
|
1749
|
+
this.scheduled.cancel = true;
|
|
1750
|
+
}
|
|
1751
|
+
const scheduled = {
|
|
1752
|
+
cancel: false,
|
|
1753
|
+
};
|
|
1754
|
+
this.scheduled = scheduled;
|
|
1755
|
+
// Ensure that no task remains pending for longer than `this.maxDelay` ms.
|
|
1756
|
+
const now = this.adapter.time;
|
|
1757
|
+
const maxDelay = Math.max(0, (this.oldestScheduledAt ?? now) + this.maxDelay - now);
|
|
1758
|
+
const delay = Math.min(maxDelay, this.delay);
|
|
1759
|
+
await this.adapter.timeout(delay);
|
|
1760
|
+
if (scheduled.cancel) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
this.scheduled = null;
|
|
1764
|
+
await this.execute();
|
|
1765
|
+
}
|
|
1766
|
+
async execute() {
|
|
1767
|
+
this.lastRun = this.adapter.time;
|
|
1768
|
+
while (this.queue.length > 0) {
|
|
1769
|
+
const queue = this.queue;
|
|
1770
|
+
this.queue = [];
|
|
1771
|
+
await queue.reduce(async (previous, task) => {
|
|
1772
|
+
await previous;
|
|
1773
|
+
try {
|
|
1774
|
+
await task.run();
|
|
1775
|
+
}
|
|
1776
|
+
catch (err) {
|
|
1777
|
+
this.debug.log(err, `while running idle task ${task.desc}`);
|
|
1778
|
+
}
|
|
1779
|
+
}, Promise.resolve());
|
|
1780
|
+
}
|
|
1781
|
+
if (this.emptyResolve !== null) {
|
|
1782
|
+
this.emptyResolve();
|
|
1783
|
+
this.emptyResolve = null;
|
|
1784
|
+
}
|
|
1785
|
+
this.empty = Promise.resolve();
|
|
1786
|
+
this.oldestScheduledAt = null;
|
|
1830
1787
|
}
|
|
1831
1788
|
schedule(desc, run) {
|
|
1832
1789
|
this.queue.push({ desc, run });
|
|
@@ -1943,6 +1900,8 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
1943
1900
|
*/
|
|
1944
1901
|
this.loggedInvalidOnlyIfCachedRequest = false;
|
|
1945
1902
|
this.ngswStatePath = this.adapter.parseUrl('ngsw/state', this.scope.registration.scope).path;
|
|
1903
|
+
// A promise resolving to the control DB table.
|
|
1904
|
+
this.controlTable = this.db.open('control');
|
|
1946
1905
|
// The install event is triggered when the service worker is first installed.
|
|
1947
1906
|
this.scope.addEventListener('install', (event) => {
|
|
1948
1907
|
// SW code updates are separate from application updates, so code updates are
|
|
@@ -1954,22 +1913,22 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
1954
1913
|
// The activate event is triggered when this version of the service worker is
|
|
1955
1914
|
// first activated.
|
|
1956
1915
|
this.scope.addEventListener('activate', (event) => {
|
|
1957
|
-
event.waitUntil((() =>
|
|
1916
|
+
event.waitUntil((async () => {
|
|
1958
1917
|
// As above, it's safe to take over from existing clients immediately, since the new SW
|
|
1959
1918
|
// version will continue to serve the old application.
|
|
1960
|
-
|
|
1919
|
+
await this.scope.clients.claim();
|
|
1961
1920
|
// Once all clients have been taken over, we can delete caches used by old versions of
|
|
1962
1921
|
// `@angular/service-worker`, which are no longer needed. This can happen in the background.
|
|
1963
|
-
this.idle.schedule('activate: cleanup-old-sw-caches', () =>
|
|
1922
|
+
this.idle.schedule('activate: cleanup-old-sw-caches', async () => {
|
|
1964
1923
|
try {
|
|
1965
|
-
|
|
1924
|
+
await this.cleanupOldSwCaches();
|
|
1966
1925
|
}
|
|
1967
1926
|
catch (err) {
|
|
1968
1927
|
// Nothing to do - cleanup failed. Just log it.
|
|
1969
1928
|
this.debugger.log(err, 'cleanupOldSwCaches @ activate: cleanup-old-sw-caches');
|
|
1970
1929
|
}
|
|
1971
|
-
})
|
|
1972
|
-
})
|
|
1930
|
+
});
|
|
1931
|
+
})());
|
|
1973
1932
|
// Rather than wait for the first fetch event, which may not arrive until
|
|
1974
1933
|
// the next time the application is loaded, the SW takes advantage of the
|
|
1975
1934
|
// activation event to schedule initialization. However, if this were run
|
|
@@ -2065,7 +2024,7 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2065
2024
|
if (!data || !data.action) {
|
|
2066
2025
|
return;
|
|
2067
2026
|
}
|
|
2068
|
-
event.waitUntil((() =>
|
|
2027
|
+
event.waitUntil((async () => {
|
|
2069
2028
|
// Initialization is the only event which is sent directly from the SW to itself, and thus
|
|
2070
2029
|
// `event.source` is not a `Client`. Handle it here, before the check for `Client` sources.
|
|
2071
2030
|
if (data.action === 'INITIALIZE') {
|
|
@@ -2077,9 +2036,9 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2077
2036
|
return;
|
|
2078
2037
|
}
|
|
2079
2038
|
// Handle the message and keep the SW alive until it's handled.
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
})
|
|
2039
|
+
await this.ensureInitialized(event);
|
|
2040
|
+
await this.handleMessage(data, event.source);
|
|
2041
|
+
})());
|
|
2083
2042
|
}
|
|
2084
2043
|
onPush(msg) {
|
|
2085
2044
|
// Push notifications without data have no effect.
|
|
@@ -2093,290 +2052,309 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2093
2052
|
// Handle the click event and keep the SW alive until it's handled.
|
|
2094
2053
|
event.waitUntil(this.handleClick(event.notification, event.action));
|
|
2095
2054
|
}
|
|
2096
|
-
ensureInitialized(event) {
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
}
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
handleMessage(msg, from) {
|
|
2124
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2125
|
-
if (isMsgCheckForUpdates(msg)) {
|
|
2126
|
-
const action = (() => __awaiter(this, void 0, void 0, function* () {
|
|
2127
|
-
yield this.checkForUpdate();
|
|
2128
|
-
}))();
|
|
2129
|
-
yield this.reportStatus(from, action, msg.statusNonce);
|
|
2130
|
-
}
|
|
2131
|
-
else if (isMsgActivateUpdate(msg)) {
|
|
2132
|
-
yield this.reportStatus(from, this.updateClient(from), msg.statusNonce);
|
|
2133
|
-
}
|
|
2134
|
-
});
|
|
2055
|
+
async ensureInitialized(event) {
|
|
2056
|
+
// Since the SW may have just been started, it may or may not have been initialized already.
|
|
2057
|
+
// `this.initialized` will be `null` if initialization has not yet been attempted, or will be a
|
|
2058
|
+
// `Promise` which will resolve (successfully or unsuccessfully) if it has.
|
|
2059
|
+
if (this.initialized !== null) {
|
|
2060
|
+
return this.initialized;
|
|
2061
|
+
}
|
|
2062
|
+
// Initialization has not yet been attempted, so attempt it. This should only ever happen once
|
|
2063
|
+
// per SW instantiation.
|
|
2064
|
+
try {
|
|
2065
|
+
this.initialized = this.initialize();
|
|
2066
|
+
await this.initialized;
|
|
2067
|
+
}
|
|
2068
|
+
catch (error) {
|
|
2069
|
+
// If initialization fails, the SW needs to enter a safe state, where it declines to respond
|
|
2070
|
+
// to network requests.
|
|
2071
|
+
this.state = DriverReadyState.SAFE_MODE;
|
|
2072
|
+
this.stateMessage = `Initialization failed due to error: ${errorToString(error)}`;
|
|
2073
|
+
throw error;
|
|
2074
|
+
}
|
|
2075
|
+
finally {
|
|
2076
|
+
// Regardless if initialization succeeded, background tasks still need to happen.
|
|
2077
|
+
event.waitUntil(this.idle.trigger());
|
|
2078
|
+
}
|
|
2135
2079
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
const desc = data.notification;
|
|
2146
|
-
let options = {};
|
|
2147
|
-
NOTIFICATION_OPTION_NAMES.filter(name => desc.hasOwnProperty(name))
|
|
2148
|
-
.forEach(name => options[name] = desc[name]);
|
|
2149
|
-
yield this.scope.registration.showNotification(desc['title'], options);
|
|
2150
|
-
});
|
|
2080
|
+
async handleMessage(msg, from) {
|
|
2081
|
+
if (isMsgCheckForUpdates(msg)) {
|
|
2082
|
+
const action = this.checkForUpdate();
|
|
2083
|
+
await this.completeOperation(from, action, msg.nonce);
|
|
2084
|
+
}
|
|
2085
|
+
else if (isMsgActivateUpdate(msg)) {
|
|
2086
|
+
const action = this.updateClient(from);
|
|
2087
|
+
await this.completeOperation(from, action, msg.nonce);
|
|
2088
|
+
}
|
|
2151
2089
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
// The filter uses `name in notification` because the properties are on the prototype so
|
|
2157
|
-
// hasOwnProperty does not work here
|
|
2158
|
-
NOTIFICATION_OPTION_NAMES.filter(name => name in notification)
|
|
2159
|
-
.forEach(name => options[name] = notification[name]);
|
|
2160
|
-
yield this.broadcast({
|
|
2161
|
-
type: 'NOTIFICATION_CLICK',
|
|
2162
|
-
data: { action, notification: options },
|
|
2163
|
-
});
|
|
2090
|
+
async handlePush(data) {
|
|
2091
|
+
await this.broadcast({
|
|
2092
|
+
type: 'PUSH',
|
|
2093
|
+
data,
|
|
2164
2094
|
});
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2095
|
+
if (!data.notification || !data.notification.title) {
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
const desc = data.notification;
|
|
2099
|
+
let options = {};
|
|
2100
|
+
NOTIFICATION_OPTION_NAMES.filter(name => desc.hasOwnProperty(name))
|
|
2101
|
+
.forEach(name => options[name] = desc[name]);
|
|
2102
|
+
await this.scope.registration.showNotification(desc['title'], options);
|
|
2103
|
+
}
|
|
2104
|
+
async handleClick(notification, action) {
|
|
2105
|
+
notification.close();
|
|
2106
|
+
const options = {};
|
|
2107
|
+
// The filter uses `name in notification` because the properties are on the prototype so
|
|
2108
|
+
// hasOwnProperty does not work here
|
|
2109
|
+
NOTIFICATION_OPTION_NAMES.filter(name => name in notification)
|
|
2110
|
+
.forEach(name => options[name] = notification[name]);
|
|
2111
|
+
const notificationAction = action === '' || action === undefined ? 'default' : action;
|
|
2112
|
+
const onActionClick = notification?.data?.onActionClick?.[notificationAction];
|
|
2113
|
+
const urlToOpen = new URL(onActionClick?.url ?? '', this.scope.registration.scope).href;
|
|
2114
|
+
switch (onActionClick?.operation) {
|
|
2115
|
+
case 'openWindow':
|
|
2116
|
+
await this.scope.clients.openWindow(urlToOpen);
|
|
2117
|
+
break;
|
|
2118
|
+
case 'focusLastFocusedOrOpen': {
|
|
2119
|
+
let matchingClient = await this.getLastFocusedMatchingClient(this.scope);
|
|
2120
|
+
if (matchingClient) {
|
|
2121
|
+
await matchingClient?.focus();
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
await this.scope.clients.openWindow(urlToOpen);
|
|
2125
|
+
}
|
|
2126
|
+
break;
|
|
2172
2127
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2128
|
+
case 'navigateLastFocusedOrOpen': {
|
|
2129
|
+
let matchingClient = await this.getLastFocusedMatchingClient(this.scope);
|
|
2130
|
+
if (matchingClient) {
|
|
2131
|
+
matchingClient = await matchingClient.navigate(urlToOpen);
|
|
2132
|
+
await matchingClient?.focus();
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
await this.scope.clients.openWindow(urlToOpen);
|
|
2136
|
+
}
|
|
2137
|
+
break;
|
|
2175
2138
|
}
|
|
2176
|
-
}
|
|
2139
|
+
}
|
|
2140
|
+
await this.broadcast({
|
|
2141
|
+
type: 'NOTIFICATION_CLICK',
|
|
2142
|
+
data: { action, notification: options },
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
async getLastFocusedMatchingClient(scope) {
|
|
2146
|
+
const windowClients = await scope.clients.matchAll({ type: 'window' });
|
|
2147
|
+
// As per the spec windowClients are `sorted in the most recently focused order`
|
|
2148
|
+
return windowClients[0];
|
|
2149
|
+
}
|
|
2150
|
+
async completeOperation(client, promise, nonce) {
|
|
2151
|
+
const response = { type: 'OPERATION_COMPLETED', nonce };
|
|
2152
|
+
try {
|
|
2153
|
+
client.postMessage({
|
|
2154
|
+
...response,
|
|
2155
|
+
result: await promise,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
catch (e) {
|
|
2159
|
+
client.postMessage({
|
|
2160
|
+
...response,
|
|
2161
|
+
error: e.toString(),
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2177
2164
|
}
|
|
2178
|
-
updateClient(client) {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
});
|
|
2165
|
+
async updateClient(client) {
|
|
2166
|
+
// Figure out which version the client is on. If it's not on the latest,
|
|
2167
|
+
// it needs to be moved.
|
|
2168
|
+
const existing = this.clientVersionMap.get(client.id);
|
|
2169
|
+
if (existing === this.latestHash) {
|
|
2170
|
+
// Nothing to do, this client is already on the latest version.
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
// Switch the client over.
|
|
2174
|
+
let previous = undefined;
|
|
2175
|
+
// Look up the application data associated with the existing version. If there
|
|
2176
|
+
// isn't any, fall back on using the hash.
|
|
2177
|
+
if (existing !== undefined) {
|
|
2178
|
+
const existingVersion = this.versions.get(existing);
|
|
2179
|
+
previous = this.mergeHashWithAppData(existingVersion.manifest, existing);
|
|
2180
|
+
}
|
|
2181
|
+
// Set the current version used by the client, and sync the mapping to disk.
|
|
2182
|
+
this.clientVersionMap.set(client.id, this.latestHash);
|
|
2183
|
+
await this.sync();
|
|
2184
|
+
// Notify the client about this activation.
|
|
2185
|
+
const current = this.versions.get(this.latestHash);
|
|
2186
|
+
const notice = {
|
|
2187
|
+
type: 'UPDATE_ACTIVATED',
|
|
2188
|
+
previous,
|
|
2189
|
+
current: this.mergeHashWithAppData(current.manifest, this.latestHash),
|
|
2190
|
+
};
|
|
2191
|
+
client.postMessage(notice);
|
|
2192
|
+
return true;
|
|
2207
2193
|
}
|
|
2208
|
-
handleFetch(event) {
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
this.
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
try
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2194
|
+
async handleFetch(event) {
|
|
2195
|
+
try {
|
|
2196
|
+
// Ensure the SW instance has been initialized.
|
|
2197
|
+
await this.ensureInitialized(event);
|
|
2198
|
+
}
|
|
2199
|
+
catch {
|
|
2200
|
+
// Since the SW is already committed to responding to the currently active request,
|
|
2201
|
+
// respond with a network fetch.
|
|
2202
|
+
return this.safeFetch(event.request);
|
|
2203
|
+
}
|
|
2204
|
+
// On navigation requests, check for new updates.
|
|
2205
|
+
if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
|
|
2206
|
+
this.scheduledNavUpdateCheck = true;
|
|
2207
|
+
this.idle.schedule('check-updates-on-navigation', async () => {
|
|
2208
|
+
this.scheduledNavUpdateCheck = false;
|
|
2209
|
+
await this.checkForUpdate();
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
// Decide which version of the app to use to serve this request. This is asynchronous as in
|
|
2213
|
+
// some cases, a record will need to be written to disk about the assignment that is made.
|
|
2214
|
+
const appVersion = await this.assignVersion(event);
|
|
2215
|
+
let res = null;
|
|
2216
|
+
try {
|
|
2217
|
+
if (appVersion !== null) {
|
|
2218
|
+
try {
|
|
2219
|
+
// Handle the request. First try the AppVersion. If that doesn't work, fall back on the
|
|
2220
|
+
// network.
|
|
2221
|
+
res = await appVersion.handleFetch(event.request, event);
|
|
2222
|
+
}
|
|
2223
|
+
catch (err) {
|
|
2224
|
+
if (err.isUnrecoverableState) {
|
|
2225
|
+
await this.notifyClientsAboutUnrecoverableState(appVersion, err.message);
|
|
2237
2226
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
// Something went wrong with the activation of this version.
|
|
2244
|
-
yield this.versionFailed(appVersion, err);
|
|
2245
|
-
return this.safeFetch(event.request);
|
|
2246
|
-
}
|
|
2247
|
-
throw err;
|
|
2227
|
+
if (err.isCritical) {
|
|
2228
|
+
// Something went wrong with handling the request from this version.
|
|
2229
|
+
this.debugger.log(err, `Driver.handleFetch(version: ${appVersion.manifestHash})`);
|
|
2230
|
+
await this.versionFailed(appVersion, err);
|
|
2231
|
+
return this.safeFetch(event.request);
|
|
2248
2232
|
}
|
|
2233
|
+
throw err;
|
|
2249
2234
|
}
|
|
2250
|
-
// The response will be `null` only if no `AppVersion` can be assigned to the request or if
|
|
2251
|
-
// the assigned `AppVersion`'s manifest doesn't specify what to do about the request.
|
|
2252
|
-
// In that case, just fall back on the network.
|
|
2253
|
-
if (res === null) {
|
|
2254
|
-
return this.safeFetch(event.request);
|
|
2255
|
-
}
|
|
2256
|
-
// The `AppVersion` returned a usable response, so return it.
|
|
2257
|
-
return res;
|
|
2258
2235
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
event.waitUntil(this.idle.trigger());
|
|
2236
|
+
// The response will be `null` only if no `AppVersion` can be assigned to the request or if
|
|
2237
|
+
// the assigned `AppVersion`'s manifest doesn't specify what to do about the request.
|
|
2238
|
+
// In that case, just fall back on the network.
|
|
2239
|
+
if (res === null) {
|
|
2240
|
+
return this.safeFetch(event.request);
|
|
2265
2241
|
}
|
|
2266
|
-
|
|
2242
|
+
// The `AppVersion` returned a usable response, so return it.
|
|
2243
|
+
return res;
|
|
2244
|
+
}
|
|
2245
|
+
finally {
|
|
2246
|
+
// Trigger the idle scheduling system. The Promise returned by `trigger()` will resolve after
|
|
2247
|
+
// a specific amount of time has passed. If `trigger()` hasn't been called again by then (e.g.
|
|
2248
|
+
// on a subsequent request), the idle task queue will be drained and the `Promise` won't
|
|
2249
|
+
// be resolved until that operation is complete as well.
|
|
2250
|
+
event.waitUntil(this.idle.trigger());
|
|
2251
|
+
}
|
|
2267
2252
|
}
|
|
2268
2253
|
/**
|
|
2269
2254
|
* Attempt to quickly reach a state where it's safe to serve responses.
|
|
2270
2255
|
*/
|
|
2271
|
-
initialize() {
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
this.idle.schedule('init post-load (update, cleanup)', () => __awaiter(this, void 0, void 0, function* () {
|
|
2303
|
-
yield this.checkForUpdate();
|
|
2304
|
-
try {
|
|
2305
|
-
yield this.cleanupCaches();
|
|
2306
|
-
}
|
|
2307
|
-
catch (err) {
|
|
2308
|
-
// Nothing to do - cleanup failed. Just log it.
|
|
2309
|
-
this.debugger.log(err, 'cleanupCaches @ init post-load');
|
|
2310
|
-
}
|
|
2311
|
-
}));
|
|
2312
|
-
}
|
|
2313
|
-
catch (_) {
|
|
2314
|
-
// Something went wrong. Try to start over by fetching a new manifest from the
|
|
2315
|
-
// server and building up an empty initial state.
|
|
2316
|
-
const manifest = yield this.fetchLatestManifest();
|
|
2317
|
-
const hash = hashManifest(manifest);
|
|
2318
|
-
manifests = {};
|
|
2319
|
-
manifests[hash] = manifest;
|
|
2320
|
-
assignments = {};
|
|
2321
|
-
latest = { latest: hash };
|
|
2322
|
-
// Save the initial state to the DB.
|
|
2323
|
-
yield Promise.all([
|
|
2324
|
-
table.write('manifests', manifests),
|
|
2325
|
-
table.write('assignments', assignments),
|
|
2326
|
-
table.write('latest', latest),
|
|
2327
|
-
]);
|
|
2328
|
-
}
|
|
2329
|
-
// At this point, either the state has been loaded successfully, or fresh state
|
|
2330
|
-
// with a new copy of the manifest has been produced. At this point, the `Driver`
|
|
2331
|
-
// can have its internals hydrated from the state.
|
|
2332
|
-
// Initialize the `versions` map by setting each hash to a new `AppVersion` instance
|
|
2333
|
-
// for that manifest.
|
|
2334
|
-
Object.keys(manifests).forEach((hash) => {
|
|
2335
|
-
const manifest = manifests[hash];
|
|
2336
|
-
// If the manifest is newly initialized, an AppVersion may have already been
|
|
2337
|
-
// created for it.
|
|
2338
|
-
if (!this.versions.has(hash)) {
|
|
2339
|
-
this.versions.set(hash, new AppVersion(this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash));
|
|
2340
|
-
}
|
|
2341
|
-
});
|
|
2342
|
-
// Map each client ID to its associated hash. Along the way, verify that the hash
|
|
2343
|
-
// is still valid for that client ID. It should not be possible for a client to
|
|
2344
|
-
// still be associated with a hash that was since removed from the state.
|
|
2345
|
-
Object.keys(assignments).forEach((clientId) => {
|
|
2346
|
-
const hash = assignments[clientId];
|
|
2347
|
-
if (this.versions.has(hash)) {
|
|
2348
|
-
this.clientVersionMap.set(clientId, hash);
|
|
2349
|
-
}
|
|
2350
|
-
else {
|
|
2351
|
-
this.clientVersionMap.set(clientId, latest.latest);
|
|
2352
|
-
this.debugger.log(`Unknown version ${hash} mapped for client ${clientId}, using latest instead`, `initialize: map assignments`);
|
|
2353
|
-
}
|
|
2256
|
+
async initialize() {
|
|
2257
|
+
// On initialization, all of the serialized state is read out of the 'control'
|
|
2258
|
+
// table. This includes:
|
|
2259
|
+
// - map of hashes to manifests of currently loaded application versions
|
|
2260
|
+
// - map of client IDs to their pinned versions
|
|
2261
|
+
// - record of the most recently fetched manifest hash
|
|
2262
|
+
//
|
|
2263
|
+
// If these values don't exist in the DB, then this is the either the first time
|
|
2264
|
+
// the SW has run or the DB state has been wiped or is inconsistent. In that case,
|
|
2265
|
+
// load a fresh copy of the manifest and reset the state from scratch.
|
|
2266
|
+
const table = await this.controlTable;
|
|
2267
|
+
// Attempt to load the needed state from the DB. If this fails, the catch {} block
|
|
2268
|
+
// will populate these variables with freshly constructed values.
|
|
2269
|
+
let manifests, assignments, latest;
|
|
2270
|
+
try {
|
|
2271
|
+
// Read them from the DB simultaneously.
|
|
2272
|
+
[manifests, assignments, latest] = await Promise.all([
|
|
2273
|
+
table.read('manifests'),
|
|
2274
|
+
table.read('assignments'),
|
|
2275
|
+
table.read('latest'),
|
|
2276
|
+
]);
|
|
2277
|
+
// Make sure latest manifest is correctly installed. If not (e.g. corrupted data),
|
|
2278
|
+
// it could stay locked in EXISTING_CLIENTS_ONLY or SAFE_MODE state.
|
|
2279
|
+
if (!this.versions.has(latest.latest) && !manifests.hasOwnProperty(latest.latest)) {
|
|
2280
|
+
this.debugger.log(`Missing manifest for latest version hash ${latest.latest}`, 'initialize: read from DB');
|
|
2281
|
+
throw new Error(`Missing manifest for latest hash ${latest.latest}`);
|
|
2282
|
+
}
|
|
2283
|
+
// Successfully loaded from saved state. This implies a manifest exists, so
|
|
2284
|
+
// the update check needs to happen in the background.
|
|
2285
|
+
this.idle.schedule('init post-load (update)', async () => {
|
|
2286
|
+
await this.checkForUpdate();
|
|
2354
2287
|
});
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
//
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
//
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2288
|
+
}
|
|
2289
|
+
catch (_) {
|
|
2290
|
+
// Something went wrong. Try to start over by fetching a new manifest from the
|
|
2291
|
+
// server and building up an empty initial state.
|
|
2292
|
+
const manifest = await this.fetchLatestManifest();
|
|
2293
|
+
const hash = hashManifest(manifest);
|
|
2294
|
+
manifests = { [hash]: manifest };
|
|
2295
|
+
assignments = {};
|
|
2296
|
+
latest = { latest: hash };
|
|
2297
|
+
// Save the initial state to the DB.
|
|
2298
|
+
await Promise.all([
|
|
2299
|
+
table.write('manifests', manifests),
|
|
2300
|
+
table.write('assignments', assignments),
|
|
2301
|
+
table.write('latest', latest),
|
|
2302
|
+
]);
|
|
2303
|
+
}
|
|
2304
|
+
// At this point, either the state has been loaded successfully, or fresh state
|
|
2305
|
+
// with a new copy of the manifest has been produced. At this point, the `Driver`
|
|
2306
|
+
// can have its internals hydrated from the state.
|
|
2307
|
+
// Schedule cleaning up obsolete caches in the background.
|
|
2308
|
+
this.idle.schedule('init post-load (cleanup)', async () => {
|
|
2309
|
+
await this.cleanupCaches();
|
|
2310
|
+
});
|
|
2311
|
+
// Initialize the `versions` map by setting each hash to a new `AppVersion` instance
|
|
2312
|
+
// for that manifest.
|
|
2313
|
+
Object.keys(manifests).forEach((hash) => {
|
|
2314
|
+
const manifest = manifests[hash];
|
|
2315
|
+
// If the manifest is newly initialized, an AppVersion may have already been
|
|
2316
|
+
// created for it.
|
|
2317
|
+
if (!this.versions.has(hash)) {
|
|
2318
|
+
this.versions.set(hash, new AppVersion(this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash));
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
// Map each client ID to its associated hash. Along the way, verify that the hash
|
|
2322
|
+
// is still valid for that client ID. It should not be possible for a client to
|
|
2323
|
+
// still be associated with a hash that was since removed from the state.
|
|
2324
|
+
Object.keys(assignments).forEach((clientId) => {
|
|
2325
|
+
const hash = assignments[clientId];
|
|
2326
|
+
if (this.versions.has(hash)) {
|
|
2327
|
+
this.clientVersionMap.set(clientId, hash);
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
this.clientVersionMap.set(clientId, latest.latest);
|
|
2331
|
+
this.debugger.log(`Unknown version ${hash} mapped for client ${clientId}, using latest instead`, `initialize: map assignments`);
|
|
2332
|
+
}
|
|
2379
2333
|
});
|
|
2334
|
+
// Set the latest version.
|
|
2335
|
+
this.latestHash = latest.latest;
|
|
2336
|
+
// Finally, assert that the latest version is in fact loaded.
|
|
2337
|
+
if (!this.versions.has(latest.latest)) {
|
|
2338
|
+
throw new Error(`Invariant violated (initialize): latest hash ${latest.latest} has no known manifest`);
|
|
2339
|
+
}
|
|
2340
|
+
// Finally, wait for the scheduling of initialization of all versions in the
|
|
2341
|
+
// manifest. Ordinarily this just schedules the initializations to happen during
|
|
2342
|
+
// the next idle period, but in development mode this might actually wait for the
|
|
2343
|
+
// full initialization.
|
|
2344
|
+
// If any of these initializations fail, versionFailed() will be called either
|
|
2345
|
+
// synchronously or asynchronously to handle the failure and re-map clients.
|
|
2346
|
+
await Promise.all(Object.keys(manifests).map(async (hash) => {
|
|
2347
|
+
try {
|
|
2348
|
+
// Attempt to schedule or initialize this version. If this operation is
|
|
2349
|
+
// successful, then initialization either succeeded or was scheduled. If
|
|
2350
|
+
// it fails, then full initialization was attempted and failed.
|
|
2351
|
+
await this.scheduleInitialization(this.versions.get(hash));
|
|
2352
|
+
}
|
|
2353
|
+
catch (err) {
|
|
2354
|
+
this.debugger.log(err, `initialize: schedule init of ${hash}`);
|
|
2355
|
+
return false;
|
|
2356
|
+
}
|
|
2357
|
+
}));
|
|
2380
2358
|
}
|
|
2381
2359
|
lookupVersionByHash(hash, debugName = 'lookupVersionByHash') {
|
|
2382
2360
|
// The version should exist, but check just in case.
|
|
@@ -2388,185 +2366,166 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2388
2366
|
/**
|
|
2389
2367
|
* Decide which version of the manifest to use for the event.
|
|
2390
2368
|
*/
|
|
2391
|
-
assignVersion(event) {
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
}
|
|
2411
|
-
const client = yield this.scope.clients.get(clientId);
|
|
2412
|
-
yield this.updateClient(client);
|
|
2413
|
-
appVersion = this.lookupVersionByHash(this.latestHash, 'assignVersion');
|
|
2414
|
-
}
|
|
2415
|
-
// TODO: make sure the version is valid.
|
|
2416
|
-
return appVersion;
|
|
2417
|
-
}
|
|
2418
|
-
else {
|
|
2419
|
-
// This is the first time this client ID has been seen. Whether the SW is in a
|
|
2420
|
-
// state to handle new clients depends on the current readiness state, so check
|
|
2421
|
-
// that first.
|
|
2422
|
-
if (this.state !== DriverReadyState.NORMAL) {
|
|
2423
|
-
// It's not safe to serve new clients in the current state. It's possible that
|
|
2424
|
-
// this is an existing client which has not been mapped yet (see below) but
|
|
2425
|
-
// even if that is the case, it's invalid to make an assignment to a known
|
|
2426
|
-
// invalid version, even if that assignment was previously implicit. Return
|
|
2427
|
-
// undefined here to let the caller know that no assignment is possible at
|
|
2428
|
-
// this time.
|
|
2429
|
-
return null;
|
|
2430
|
-
}
|
|
2431
|
-
// It's safe to handle this request. Two cases apply. Either:
|
|
2432
|
-
// 1) the browser assigned a client ID at the time of the navigation request, and
|
|
2433
|
-
// this is truly the first time seeing this client, or
|
|
2434
|
-
// 2) a navigation request came previously from the same client, but with no client
|
|
2435
|
-
// ID attached. Browsers do this to avoid creating a client under the origin in
|
|
2436
|
-
// the event the navigation request is just redirected.
|
|
2437
|
-
//
|
|
2438
|
-
// In case 1, the latest version can safely be used.
|
|
2439
|
-
// In case 2, the latest version can be used, with the assumption that the previous
|
|
2440
|
-
// navigation request was answered under the same version. This assumption relies
|
|
2441
|
-
// on the fact that it's unlikely an update will come in between the navigation
|
|
2442
|
-
// request and requests for subsequent resources on that page.
|
|
2443
|
-
// First validate the current state.
|
|
2369
|
+
async assignVersion(event) {
|
|
2370
|
+
// First, check whether the event has a (non empty) client ID. If it does, the version may
|
|
2371
|
+
// already be associated.
|
|
2372
|
+
//
|
|
2373
|
+
// NOTE: For navigation requests, we care about the `resultingClientId`. If it is undefined or
|
|
2374
|
+
// the empty string (which is the case for sub-resource requests), we look at `clientId`.
|
|
2375
|
+
const clientId = event.resultingClientId || event.clientId;
|
|
2376
|
+
if (clientId) {
|
|
2377
|
+
// Check if there is an assigned client id.
|
|
2378
|
+
if (this.clientVersionMap.has(clientId)) {
|
|
2379
|
+
// There is an assignment for this client already.
|
|
2380
|
+
const hash = this.clientVersionMap.get(clientId);
|
|
2381
|
+
let appVersion = this.lookupVersionByHash(hash, 'assignVersion');
|
|
2382
|
+
// Ordinarily, this client would be served from its assigned version. But, if this
|
|
2383
|
+
// request is a navigation request, this client can be updated to the latest
|
|
2384
|
+
// version immediately.
|
|
2385
|
+
if (this.state === DriverReadyState.NORMAL && hash !== this.latestHash &&
|
|
2386
|
+
appVersion.isNavigationRequest(event.request)) {
|
|
2387
|
+
// Update this client to the latest version immediately.
|
|
2444
2388
|
if (this.latestHash === null) {
|
|
2445
2389
|
throw new Error(`Invariant violated (assignVersion): latestHash was null`);
|
|
2446
2390
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2391
|
+
const client = await this.scope.clients.get(clientId);
|
|
2392
|
+
if (client) {
|
|
2393
|
+
await this.updateClient(client);
|
|
2394
|
+
}
|
|
2395
|
+
appVersion = this.lookupVersionByHash(this.latestHash, 'assignVersion');
|
|
2452
2396
|
}
|
|
2397
|
+
// TODO: make sure the version is valid.
|
|
2398
|
+
return appVersion;
|
|
2453
2399
|
}
|
|
2454
2400
|
else {
|
|
2455
|
-
//
|
|
2456
|
-
//
|
|
2401
|
+
// This is the first time this client ID has been seen. Whether the SW is in a
|
|
2402
|
+
// state to handle new clients depends on the current readiness state, so check
|
|
2403
|
+
// that first.
|
|
2457
2404
|
if (this.state !== DriverReadyState.NORMAL) {
|
|
2405
|
+
// It's not safe to serve new clients in the current state. It's possible that
|
|
2406
|
+
// this is an existing client which has not been mapped yet (see below) but
|
|
2407
|
+
// even if that is the case, it's invalid to make an assignment to a known
|
|
2408
|
+
// invalid version, even if that assignment was previously implicit. Return
|
|
2409
|
+
// undefined here to let the caller know that no assignment is possible at
|
|
2410
|
+
// this time.
|
|
2458
2411
|
return null;
|
|
2459
2412
|
}
|
|
2460
|
-
//
|
|
2461
|
-
//
|
|
2413
|
+
// It's safe to handle this request. Two cases apply. Either:
|
|
2414
|
+
// 1) the browser assigned a client ID at the time of the navigation request, and
|
|
2415
|
+
// this is truly the first time seeing this client, or
|
|
2416
|
+
// 2) a navigation request came previously from the same client, but with no client
|
|
2417
|
+
// ID attached. Browsers do this to avoid creating a client under the origin in
|
|
2418
|
+
// the event the navigation request is just redirected.
|
|
2419
|
+
//
|
|
2420
|
+
// In case 1, the latest version can safely be used.
|
|
2421
|
+
// In case 2, the latest version can be used, with the assumption that the previous
|
|
2422
|
+
// navigation request was answered under the same version. This assumption relies
|
|
2423
|
+
// on the fact that it's unlikely an update will come in between the navigation
|
|
2424
|
+
// request and requests for subsequent resources on that page.
|
|
2462
2425
|
// First validate the current state.
|
|
2463
2426
|
if (this.latestHash === null) {
|
|
2464
2427
|
throw new Error(`Invariant violated (assignVersion): latestHash was null`);
|
|
2465
2428
|
}
|
|
2429
|
+
// Pin this client ID to the current latest version, indefinitely.
|
|
2430
|
+
this.clientVersionMap.set(clientId, this.latestHash);
|
|
2431
|
+
await this.sync();
|
|
2466
2432
|
// Return the latest `AppVersion`.
|
|
2467
2433
|
return this.lookupVersionByHash(this.latestHash, 'assignVersion');
|
|
2468
2434
|
}
|
|
2469
|
-
}
|
|
2435
|
+
}
|
|
2436
|
+
else {
|
|
2437
|
+
// No client ID was associated with the request. This must be a navigation request
|
|
2438
|
+
// for a new client. First check that the SW is accepting new clients.
|
|
2439
|
+
if (this.state !== DriverReadyState.NORMAL) {
|
|
2440
|
+
return null;
|
|
2441
|
+
}
|
|
2442
|
+
// Serve it with the latest version, and assume that the client will actually get
|
|
2443
|
+
// associated with that version on the next request.
|
|
2444
|
+
// First validate the current state.
|
|
2445
|
+
if (this.latestHash === null) {
|
|
2446
|
+
throw new Error(`Invariant violated (assignVersion): latestHash was null`);
|
|
2447
|
+
}
|
|
2448
|
+
// Return the latest `AppVersion`.
|
|
2449
|
+
return this.lookupVersionByHash(this.latestHash, 'assignVersion');
|
|
2450
|
+
}
|
|
2470
2451
|
}
|
|
2471
|
-
fetchLatestManifest(ignoreOfflineError = false) {
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
if (
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
yield this.scope.registration.unregister();
|
|
2478
|
-
}
|
|
2479
|
-
else if ((res.status === 503 || res.status === 504) && ignoreOfflineError) {
|
|
2480
|
-
return null;
|
|
2481
|
-
}
|
|
2482
|
-
throw new Error(`Manifest fetch failed! (status: ${res.status})`);
|
|
2452
|
+
async fetchLatestManifest(ignoreOfflineError = false) {
|
|
2453
|
+
const res = await this.safeFetch(this.adapter.newRequest('ngsw.json?ngsw-cache-bust=' + Math.random()));
|
|
2454
|
+
if (!res.ok) {
|
|
2455
|
+
if (res.status === 404) {
|
|
2456
|
+
await this.deleteAllCaches();
|
|
2457
|
+
await this.scope.registration.unregister();
|
|
2483
2458
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2459
|
+
else if ((res.status === 503 || res.status === 504) && ignoreOfflineError) {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
throw new Error(`Manifest fetch failed! (status: ${res.status})`);
|
|
2463
|
+
}
|
|
2464
|
+
this.lastUpdateCheck = this.adapter.time;
|
|
2465
|
+
return res.json();
|
|
2487
2466
|
}
|
|
2488
|
-
deleteAllCaches() {
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
const ownCacheNames = cacheNames.filter(name => name.startsWith(`${this.adapter.cacheNamePrefix}:`));
|
|
2492
|
-
yield Promise.all(ownCacheNames.map(name => this.scope.caches.delete(name)));
|
|
2493
|
-
});
|
|
2467
|
+
async deleteAllCaches() {
|
|
2468
|
+
const cacheNames = await this.adapter.caches.keys();
|
|
2469
|
+
await Promise.all(cacheNames.map(name => this.adapter.caches.delete(name)));
|
|
2494
2470
|
}
|
|
2495
2471
|
/**
|
|
2496
2472
|
* Schedule the SW's attempt to reach a fully prefetched state for the given AppVersion
|
|
2497
2473
|
* when the SW is not busy and has connectivity. This returns a Promise which must be
|
|
2498
2474
|
* awaited, as under some conditions the AppVersion might be initialized immediately.
|
|
2499
2475
|
*/
|
|
2500
|
-
scheduleInitialization(appVersion) {
|
|
2501
|
-
|
|
2502
|
-
const initialize = () => __awaiter(this, void 0, void 0, function* () {
|
|
2503
|
-
try {
|
|
2504
|
-
yield appVersion.initializeFully();
|
|
2505
|
-
}
|
|
2506
|
-
catch (err) {
|
|
2507
|
-
this.debugger.log(err, `initializeFully for ${appVersion.manifestHash}`);
|
|
2508
|
-
yield this.versionFailed(appVersion, err);
|
|
2509
|
-
}
|
|
2510
|
-
});
|
|
2511
|
-
// TODO: better logic for detecting localhost.
|
|
2512
|
-
if (this.scope.registration.scope.indexOf('://localhost') > -1) {
|
|
2513
|
-
return initialize();
|
|
2514
|
-
}
|
|
2515
|
-
this.idle.schedule(`initialization(${appVersion.manifestHash})`, initialize);
|
|
2516
|
-
});
|
|
2517
|
-
}
|
|
2518
|
-
versionFailed(appVersion, err) {
|
|
2519
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2520
|
-
// This particular AppVersion is broken. First, find the manifest hash.
|
|
2521
|
-
const broken = Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
|
|
2522
|
-
if (broken === undefined) {
|
|
2523
|
-
// This version is no longer in use anyway, so nobody cares.
|
|
2524
|
-
return;
|
|
2525
|
-
}
|
|
2526
|
-
const brokenHash = broken[0];
|
|
2527
|
-
const affectedClients = Array.from(this.clientVersionMap.entries())
|
|
2528
|
-
.filter(([clientId, hash]) => hash === brokenHash)
|
|
2529
|
-
.map(([clientId]) => clientId);
|
|
2530
|
-
// TODO: notify affected apps.
|
|
2531
|
-
// The action taken depends on whether the broken manifest is the active (latest) or not.
|
|
2532
|
-
// If so, the SW cannot accept new clients, but can continue to service old ones.
|
|
2533
|
-
if (this.latestHash === brokenHash) {
|
|
2534
|
-
// The latest manifest is broken. This means that new clients are at the mercy of the
|
|
2535
|
-
// network, but caches continue to be valid for previous versions. This is
|
|
2536
|
-
// unfortunate but unavoidable.
|
|
2537
|
-
this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
|
|
2538
|
-
this.stateMessage = `Degraded due to: ${errorToString(err)}`;
|
|
2539
|
-
// Cancel the binding for the affected clients.
|
|
2540
|
-
affectedClients.forEach(clientId => this.clientVersionMap.delete(clientId));
|
|
2541
|
-
}
|
|
2542
|
-
else {
|
|
2543
|
-
// The latest version is viable, but this older version isn't. The only
|
|
2544
|
-
// possible remedy is to stop serving the older version and go to the network.
|
|
2545
|
-
// Put the affected clients on the latest version.
|
|
2546
|
-
affectedClients.forEach(clientId => this.clientVersionMap.set(clientId, this.latestHash));
|
|
2547
|
-
}
|
|
2476
|
+
async scheduleInitialization(appVersion) {
|
|
2477
|
+
const initialize = async () => {
|
|
2548
2478
|
try {
|
|
2549
|
-
|
|
2479
|
+
await appVersion.initializeFully();
|
|
2550
2480
|
}
|
|
2551
|
-
catch (
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
this.debugger.log(err2, `Driver.versionFailed(${err.message || err})`);
|
|
2481
|
+
catch (err) {
|
|
2482
|
+
this.debugger.log(err, `initializeFully for ${appVersion.manifestHash}`);
|
|
2483
|
+
await this.versionFailed(appVersion, err);
|
|
2555
2484
|
}
|
|
2556
|
-
}
|
|
2485
|
+
};
|
|
2486
|
+
// TODO: better logic for detecting localhost.
|
|
2487
|
+
if (this.scope.registration.scope.indexOf('://localhost') > -1) {
|
|
2488
|
+
return initialize();
|
|
2489
|
+
}
|
|
2490
|
+
this.idle.schedule(`initialization(${appVersion.manifestHash})`, initialize);
|
|
2491
|
+
}
|
|
2492
|
+
async versionFailed(appVersion, err) {
|
|
2493
|
+
// This particular AppVersion is broken. First, find the manifest hash.
|
|
2494
|
+
const broken = Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
|
|
2495
|
+
if (broken === undefined) {
|
|
2496
|
+
// This version is no longer in use anyway, so nobody cares.
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const brokenHash = broken[0];
|
|
2500
|
+
// The specified version is broken and new clients should not be served from it. However, it is
|
|
2501
|
+
// deemed even riskier to switch the existing clients to a different version or to the network.
|
|
2502
|
+
// Therefore, we keep clients on their current version (even if broken) and ensure that no new
|
|
2503
|
+
// clients will be assigned to it.
|
|
2504
|
+
// TODO: notify affected apps.
|
|
2505
|
+
// The action taken depends on whether the broken manifest is the active (latest) or not.
|
|
2506
|
+
// - If the broken version is not the latest, no further action is necessary, since new clients
|
|
2507
|
+
// will be assigned to the latest version anyway.
|
|
2508
|
+
// - If the broken version is the latest, the SW cannot accept new clients (but can continue to
|
|
2509
|
+
// service old ones).
|
|
2510
|
+
if (this.latestHash === brokenHash) {
|
|
2511
|
+
// The latest manifest is broken. This means that new clients are at the mercy of the network,
|
|
2512
|
+
// but caches continue to be valid for previous versions. This is unfortunate but unavoidable.
|
|
2513
|
+
this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
|
|
2514
|
+
this.stateMessage = `Degraded due to: ${errorToString(err)}`;
|
|
2515
|
+
}
|
|
2557
2516
|
}
|
|
2558
|
-
setupUpdate(manifest, hash) {
|
|
2559
|
-
|
|
2517
|
+
async setupUpdate(manifest, hash) {
|
|
2518
|
+
try {
|
|
2560
2519
|
const newVersion = new AppVersion(this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash);
|
|
2561
2520
|
// Firstly, check if the manifest version is correct.
|
|
2562
2521
|
if (manifest.configVersion !== SUPPORTED_CONFIG_VERSION) {
|
|
2563
|
-
|
|
2564
|
-
|
|
2522
|
+
await this.deleteAllCaches();
|
|
2523
|
+
await this.scope.registration.unregister();
|
|
2565
2524
|
throw new Error(`Invalid config version: expected ${SUPPORTED_CONFIG_VERSION}, got ${manifest.configVersion}.`);
|
|
2566
2525
|
}
|
|
2567
2526
|
// Cause the new version to become fully initialized. If this fails, then the
|
|
2568
2527
|
// version will not be available for use.
|
|
2569
|
-
|
|
2528
|
+
await newVersion.initializeFully(this);
|
|
2570
2529
|
// Install this as an active version of the app.
|
|
2571
2530
|
this.versions.set(hash, newVersion);
|
|
2572
2531
|
// Future new clients will use this hash as the latest version.
|
|
@@ -2577,122 +2536,113 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2577
2536
|
this.state = DriverReadyState.NORMAL;
|
|
2578
2537
|
this.stateMessage = '(nominal)';
|
|
2579
2538
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
}
|
|
2539
|
+
await this.sync();
|
|
2540
|
+
await this.notifyClientsAboutVersionReady(manifest, hash);
|
|
2541
|
+
}
|
|
2542
|
+
catch (e) {
|
|
2543
|
+
await this.notifyClientsAboutVersionInstallationFailed(manifest, hash, e);
|
|
2544
|
+
throw e;
|
|
2545
|
+
}
|
|
2583
2546
|
}
|
|
2584
|
-
checkForUpdate() {
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
return false;
|
|
2594
|
-
}
|
|
2595
|
-
hash = hashManifest(manifest);
|
|
2596
|
-
// Check whether this is really an update.
|
|
2597
|
-
if (this.versions.has(hash)) {
|
|
2598
|
-
return false;
|
|
2599
|
-
}
|
|
2600
|
-
yield this.setupUpdate(manifest, hash);
|
|
2601
|
-
return true;
|
|
2547
|
+
async checkForUpdate() {
|
|
2548
|
+
let hash = '(unknown)';
|
|
2549
|
+
try {
|
|
2550
|
+
const manifest = await this.fetchLatestManifest(true);
|
|
2551
|
+
if (manifest === null) {
|
|
2552
|
+
// Client or server offline. Unable to check for updates at this time.
|
|
2553
|
+
// Continue to service clients (existing and new).
|
|
2554
|
+
this.debugger.log('Check for update aborted. (Client or server offline.)');
|
|
2555
|
+
return false;
|
|
2602
2556
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
this.stateMessage = `Degraded due to failed initialization: ${errorToString(err)}`;
|
|
2557
|
+
hash = hashManifest(manifest);
|
|
2558
|
+
// Check whether this is really an update.
|
|
2559
|
+
if (this.versions.has(hash)) {
|
|
2607
2560
|
return false;
|
|
2608
2561
|
}
|
|
2609
|
-
|
|
2562
|
+
await this.notifyClientsAboutVersionDetected(manifest, hash);
|
|
2563
|
+
await this.setupUpdate(manifest, hash);
|
|
2564
|
+
return true;
|
|
2565
|
+
}
|
|
2566
|
+
catch (err) {
|
|
2567
|
+
this.debugger.log(err, `Error occurred while updating to manifest ${hash}`);
|
|
2568
|
+
this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
|
|
2569
|
+
this.stateMessage = `Degraded due to failed initialization: ${errorToString(err)}`;
|
|
2570
|
+
return false;
|
|
2571
|
+
}
|
|
2610
2572
|
}
|
|
2611
2573
|
/**
|
|
2612
2574
|
* Synchronize the existing state to the underlying database.
|
|
2613
2575
|
*/
|
|
2614
|
-
sync() {
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
//
|
|
2644
|
-
//
|
|
2645
|
-
//
|
|
2646
|
-
const activeClients = (yield this.scope.clients.matchAll()).map(client => client.id);
|
|
2647
|
-
// A simple list of client ids that the SW has kept track of. Subtracting
|
|
2648
|
-
// activeClients from this list will result in the set of client ids which are
|
|
2649
|
-
// being tracked but are no longer used in the browser, and thus can be cleaned up.
|
|
2576
|
+
async sync() {
|
|
2577
|
+
const table = await this.controlTable;
|
|
2578
|
+
// Construct a serializable map of hashes to manifests.
|
|
2579
|
+
const manifests = {};
|
|
2580
|
+
this.versions.forEach((version, hash) => {
|
|
2581
|
+
manifests[hash] = version.manifest;
|
|
2582
|
+
});
|
|
2583
|
+
// Construct a serializable map of client ids to version hashes.
|
|
2584
|
+
const assignments = {};
|
|
2585
|
+
this.clientVersionMap.forEach((hash, clientId) => {
|
|
2586
|
+
assignments[clientId] = hash;
|
|
2587
|
+
});
|
|
2588
|
+
// Record the latest entry. Since this is a sync which is necessarily happening after
|
|
2589
|
+
// initialization, latestHash should always be valid.
|
|
2590
|
+
const latest = {
|
|
2591
|
+
latest: this.latestHash,
|
|
2592
|
+
};
|
|
2593
|
+
// Synchronize all of these.
|
|
2594
|
+
await Promise.all([
|
|
2595
|
+
table.write('manifests', manifests),
|
|
2596
|
+
table.write('assignments', assignments),
|
|
2597
|
+
table.write('latest', latest),
|
|
2598
|
+
]);
|
|
2599
|
+
}
|
|
2600
|
+
async cleanupCaches() {
|
|
2601
|
+
try {
|
|
2602
|
+
// Query for all currently active clients, and list the client IDs. This may skip some clients
|
|
2603
|
+
// in the browser back-forward cache, but not much can be done about that.
|
|
2604
|
+
const activeClients = new Set((await this.scope.clients.matchAll()).map(client => client.id));
|
|
2605
|
+
// A simple list of client IDs that the SW has kept track of. Subtracting `activeClients` from
|
|
2606
|
+
// this list will result in the set of client IDs which are being tracked but are no longer
|
|
2607
|
+
// used in the browser, and thus can be cleaned up.
|
|
2650
2608
|
const knownClients = Array.from(this.clientVersionMap.keys());
|
|
2651
|
-
// Remove clients in the clientVersionMap that are no longer active.
|
|
2652
|
-
knownClients.filter(id => activeClients.
|
|
2653
|
-
|
|
2654
|
-
// Next, determine the set of versions which are still used. All others can be
|
|
2655
|
-
|
|
2656
|
-
const usedVersions = new Set();
|
|
2657
|
-
this.clientVersionMap.forEach((version, _) => usedVersions.add(version));
|
|
2609
|
+
// Remove clients in the `clientVersionMap` that are no longer active.
|
|
2610
|
+
const obsoleteClients = knownClients.filter(id => !activeClients.has(id));
|
|
2611
|
+
obsoleteClients.forEach(id => this.clientVersionMap.delete(id));
|
|
2612
|
+
// Next, determine the set of versions which are still used. All others can be removed.
|
|
2613
|
+
const usedVersions = new Set(this.clientVersionMap.values());
|
|
2658
2614
|
// Collect all obsolete versions by filtering out used versions from the set of all versions.
|
|
2659
2615
|
const obsoleteVersions = Array.from(this.versions.keys())
|
|
2660
2616
|
.filter(version => !usedVersions.has(version) && version !== this.latestHash);
|
|
2661
2617
|
// Remove all the versions which are no longer used.
|
|
2662
|
-
|
|
2663
|
-
// Wait for the other cleanup operations to complete.
|
|
2664
|
-
yield previous;
|
|
2665
|
-
// Try to get past the failure of one particular version to clean up (this
|
|
2666
|
-
// shouldn't happen, but handle it just in case).
|
|
2667
|
-
try {
|
|
2668
|
-
// Get ahold of the AppVersion for this particular hash.
|
|
2669
|
-
const instance = this.versions.get(version);
|
|
2670
|
-
// Delete it from the canonical map.
|
|
2671
|
-
this.versions.delete(version);
|
|
2672
|
-
// Clean it up.
|
|
2673
|
-
yield instance.cleanup();
|
|
2674
|
-
}
|
|
2675
|
-
catch (err) {
|
|
2676
|
-
// Oh well? Not much that can be done here. These caches will be removed when
|
|
2677
|
-
// the SW revs its format version, which happens from time to time.
|
|
2678
|
-
this.debugger.log(err, `cleanupCaches - cleanup ${version}`);
|
|
2679
|
-
}
|
|
2680
|
-
}), Promise.resolve());
|
|
2618
|
+
obsoleteVersions.forEach(version => this.versions.delete(version));
|
|
2681
2619
|
// Commit all the changes to the saved state.
|
|
2682
|
-
|
|
2683
|
-
|
|
2620
|
+
await this.sync();
|
|
2621
|
+
// Delete all caches that are no longer needed.
|
|
2622
|
+
const allCaches = await this.adapter.caches.keys();
|
|
2623
|
+
const usedCaches = new Set(await this.getCacheNames());
|
|
2624
|
+
const cachesToDelete = allCaches.filter(name => !usedCaches.has(name));
|
|
2625
|
+
await Promise.all(cachesToDelete.map(name => this.adapter.caches.delete(name)));
|
|
2626
|
+
}
|
|
2627
|
+
catch (err) {
|
|
2628
|
+
// Oh well? Not much that can be done here. These caches will be removed on the next attempt
|
|
2629
|
+
// or when the SW revs its format version, which happens from time to time.
|
|
2630
|
+
this.debugger.log(err, 'cleanupCaches');
|
|
2631
|
+
}
|
|
2684
2632
|
}
|
|
2685
2633
|
/**
|
|
2686
2634
|
* Delete caches that were used by older versions of `@angular/service-worker` to avoid running
|
|
2687
2635
|
* into storage quota limitations imposed by browsers.
|
|
2688
2636
|
* (Since at this point the SW has claimed all clients, it is safe to remove those caches.)
|
|
2689
2637
|
*/
|
|
2690
|
-
cleanupOldSwCaches() {
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2638
|
+
async cleanupOldSwCaches() {
|
|
2639
|
+
// This is an exceptional case, where we need to interact with caches that would not be
|
|
2640
|
+
// generated by this ServiceWorker (but by old versions of it). Use the native `CacheStorage`
|
|
2641
|
+
// directly.
|
|
2642
|
+
const caches = this.adapter.caches.original;
|
|
2643
|
+
const cacheNames = await caches.keys();
|
|
2644
|
+
const oldSwCacheNames = cacheNames.filter(name => /^ngsw:(?!\/)/.test(name));
|
|
2645
|
+
await Promise.all(oldSwCacheNames.map(name => caches.delete(name)));
|
|
2696
2646
|
}
|
|
2697
2647
|
/**
|
|
2698
2648
|
* Determine if a specific version of the given resource is cached anywhere within the SW,
|
|
@@ -2708,35 +2658,29 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2708
2658
|
// reduction, if a response has already been identified, then pass it through, as no
|
|
2709
2659
|
// future operation could change the response. If no response has been found yet, keep
|
|
2710
2660
|
// checking versions until one is or until all versions have been exhausted.
|
|
2711
|
-
.reduce((prev, version) =>
|
|
2661
|
+
.reduce(async (prev, version) => {
|
|
2712
2662
|
// First, check the previous result. If a non-null result has been found already, just
|
|
2713
2663
|
// return it.
|
|
2714
|
-
if (
|
|
2664
|
+
if (await prev !== null) {
|
|
2715
2665
|
return prev;
|
|
2716
2666
|
}
|
|
2717
2667
|
// No result has been found yet. Try the next `AppVersion`.
|
|
2718
2668
|
return version.lookupResourceWithHash(url, hash);
|
|
2719
|
-
}
|
|
2669
|
+
}, Promise.resolve(null));
|
|
2720
2670
|
}
|
|
2721
|
-
lookupResourceWithoutHash(url) {
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
return version ? version.lookupResourceWithoutHash(url) : null;
|
|
2726
|
-
});
|
|
2671
|
+
async lookupResourceWithoutHash(url) {
|
|
2672
|
+
await this.initialized;
|
|
2673
|
+
const version = this.versions.get(this.latestHash);
|
|
2674
|
+
return version ? version.lookupResourceWithoutHash(url) : null;
|
|
2727
2675
|
}
|
|
2728
|
-
previouslyCachedResources() {
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
return version ? version.previouslyCachedResources() : [];
|
|
2733
|
-
});
|
|
2676
|
+
async previouslyCachedResources() {
|
|
2677
|
+
await this.initialized;
|
|
2678
|
+
const version = this.versions.get(this.latestHash);
|
|
2679
|
+
return version ? version.previouslyCachedResources() : [];
|
|
2734
2680
|
}
|
|
2735
|
-
recentCacheStatus(url) {
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
return version ? version.recentCacheStatus(url) : UpdateCacheStatus.NOT_CACHED;
|
|
2739
|
-
});
|
|
2681
|
+
async recentCacheStatus(url) {
|
|
2682
|
+
const version = this.versions.get(this.latestHash);
|
|
2683
|
+
return version ? version.recentCacheStatus(url) : UpdateCacheStatus.NOT_CACHED;
|
|
2740
2684
|
}
|
|
2741
2685
|
mergeHashWithAppData(manifest, hash) {
|
|
2742
2686
|
return {
|
|
@@ -2744,106 +2688,126 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2744
2688
|
appData: manifest.appData,
|
|
2745
2689
|
};
|
|
2746
2690
|
}
|
|
2747
|
-
notifyClientsAboutUnrecoverableState(appVersion, reason) {
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2691
|
+
async notifyClientsAboutUnrecoverableState(appVersion, reason) {
|
|
2692
|
+
const broken = Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
|
|
2693
|
+
if (broken === undefined) {
|
|
2694
|
+
// This version is no longer in use anyway, so nobody cares.
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const brokenHash = broken[0];
|
|
2698
|
+
const affectedClients = Array.from(this.clientVersionMap.entries())
|
|
2699
|
+
.filter(([clientId, hash]) => hash === brokenHash)
|
|
2700
|
+
.map(([clientId]) => clientId);
|
|
2701
|
+
await Promise.all(affectedClients.map(async (clientId) => {
|
|
2702
|
+
const client = await this.scope.clients.get(clientId);
|
|
2703
|
+
if (client) {
|
|
2704
|
+
client.postMessage({ type: 'UNRECOVERABLE_STATE', reason });
|
|
2705
|
+
}
|
|
2706
|
+
}));
|
|
2707
|
+
}
|
|
2708
|
+
async notifyClientsAboutVersionInstallationFailed(manifest, hash, error) {
|
|
2709
|
+
await this.initialized;
|
|
2710
|
+
const clients = await this.scope.clients.matchAll();
|
|
2711
|
+
await Promise.all(clients.map(async (client) => {
|
|
2712
|
+
// Send a notice.
|
|
2713
|
+
client.postMessage({
|
|
2714
|
+
type: 'VERSION_INSTALLATION_FAILED',
|
|
2715
|
+
version: this.mergeHashWithAppData(manifest, hash),
|
|
2716
|
+
error: errorToString(error),
|
|
2717
|
+
});
|
|
2718
|
+
}));
|
|
2719
|
+
}
|
|
2720
|
+
async notifyClientsAboutVersionDetected(manifest, hash) {
|
|
2721
|
+
await this.initialized;
|
|
2722
|
+
const clients = await this.scope.clients.matchAll();
|
|
2723
|
+
await Promise.all(clients.map(async (client) => {
|
|
2724
|
+
// Firstly, determine which version this client is on.
|
|
2725
|
+
const version = this.clientVersionMap.get(client.id);
|
|
2726
|
+
if (version === undefined) {
|
|
2727
|
+
// Unmapped client - assume it's the latest.
|
|
2752
2728
|
return;
|
|
2753
2729
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2730
|
+
// Send a notice.
|
|
2731
|
+
client.postMessage({ type: 'VERSION_DETECTED', version: this.mergeHashWithAppData(manifest, hash) });
|
|
2732
|
+
}));
|
|
2733
|
+
}
|
|
2734
|
+
async notifyClientsAboutVersionReady(manifest, hash) {
|
|
2735
|
+
await this.initialized;
|
|
2736
|
+
const clients = await this.scope.clients.matchAll();
|
|
2737
|
+
await Promise.all(clients.map(async (client) => {
|
|
2738
|
+
// Firstly, determine which version this client is on.
|
|
2739
|
+
const version = this.clientVersionMap.get(client.id);
|
|
2740
|
+
if (version === undefined) {
|
|
2741
|
+
// Unmapped client - assume it's the latest.
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (version === this.latestHash) {
|
|
2745
|
+
// Client is already on the latest version, no need for a notification.
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
const current = this.versions.get(version);
|
|
2749
|
+
// Send a notice.
|
|
2750
|
+
const notice = {
|
|
2751
|
+
type: 'VERSION_READY',
|
|
2752
|
+
currentVersion: this.mergeHashWithAppData(current.manifest, version),
|
|
2753
|
+
latestVersion: this.mergeHashWithAppData(manifest, hash),
|
|
2754
|
+
};
|
|
2755
|
+
client.postMessage(notice);
|
|
2756
|
+
}));
|
|
2763
2757
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
yield Promise.all(clients.map((client) => __awaiter(this, void 0, void 0, function* () {
|
|
2769
|
-
// Firstly, determine which version this client is on.
|
|
2770
|
-
const version = this.clientVersionMap.get(client.id);
|
|
2771
|
-
if (version === undefined) {
|
|
2772
|
-
// Unmapped client - assume it's the latest.
|
|
2773
|
-
return;
|
|
2774
|
-
}
|
|
2775
|
-
if (version === this.latestHash) {
|
|
2776
|
-
// Client is already on the latest version, no need for a notification.
|
|
2777
|
-
return;
|
|
2778
|
-
}
|
|
2779
|
-
const current = this.versions.get(version);
|
|
2780
|
-
// Send a notice.
|
|
2781
|
-
const notice = {
|
|
2782
|
-
type: 'UPDATE_AVAILABLE',
|
|
2783
|
-
current: this.mergeHashWithAppData(current.manifest, version),
|
|
2784
|
-
available: this.mergeHashWithAppData(next.manifest, this.latestHash),
|
|
2785
|
-
};
|
|
2786
|
-
client.postMessage(notice);
|
|
2787
|
-
})));
|
|
2758
|
+
async broadcast(msg) {
|
|
2759
|
+
const clients = await this.scope.clients.matchAll();
|
|
2760
|
+
clients.forEach(client => {
|
|
2761
|
+
client.postMessage(msg);
|
|
2788
2762
|
});
|
|
2789
2763
|
}
|
|
2790
|
-
|
|
2791
|
-
return
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
}
|
|
2764
|
+
async debugState() {
|
|
2765
|
+
return {
|
|
2766
|
+
state: DriverReadyState[this.state],
|
|
2767
|
+
why: this.stateMessage,
|
|
2768
|
+
latestHash: this.latestHash,
|
|
2769
|
+
lastUpdateCheck: this.lastUpdateCheck,
|
|
2770
|
+
};
|
|
2797
2771
|
}
|
|
2798
|
-
|
|
2799
|
-
|
|
2772
|
+
async debugVersions() {
|
|
2773
|
+
// Build list of versions.
|
|
2774
|
+
return Array.from(this.versions.keys()).map(hash => {
|
|
2775
|
+
const version = this.versions.get(hash);
|
|
2776
|
+
const clients = Array.from(this.clientVersionMap.entries())
|
|
2777
|
+
.filter(([clientId, version]) => version === hash)
|
|
2778
|
+
.map(([clientId, version]) => clientId);
|
|
2800
2779
|
return {
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2780
|
+
hash,
|
|
2781
|
+
manifest: version.manifest,
|
|
2782
|
+
clients,
|
|
2783
|
+
status: '',
|
|
2805
2784
|
};
|
|
2806
2785
|
});
|
|
2807
2786
|
}
|
|
2808
|
-
|
|
2809
|
-
return
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
.filter(([clientId, version]) => version === hash)
|
|
2815
|
-
.map(([clientId, version]) => clientId);
|
|
2816
|
-
return {
|
|
2817
|
-
hash,
|
|
2818
|
-
manifest: version.manifest,
|
|
2819
|
-
clients,
|
|
2820
|
-
status: '',
|
|
2821
|
-
};
|
|
2822
|
-
});
|
|
2823
|
-
});
|
|
2787
|
+
async debugIdleState() {
|
|
2788
|
+
return {
|
|
2789
|
+
queue: this.idle.taskDescriptions,
|
|
2790
|
+
lastTrigger: this.idle.lastTrigger,
|
|
2791
|
+
lastRun: this.idle.lastRun,
|
|
2792
|
+
};
|
|
2824
2793
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
return
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2794
|
+
async safeFetch(req) {
|
|
2795
|
+
try {
|
|
2796
|
+
return await this.scope.fetch(req);
|
|
2797
|
+
}
|
|
2798
|
+
catch (err) {
|
|
2799
|
+
this.debugger.log(err, `Driver.fetch(${req.url})`);
|
|
2800
|
+
return this.adapter.newResponse(null, {
|
|
2801
|
+
status: 504,
|
|
2802
|
+
statusText: 'Gateway Timeout',
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2833
2805
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
catch (err) {
|
|
2840
|
-
this.debugger.log(err, `Driver.fetch(${req.url})`);
|
|
2841
|
-
return this.adapter.newResponse(null, {
|
|
2842
|
-
status: 504,
|
|
2843
|
-
statusText: 'Gateway Timeout',
|
|
2844
|
-
});
|
|
2845
|
-
}
|
|
2846
|
-
});
|
|
2806
|
+
async getCacheNames() {
|
|
2807
|
+
const controlTable = await this.controlTable;
|
|
2808
|
+
const appVersions = Array.from(this.versions.values());
|
|
2809
|
+
const appVersionCacheNames = await Promise.all(appVersions.map(version => version.getCacheNames()));
|
|
2810
|
+
return [controlTable.cacheName].concat(...appVersionCacheNames);
|
|
2847
2811
|
}
|
|
2848
2812
|
}
|
|
2849
2813
|
|
|
@@ -2855,7 +2819,7 @@ ${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }
|
|
|
2855
2819
|
* found in the LICENSE file at https://angular.io/license
|
|
2856
2820
|
*/
|
|
2857
2821
|
const scope = self;
|
|
2858
|
-
const adapter = new Adapter(scope.registration.scope);
|
|
2859
|
-
|
|
2822
|
+
const adapter = new Adapter(scope.registration.scope, self.caches);
|
|
2823
|
+
new Driver(scope, adapter, new CacheDatabase(adapter));
|
|
2860
2824
|
|
|
2861
|
-
}()
|
|
2825
|
+
})();
|