@angular/service-worker
Version:
Angular - service worker tooling!
1,015 lines (1,007 loc) • 137 kB
JavaScript
(function () {
'use strict';
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Adapts the service worker to its runtime environment.
*
* Mostly, this is used to mock out identifiers which are otherwise read
* from the global scope.
*/
class Adapter {
/**
* Wrapper around the `Request` constructor.
*/
newRequest(input, init) {
return new Request(input, init);
}
/**
* Wrapper around the `Response` constructor.
*/
newResponse(body, init) { return new Response(body, init); }
/**
* Wrapper around the `Headers` constructor.
*/
newHeaders(headers) { return new Headers(headers); }
/**
* Test if a given object is an instance of `Client`.
*/
isClient(source) { return (source instanceof Client); }
/**
* Read the current UNIX time in milliseconds.
*/
get time() { return Date.now(); }
/**
* Extract the pathname of a URL.
*/
parseUrl(url, relativeTo) {
const parsed = new URL(url, relativeTo);
return { origin: parsed.origin, path: parsed.pathname };
}
/**
* Wait for a given amount of time before completing a Promise.
*/
timeout(ms) {
return new Promise(resolve => { setTimeout(() => resolve(), ms); });
}
}
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* An error returned in rejected promises if the given key is not found in the table.
*/
class NotFound {
constructor(table, key) {
this.table = table;
this.key = key;
}
}
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* An implementation of a `Database` that uses the `CacheStorage` API to serialize
* state within mock `Response` objects.
*/
class CacheDatabase {
constructor(scope, adapter) {
this.scope = scope;
this.adapter = adapter;
this.tables = new Map();
}
'delete'(name) {
if (this.tables.has(name)) {
this.tables.delete(name);
}
return this.scope.caches.delete(`ngsw:db:${name}`);
}
list() {
return this.scope.caches.keys().then(keys => keys.filter(key => key.startsWith('ngsw:db:')));
}
open(name) {
if (!this.tables.has(name)) {
const table = this.scope.caches.open(`ngsw:db:${name}`)
.then(cache => new CacheTable(name, cache, this.adapter));
this.tables.set(name, table);
}
return this.tables.get(name);
}
}
/**
* A `Table` backed by a `Cache`.
*/
class CacheTable {
constructor(table, cache, adapter) {
this.table = table;
this.cache = cache;
this.adapter = adapter;
}
request(key) { return this.adapter.newRequest('/' + key); }
'delete'(key) { return this.cache.delete(this.request(key)); }
keys() {
return this.cache.keys().then(requests => requests.map(req => req.url.substr(1)));
}
read(key) {
return this.cache.match(this.request(key)).then(res => {
if (res === undefined) {
return Promise.reject(new NotFound(this.table, key));
}
return res.json();
});
}
write(key, value) {
return this.cache.put(this.request(key), this.adapter.newResponse(JSON.stringify(value)));
}
}
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
var UpdateCacheStatus;
(function (UpdateCacheStatus) {
UpdateCacheStatus[UpdateCacheStatus["NOT_CACHED"] = 0] = "NOT_CACHED";
UpdateCacheStatus[UpdateCacheStatus["CACHED_BUT_UNUSED"] = 1] = "CACHED_BUT_UNUSED";
UpdateCacheStatus[UpdateCacheStatus["CACHED"] = 2] = "CACHED";
})(UpdateCacheStatus || (UpdateCacheStatus = {}));
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
class SwCriticalError extends Error {
constructor() {
super(...arguments);
this.isCritical = true;
}
}
function errorToString(error) {
if (error instanceof Error) {
return `${error.message}\n${error.stack}`;
}
else {
return `${error}`;
}
}
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Compute the SHA1 of the given string
*
* see http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
*
* WARNING: this function has not been designed not tested with security in mind.
* DO NOT USE IT IN A SECURITY SENSITIVE CONTEXT.
*
* Borrowed from @angular/compiler/src/i18n/digest.ts
*/
function sha1(str) {
const utf8 = str;
const words32 = stringToWords32(utf8, Endian.Big);
return _sha1(words32, utf8.length * 8);
}
function sha1Binary(buffer) {
const words32 = arrayBufferToWords32(buffer, Endian.Big);
return _sha1(words32, buffer.byteLength * 8);
}
function _sha1(words32, len) {
const w = new Array(80);
let [a, b, c, d, e] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
words32[len >> 5] |= 0x80 << (24 - len % 32);
words32[((len + 64 >> 9) << 4) + 15] = len;
for (let i = 0; i < words32.length; i += 16) {
const [h0, h1, h2, h3, h4] = [a, b, c, d, e];
for (let j = 0; j < 80; j++) {
if (j < 16) {
w[j] = words32[i + j];
}
else {
w[j] = rol32(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
}
const [f, k] = fk(j, b, c, d);
const temp = [rol32(a, 5), f, e, k, w[j]].reduce(add32);
[e, d, c, b, a] = [d, c, rol32(b, 30), a, temp];
}
[a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)];
}
return byteStringToHexString(words32ToByteString([a, b, c, d, e]));
}
function add32(a, b) {
return add32to64(a, b)[1];
}
function add32to64(a, b) {
const low = (a & 0xffff) + (b & 0xffff);
const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
return [high >>> 16, (high << 16) | (low & 0xffff)];
}
// Rotate a 32b number left `count` position
function rol32(a, count) {
return (a << count) | (a >>> (32 - count));
}
var Endian;
(function (Endian) {
Endian[Endian["Little"] = 0] = "Little";
Endian[Endian["Big"] = 1] = "Big";
})(Endian || (Endian = {}));
function fk(index, b, c, d) {
if (index < 20) {
return [(b & c) | (~b & d), 0x5a827999];
}
if (index < 40) {
return [b ^ c ^ d, 0x6ed9eba1];
}
if (index < 60) {
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
}
return [b ^ c ^ d, 0xca62c1d6];
}
function stringToWords32(str, endian) {
const words32 = Array((str.length + 3) >>> 2);
for (let i = 0; i < words32.length; i++) {
words32[i] = wordAt(str, i * 4, endian);
}
return words32;
}
function arrayBufferToWords32(buffer, endian) {
const words32 = Array((buffer.byteLength + 3) >>> 2);
const view = new Uint8Array(buffer);
for (let i = 0; i < words32.length; i++) {
words32[i] = wordAt(view, i * 4, endian);
}
return words32;
}
function byteAt(str, index) {
if (typeof str === 'string') {
return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
}
else {
return index >= str.byteLength ? 0 : str[index] & 0xff;
}
}
function wordAt(str, index, endian) {
let word = 0;
if (endian === Endian.Big) {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << (24 - 8 * i);
}
}
else {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << 8 * i;
}
}
return word;
}
function words32ToByteString(words32) {
return words32.reduce((str, word) => str + word32ToByteString(word), '');
}
function word32ToByteString(word) {
let str = '';
for (let i = 0; i < 4; i++) {
str += String.fromCharCode((word >>> 8 * (3 - i)) & 0xff);
}
return str;
}
function byteStringToHexString(str) {
let hex = '';
for (let i = 0; i < str.length; i++) {
const b = byteAt(str, i);
hex += (b >>> 4).toString(16) + (b & 0x0f).toString(16);
}
return hex.toLowerCase();
}
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/**
* A group of assets that are cached in a `Cache` and managed by a given policy.
*
* Concrete classes derive from this base and specify the exact caching policy.
*/
class AssetGroup {
constructor(scope, adapter, idle, config, hashes, db, prefix) {
this.scope = scope;
this.adapter = adapter;
this.idle = idle;
this.config = config;
this.hashes = hashes;
this.db = db;
this.prefix = prefix;
/**
* A deduplication cache, to make sure the SW never makes two network requests
* for the same resource at once. Managed by `fetchAndCacheOnce`.
*/
this.inFlightRequests = new Map();
/**
* Regular expression patterns.
*/
this.patterns = [];
this.name = config.name;
// Patterns in the config are regular expressions disguised as strings. Breathe life into them.
this.patterns = this.config.patterns.map(pattern => new RegExp(pattern));
// This is the primary cache, which holds all of the cached requests for this group. If a
// resource
// isn't in this cache, it hasn't been fetched yet.
this.cache = this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
// This is the metadata table, which holds specific information for each cached URL, such as
// the timestamp of when it was added to the cache.
this.metadata = this.db.open(`${this.prefix}:${this.config.name}:meta`);
// Determine the origin from the registration scope. This is used to differentiate between
// relative and absolute URLs.
this.origin =
this.adapter.parseUrl(this.scope.registration.scope, this.scope.registration.scope).origin;
}
cacheStatus(url) {
return __awaiter(this, void 0, void 0, function* () {
const cache = yield this.cache;
const meta = yield this.metadata;
const res = yield cache.match(this.adapter.newRequest(url));
if (res === undefined) {
return UpdateCacheStatus.NOT_CACHED;
}
try {
const data = yield meta.read(url);
if (!data.used) {
return UpdateCacheStatus.CACHED_BUT_UNUSED;
}
}
catch (_) {
// Error on the side of safety and assume cached.
}
return UpdateCacheStatus.CACHED;
});
}
/**
* Clean up all the cached data for this group.
*/
cleanup() {
return __awaiter(this, void 0, void 0, function* () {
yield this.scope.caches.delete(`${this.prefix}:${this.config.name}:cache`);
yield this.db.delete(`${this.prefix}:${this.config.name}:meta`);
});
}
/**
* Process a request for a given resource and return it, or return null if it's not available.
*/
handleFetch(req, ctx) {
return __awaiter(this, void 0, void 0, function* () {
const url = this.getConfigUrl(req.url);
// Either the request matches one of the known resource URLs, one of the patterns for
// dynamically matched URLs, or neither. Determine which is the case for this request in
// order to decide how to handle it.
if (this.config.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url))) {
// This URL matches a known resource. Either it's been cached already or it's missing, in
// which case it needs to be loaded from the network.
// Open the cache to check whether this resource is present.
const cache = yield this.cache;
// Look for a cached response. If one exists, it can be used to resolve the fetch
// operation.
const cachedResponse = yield cache.match(req);
if (cachedResponse !== undefined) {
// A response has already been cached (which presumably matches the hash for this
// resource). Check whether it's safe to serve this resource from cache.
if (this.hashes.has(url)) {
// This resource has a hash, and thus is versioned by the manifest. It's safe to return
// the response.
return cachedResponse;
}
else {
// This resource has no hash, and yet exists in the cache. Check how old this request is
// to make sure it's still usable.
if (yield this.needToRevalidate(req, cachedResponse)) {
this.idle.schedule(`revalidate(${this.prefix}, ${this.config.name}): ${req.url}`, () => __awaiter(this, void 0, void 0, function* () { yield this.fetchAndCacheOnce(req); }));
}
// In either case (revalidation or not), the cached response must be good.
return cachedResponse;
}
}
// No already-cached response exists, so attempt a fetch/cache operation. The original request
// may specify things like credential inclusion, but for assets these are not honored in order
// to avoid issues with opaque responses. The SW requests the data itself.
const res = yield this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
// If this is successful, the response needs to be cloned as it might be used to respond to
// multiple fetch operations at the same time.
return res.clone();
}
else {
return null;
}
});
}
getConfigUrl(url) {
// If the URL is relative to the SW's own origin, then only consider the path relative to
// the domain root. Determine this by checking the URL's origin against the SW's.
const parsed = this.adapter.parseUrl(url, this.scope.registration.scope);
if (parsed.origin === this.origin) {
// The URL is relative to the SW's origin domain.
return parsed.path;
}
else {
return url;
}
}
/**
* Some resources are cached without a hash, meaning that their expiration is controlled
* by HTTP caching headers. Check whether the given request/response pair is still valid
* per the caching headers.
*/
needToRevalidate(req, res) {
return __awaiter(this, void 0, void 0, function* () {
// Three different strategies apply here:
// 1) The request has a Cache-Control header, and thus expiration needs to be based on its age.
// 2) The request has an Expires header, and expiration is based on the current timestamp.
// 3) The request has no applicable caching headers, and must be revalidated.
if (res.headers.has('Cache-Control')) {
// Figure out if there is a max-age directive in the Cache-Control header.
const cacheControl = res.headers.get('Cache-Control');
const cacheDirectives = cacheControl
// Directives are comma-separated within the Cache-Control header value.
.split(',')
// Make sure each directive doesn't have extraneous whitespace.
.map(v => v.trim())
// Some directives have values (like maxage and s-maxage)
.map(v => v.split('='));
// Lowercase all the directive names.
cacheDirectives.forEach(v => v[0] = v[0].toLowerCase());
// Find the max-age directive, if one exists.
const maxAgeDirective = cacheDirectives.find(v => v[0] === 'max-age');
const cacheAge = maxAgeDirective ? maxAgeDirective[1] : undefined;
if (!cacheAge) {
// No usable TTL defined. Must assume that the response is stale.
return true;
}
try {
const maxAge = 1000 * parseInt(cacheAge);
// Determine the origin time of this request. If the SW has metadata on the request (which
// it
// should), it will have the time the request was added to the cache. If it doesn't for some
// reason, the request may have a Date header which will serve the same purpose.
let ts;
try {
// Check the metadata table. If a timestamp is there, use it.
const metaTable = yield this.metadata;
ts = (yield metaTable.read(req.url)).ts;
}
catch (e) {
// Otherwise, look for a Date header.
const date = res.headers.get('Date');
if (date === null) {
// Unable to determine when this response was created. Assume that it's stale, and
// revalidate it.
return true;
}
ts = Date.parse(date);
}
const age = this.adapter.time - ts;
return age < 0 || age > maxAge;
}
catch (e) {
// Assume stale.
return true;
}
}
else if (res.headers.has('Expires')) {
// Determine if the expiration time has passed.
const expiresStr = res.headers.get('Expires');
try {
// The request needs to be revalidated if the current time is later than the expiration
// time, if it parses correctly.
return this.adapter.time > Date.parse(expiresStr);
}
catch (e) {
// The expiration date failed to parse, so revalidate as a precaution.
return true;
}
}
else {
// No way to evaluate staleness, so assume the response is already stale.
return true;
}
});
}
/**
* Fetch the complete state of a cached resource, or return null if it's not found.
*/
fetchFromCacheOnly(url) {
return __awaiter(this, void 0, void 0, function* () {
const cache = yield this.cache;
const metaTable = yield this.metadata;
// Lookup the response in the cache.
const response = yield cache.match(this.adapter.newRequest(url));
if (response === undefined) {
// It's not found, return null.
return null;
}
// Next, lookup the cached metadata.
let metadata = undefined;
try {
metadata = yield metaTable.read(url);
}
catch (e) {
// Do nothing, not found. This shouldn't happen, but it can be handled.
}
// Return both the response and any available metadata.
return { response, metadata };
});
}
/**
* Lookup all resources currently stored in the cache which have no associated hash.
*/
unhashedResources() {
return __awaiter(this, void 0, void 0, function* () {
const cache = yield this.cache;
// Start with the set of all cached URLs.
return (yield cache.keys())
.map(request => request.url)
// Exclude the URLs which have hashes.
.filter(url => !this.hashes.has(url));
});
}
/**
* Fetch the given resource from the network, and cache it if able.
*/
fetchAndCacheOnce(req, used = true) {
return __awaiter(this, void 0, void 0, function* () {
// The `inFlightRequests` map holds information about which caching operations are currently
// underway for known resources. If this request appears there, another "thread" is already
// in the process of caching it, and this work should not be duplicated.
if (this.inFlightRequests.has(req.url)) {
// There is a caching operation already in progress for this request. Wait for it to
// complete, and hopefully it will have yielded a useful response.
return this.inFlightRequests.get(req.url);
}
// No other caching operation is being attempted for this resource, so it will be owned here.
// Go to the network and get the correct version.
const fetchOp = this.fetchFromNetwork(req);
// Save this operation in `inFlightRequests` so any other "thread" attempting to cache it
// will block on this chain instead of duplicating effort.
this.inFlightRequests.set(req.url, fetchOp);
// Make sure this attempt is cleaned up properly on failure.
try {
// Wait for a response. If this fails, the request will remain in `inFlightRequests`
// indefinitely.
const res = yield fetchOp;
// It's very important that only successful responses are cached. Unsuccessful responses
// should never be cached as this can completely break applications.
if (!res.ok) {
throw new Error(`Response not Ok (fetchAndCacheOnce): request for ${req.url} returned response ${res.status} ${res.statusText}`);
}
try {
// This response is safe to cache (as long as it's cloned). Wait until the cache operation
// is complete.
const cache = yield this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
yield cache.put(req, res.clone());
// If the request is not hashed, update its metadata, especially the timestamp. This is
// needed for future determination of whether this cached response is stale or not.
if (!this.hashes.has(req.url)) {
// Metadata is tracked for requests that are unhashed.
const meta = { ts: this.adapter.time, used };
const metaTable = yield this.metadata;
yield metaTable.write(req.url, meta);
}
return res;
}
catch (err) {
// Among other cases, this can happen when the user clears all data through the DevTools,
// but the SW is still running and serving another tab. In that case, trying to write to the
// caches throws an `Entry was not found` error.
// If this happens the SW can no longer work correctly. This situation is unrecoverable.
throw new SwCriticalError(`Failed to update the caches for request to '${req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
}
}
finally {
// Finally, it can be removed from `inFlightRequests`. This might result in a double-remove
// if some other chain was already making this request too, but that won't hurt anything.
this.inFlightRequests.delete(req.url);
}
});
}
fetchFromNetwork(req, redirectLimit = 3) {
return __awaiter(this, void 0, void 0, function* () {
// Make a cache-busted request for the resource.
const res = yield this.cacheBustedFetchFromNetwork(req);
// Check for redirected responses, and follow the redirects.
if (res['redirected'] && !!res.url) {
// If the redirect limit is exhausted, fail with an error.
if (redirectLimit === 0) {
throw new SwCriticalError(`Response hit redirect limit (fetchFromNetwork): request redirected too many times, next is ${res.url}`);
}
// Unwrap the redirect directly.
return this.fetchFromNetwork(this.adapter.newRequest(res.url), redirectLimit - 1);
}
return res;
});
}
/**
* Load a particular asset from the network, accounting for hash validation.
*/
cacheBustedFetchFromNetwork(req) {
return __awaiter(this, void 0, void 0, function* () {
const url = this.getConfigUrl(req.url);
// If a hash is available for this resource, then compare the fetched version with the
// canonical hash. Otherwise, the network version will have to be trusted.
if (this.hashes.has(url)) {
// It turns out this resource does have a hash. Look it up. Unless the fetched version
// matches this hash, it's invalid and the whole manifest may need to be thrown out.
const canonicalHash = this.hashes.get(url);
// Ideally, the resource would be requested with cache-busting to guarantee the SW gets
// the freshest version. However, doing this would eliminate any chance of the response
// being in the HTTP cache. Given that the browser has recently actively loaded the page,
// it's likely that many of the responses the SW needs to cache are in the HTTP cache and
// are fresh enough to use. In the future, this could be done by setting cacheMode to
// *only* check the browser cache for a cached version of the resource, when cacheMode is
// fully supported. For now, the resource is fetched directly, without cache-busting, and
// if the hash test fails a cache-busted request is tried before concluding that the
// resource isn't correct. This gives the benefit of acceleration via the HTTP cache
// without the risk of stale data, at the expense of a duplicate request in the event of
// a stale response.
// Fetch the resource from the network (possibly hitting the HTTP cache).
const networkResult = yield this.safeFetch(req);
// Decide whether a cache-busted request is necessary. It might be for two independent
// reasons: either the non-cache-busted request failed (hopefully transiently) or if the
// hash of the content retrieved does not match the canonical hash from the manifest. It's
// only valid to access the content of the first response if the request was successful.
let makeCacheBustedRequest = networkResult.ok;
if (makeCacheBustedRequest) {
// The request was successful. A cache-busted request is only necessary if the hashes
// don't match. Compare them, making sure to clone the response so it can be used later
// if it proves to be valid.
const fetchedHash = sha1Binary(yield networkResult.clone().arrayBuffer());
makeCacheBustedRequest = (fetchedHash !== canonicalHash);
}
// Make a cache busted request to the network, if necessary.
if (makeCacheBustedRequest) {
// Hash failure, the version that was retrieved under the default URL did not have the
// hash expected. This could be because the HTTP cache got in the way and returned stale
// data, or because the version on the server really doesn't match. A cache-busting
// request will differentiate these two situations.
// TODO: handle case where the URL has parameters already (unlikely for assets).
const cacheBustReq = this.adapter.newRequest(this.cacheBust(req.url));
const cacheBustedResult = yield this.safeFetch(cacheBustReq);
// If the response was unsuccessful, there's nothing more that can be done.
if (!cacheBustedResult.ok) {
throw new SwCriticalError(`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${req.url} returned response ${cacheBustedResult.status} ${cacheBustedResult.statusText}`);
}
// Hash the contents.
const cacheBustedHash = sha1Binary(yield cacheBustedResult.clone().arrayBuffer());
// If the cache-busted version doesn't match, then the manifest is not an accurate
// representation of the server's current set of files, and the SW should give up.
if (canonicalHash !== cacheBustedHash) {
throw new SwCriticalError(`Hash mismatch (cacheBustedFetchFromNetwork): ${req.url}: expected ${canonicalHash}, got ${cacheBustedHash} (after cache busting)`);
}
// If it does match, then use the cache-busted result.
return cacheBustedResult;
}
// Excellent, the version from the network matched on the first try, with no need for
// cache-busting. Use it.
return networkResult;
}
else {
// This URL doesn't exist in our hash database, so it must be requested directly.
return this.safeFetch(req);
}
});
}
/**
* Possibly update a resource, if it's expired and needs to be updated. A no-op otherwise.
*/
maybeUpdate(updateFrom, req, cache) {
return __awaiter(this, void 0, void 0, function* () {
const url = this.getConfigUrl(req.url);
const meta = yield this.metadata;
// Check if this resource is hashed and already exists in the cache of a prior version.
if (this.hashes.has(url)) {
const hash = this.hashes.get(url);
// Check the caches of prior versions, using the hash to ensure the correct version of
// the resource is loaded.
const res = yield updateFrom.lookupResourceWithHash(url, hash);
// If a previously cached version was available, copy it over to this cache.
if (res !== null) {
// Copy to this cache.
yield cache.put(req, res);
yield meta.write(req.url, { ts: this.adapter.time, used: false });
// No need to do anything further with this resource, it's now cached properly.
return true;
}
}
// No up-to-date version of this resource could be found.
return false;
});
}
/**
* Construct a cache-busting URL for a given URL.
*/
cacheBust(url) {
return url + (url.indexOf('?') === -1 ? '?' : '&') + 'ngsw-cache-bust=' + Math.random();
}
safeFetch(req) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield this.scope.fetch(req);
}
catch (err) {
return this.adapter.newResponse('', {
status: 504,
statusText: 'Gateway Timeout',
});
}
});
}
}
/**
* An `AssetGroup` that prefetches all of its resources during initialization.
*/
class PrefetchAssetGroup extends AssetGroup {
initializeFully(updateFrom) {
return __awaiter(this, void 0, void 0, function* () {
// Open the cache which actually holds requests.
const cache = yield this.cache;
// Cache all known resources serially. As this reduce proceeds, each Promise waits
// on the last before starting the fetch/cache operation for the next request. Any
// errors cause fall-through to the final Promise which rejects.
yield this.config.urls.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
// Wait on all previous operations to complete.
yield previous;
// Construct the Request for this url.
const req = this.adapter.newRequest(url);
// First, check the cache to see if there is already a copy of this resource.
const alreadyCached = (yield cache.match(req)) !== undefined;
// If the resource is in the cache already, it can be skipped.
if (alreadyCached) {
return;
}
// If an update source is available.
if (updateFrom !== undefined && (yield this.maybeUpdate(updateFrom, req, cache))) {
return;
}
// Otherwise, go to the network and hopefully cache the response (if successful).
yield this.fetchAndCacheOnce(req, false);
}), Promise.resolve());
// Handle updating of unknown (unhashed) resources. This is only possible if there's
// a source to update from.
if (updateFrom !== undefined) {
const metaTable = yield this.metadata;
// Select all of the previously cached resources. These are cached unhashed resources
// from previous versions of the app, in any asset group.
yield (yield updateFrom.previouslyCachedResources())
// First, narrow down the set of resources to those which are handled by this group.
// Either it's a known URL, or it matches a given pattern.
.filter(url => this.config.urls.some(cacheUrl => cacheUrl === url) ||
this.patterns.some(pattern => pattern.test(url)))
// Finally, process each resource in turn.
.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
yield previous;
const req = this.adapter.newRequest(url);
// It's possible that the resource in question is already cached. If so,
// continue to the next one.
const alreadyCached = ((yield cache.match(req)) !== undefined);
if (alreadyCached) {
return;
}
// Get the most recent old version of the resource.
const res = yield updateFrom.lookupResourceWithoutHash(url);
if (res === null || res.metadata === undefined) {
// Unexpected, but not harmful.
return;
}
// Write it into the cache. It may already be expired, but it can still serve
// traffic until it's updated (stale-while-revalidate approach).
yield cache.put(req, res.response);
yield metaTable.write(url, Object.assign({}, res.metadata, { used: false }));
}), Promise.resolve());
}
});
}
}
class LazyAssetGroup extends AssetGroup {
initializeFully(updateFrom) {
return __awaiter(this, void 0, void 0, function* () {
// No action necessary if no update source is available - resources managed in this group
// are all lazily loaded, so there's nothing to initialize.
if (updateFrom === undefined) {
return;
}
// Open the cache which actually holds requests.
const cache = yield this.cache;
// Loop through the listed resources, caching any which are available.
yield this.config.urls.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
// Wait on all previous operations to complete.
yield previous;
// Construct the Request for this url.
const req = this.adapter.newRequest(url);
// First, check the cache to see if there is already a copy of this resource.
const alreadyCached = (yield cache.match(req)) !== undefined;
// If the resource is in the cache already, it can be skipped.
if (alreadyCached) {
return;
}
const updated = yield this.maybeUpdate(updateFrom, req, cache);
if (this.config.updateMode === 'prefetch' && !updated) {
// If the resource was not updated, either it was not cached before or
// the previously cached version didn't match the updated hash. In that
// case, prefetch update mode dictates that the resource will be updated,
// except if it was not previously utilized. Check the status of the
// cached resource to see.
const cacheStatus = yield updateFrom.recentCacheStatus(url);
// If the resource is not cached, or was cached but unused, then it will be
// loaded lazily.
if (cacheStatus !== UpdateCacheStatus.CACHED) {
return;
}
// Update from the network.
yield this.fetchAndCacheOnce(req, false);
}
}), Promise.resolve());
});
}
}
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/**
* Manages an instance of `LruState` and moves URLs to the head of the
* chain when requested.
*/
class LruList {
constructor(state) {
if (state === undefined) {
state = {
head: null,
tail: null,
map: {},
count: 0,
};
}
this.state = state;
}
/**
* The current count of URLs in the list.
*/
get size() { return this.state.count; }
/**
* Remove the tail.
*/
pop() {
// If there is no tail, return null.
if (this.state.tail === null) {
return null;
}
const url = this.state.tail;
this.remove(url);
// This URL has been successfully evicted.
return url;
}
remove(url) {
const node = this.state.map[url];
if (node === undefined) {
return false;
}
// Special case if removing the current head.
if (this.state.head === url) {
// The node is the current head. Special case the removal.
if (node.next === null) {
// This is the only node. Reset the cache to be empty.
this.state.head = null;
this.state.tail = null;
this.state.map = {};
this.state.count = 0;
return true;
}
// There is at least one other node. Make the next node the new head.
const next = this.state.map[node.next];
next.previous = null;
this.state.head = next.url;
node.next = null;
delete this.state.map[url];
this.state.count--;
return true;
}
// The node is not the head, so it has a previous. It may or may not be the tail.
// If it is not, then it has a next. First, grab the previous node.
const previous = this.state.map[node.previous];
// Fix the forward pointer to skip over node and go directly to node.next.
previous.next = node.next;
// node.next may or may not be set. If it is, fix the back pointer to skip over node.
// If it's not set, then this node happened to be the tail, and the tail needs to be
// updated to point to the previous node (removing the tail).
if (node.next !== null) {
// There is a next node, fix its back pointer to skip this node.
this.state.map[node.next].previous = node.previous;
}
else {
// There is no next node - the accessed node must be the tail. Move the tail pointer.
this.state.tail = node.previous;
}
node.next = null;
node.previous = null;
delete this.state.map[url];
// Count the removal.
this.state.count--;
return true;
}
accessed(url) {
// When a URL is accessed, its node needs to be moved to the head of the chain.
// This is accomplished in two steps:
//
// 1) remove the node from its position within the chain.
// 2) insert the node as the new head.
//
// Sometimes, a URL is accessed which has not been seen before. In this case, step 1 can
// be skipped completely (which will grow the chain by one). Of course, if the node is
// already the head, this whole operation can be skipped.
if (this.state.head === url) {
// The URL is already in the head position, accessing it is a no-op.
return;
}
// Look up the node in the map, and construct a new entry if it's
const node = this.state.map[url] || { url, next: null, previous: null };
// Step 1: remove the node from its position within the chain, if it is in the chain.
if (this.state.map[url] !== undefined) {
this.remove(url);
}
// Step 2: insert the node at the head of the chain.
// First, check if there's an existing head node. If there is, it has previous: null.
// Its previous pointer should be set to the node we're inserting.
if (this.state.head !== null) {
this.state.map[this.state.head].previous = url;
}
// The next pointer of the node being inserted gets set to the old head, before the head
// pointer is updated to this node.
node.next = this.state.head;
// The new head is the new node.
this.state.head = url;
// If there is no tail, then this is the first node, and is both the head and the tail.
if (this.state.tail === null) {
this.state.tail = url;
}
// Set the node in the map of nodes (if the URL has been seen before, this is a no-op)
// and count the insertion.
this.state.map[url] = node;
this.state.count++;
}
}
/**
* A group of cached resources determined by a set of URL patterns which follow a LRU policy
* for caching.
*/
class DataGroup {
constructor(scope, adapter, config, db, prefix) {
this.scope = scope;
this.adapter = adapter;
this.config = config;
this.db = db;
this.prefix = prefix;
/**
* Tracks the LRU state of resources in this cache.
*/
this._lru = null;
this.patterns = this.config.patterns.map(pattern => new RegExp(pattern));
this.cache = this.scope.caches.open(`${this.prefix}:dynamic:${this.config.name}:cache`);
this.lruTable = this.db.open(`${this