|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- /*
- Copyright 2017 Google Inc.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
- import {cacheNames} from 'workbox-core/_private/cacheNames.mjs';
- import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
- import {fetchWrapper} from 'workbox-core/_private/fetchWrapper.mjs';
- import {cacheWrapper} from 'workbox-core/_private/cacheWrapper.mjs';
- import {assert} from 'workbox-core/_private/assert.mjs';
- import PrecacheEntry from '../models/PrecacheEntry.mjs';
- import PrecachedDetailsModel from '../models/PrecachedDetailsModel.mjs';
- import showWarningsIfNeeded from '../utils/showWarningsIfNeeded.mjs';
- import printInstallDetails from '../utils/printInstallDetails.mjs';
- import printCleanupDetails from '../utils/printCleanupDetails.mjs';
- import cleanRedirect from '../utils/cleanRedirect.mjs';
- import '../_version.mjs';
-
- /**
- * Performs efficient precaching of assets.
- *
- * @memberof workbox.precaching
- */
- class PrecacheController {
- /**
- * Create a new PrecacheController.
- *
- * @param {string} cacheName
- */
- constructor(cacheName) {
- this._cacheName = cacheNames.getPrecacheName(cacheName);
- this._entriesToCacheMap = new Map();
- this._precacheDetailsModel = new PrecachedDetailsModel(this._cacheName);
- }
-
- /**
- * This method will add items to the precache list, removing duplicates
- * and ensuring the information is valid.
- *
- * @param {
- * Array<module:workbox-precaching.PrecacheController.PrecacheEntry|string>
- * } entries Array of entries to
- * precache.
- */
- addToCacheList(entries) {
- if (process.env.NODE_ENV !== 'production') {
- assert.isArray(entries, {
- moduleName: 'workbox-precaching',
- className: 'PrecacheController',
- funcName: 'addToCacheList',
- paramName: 'entries',
- });
- }
-
- entries.map((userEntry) => {
- this._addEntryToCacheList(
- this._parseEntry(userEntry)
- );
- });
- }
-
- /**
- * This method returns a precache entry.
- *
- * @private
- * @param {string|Object} input
- * @return {PrecacheEntry}
- */
- _parseEntry(input) {
- switch (typeof input) {
- case 'string': {
- if (process.env.NODE_ENV !== 'production') {
- if (input.length === 0) {
- throw new WorkboxError(
- 'add-to-cache-list-unexpected-type', {
- entry: input,
- }
- );
- }
- }
-
- return new PrecacheEntry(input, input, input);
- }
- case 'object': {
- if (process.env.NODE_ENV !== 'production') {
- if (!input || !input.url) {
- throw new WorkboxError(
- 'add-to-cache-list-unexpected-type', {
- entry: input,
- }
- );
- }
- }
-
- return new PrecacheEntry(
- input, input.url, input.revision || input.url, !!input.revision);
- }
- default:
- throw new WorkboxError('add-to-cache-list-unexpected-type', {
- entry: input,
- });
- }
- }
-
- /**
- * Adds an entry to the precache list, accounting for possible duplicates.
- *
- * @private
- * @param {PrecacheEntry} entryToAdd
- */
- _addEntryToCacheList(entryToAdd) {
- // Check if the entry is already part of the map
- const existingEntry = this._entriesToCacheMap.get(entryToAdd._entryId);
- if (!existingEntry) {
- this._entriesToCacheMap.set(entryToAdd._entryId, entryToAdd);
- return;
- }
-
- // Duplicates are fine, but make sure the revision information
- // is the same.
- if (existingEntry._revision !== entryToAdd._revision) {
- throw new WorkboxError('add-to-cache-list-conflicting-entries', {
- firstEntry: existingEntry._originalInput,
- secondEntry: entryToAdd._originalInput,
- });
- }
- }
-
- /**
- * Call this method from a service work install event to start
- * precaching assets.
- *
- * @param {Object} options
- * @param {boolean} [options.suppressWarnings] Suppress warning messages.
- * @param {Event} [options.event] The install event (if needed).
- * @param {Array<Object>} [options.plugins] Plugins to be used for fetching
- * and caching during install.
- * @return {Promise<workbox.precaching.InstallResult>}
- */
- async install({suppressWarnings = false, event, plugins} = {}) {
- if (process.env.NODE_ENV !== 'production') {
- if (suppressWarnings !== true) {
- showWarningsIfNeeded(this._entriesToCacheMap);
- }
-
- if (plugins) {
- assert.isArray(plugins, {
- moduleName: 'workbox-precaching',
- className: 'PrecacheController',
- funcName: 'install',
- paramName: 'plugins',
- });
- }
- }
-
- // Empty the temporary cache.
- // NOTE: We remove all entries instead of deleting the cache as the cache
- // may be marked for deletion but still exist until a later stage
- // resulting in unexpected behavior of being deletect when all references
- // are dropped.
- // https://github.com/GoogleChrome/workbox/issues/1368
- const tempCache = await caches.open(this._getTempCacheName());
- const requests = await tempCache.keys();
- await Promise.all(requests.map((request) => {
- return tempCache.delete(request);
- }));
-
- const entriesToPrecache = [];
- const entriesAlreadyPrecached = [];
-
- for (const precacheEntry of this._entriesToCacheMap.values()) {
- if (await this._precacheDetailsModel._isEntryCached(
- this._cacheName, precacheEntry)) {
- entriesAlreadyPrecached.push(precacheEntry);
- } else {
- entriesToPrecache.push(precacheEntry);
- }
- }
-
- // Wait for all requests to be cached.
- await Promise.all(entriesToPrecache.map((precacheEntry) => {
- return this._cacheEntryInTemp({event, plugins, precacheEntry});
- }));
-
- if (process.env.NODE_ENV !== 'production') {
- printInstallDetails(entriesToPrecache, entriesAlreadyPrecached);
- }
-
- return {
- updatedEntries: entriesToPrecache,
- notUpdatedEntries: entriesAlreadyPrecached,
- };
- }
-
- /**
- * Takes the current set of temporary files and moves them to the final
- * cache, deleting the temporary cache once copying is complete.
- *
- * @param {Object} options
- * @param {Array<Object>} options.plugins Plugins to be used for fetching
- * and caching during install.
- * @return {
- * Promise<workbox.precaching.CleanupResult>}
- * Resolves with an object containing details of the deleted cache requests
- * and precache revision details.
- */
- async activate(options = {}) {
- const tempCache = await caches.open(this._getTempCacheName());
-
- const requests = await tempCache.keys();
- // Process each request/response one at a time, deleting the temporary entry
- // when done, to help avoid triggering quota errors.
- for (const request of requests) {
- const response = await tempCache.match(request);
- await cacheWrapper.put({
- cacheName: this._cacheName,
- request,
- response,
- plugins: options.plugins,
- });
- await tempCache.delete(request);
- }
-
- return this._cleanup();
- }
-
- /**
- * Returns the name of the temporary cache.
- *
- * @return {string}
- *
- * @private
- */
- _getTempCacheName() {
- return `${this._cacheName}-temp`;
- }
-
- /**
- * Requests the entry and saves it to the cache if the response
- * is valid.
- *
- * @private
- * @param {Object} options
- * @param {BaseCacheEntry} options.precacheEntry The entry to fetch and cache.
- * @param {Event} [options.event] The install event (if passed).
- * @param {Array<Object>} [options.plugins] An array of plugins to apply to
- * fetch and caching.
- * @return {Promise<boolean>} Returns a promise that resolves once the entry
- * has been fetched and cached or skipped if no update is needed. The
- * promise resolves with true if the entry was cached / updated and
- * false if the entry is already cached and up-to-date.
- */
- async _cacheEntryInTemp({precacheEntry, event, plugins}) {
- let response = await fetchWrapper.fetch({
- request: precacheEntry._networkRequest,
- event,
- fetchOptions: null,
- plugins,
- });
-
- if (response.redirected) {
- response = await cleanRedirect(response);
- }
-
- await cacheWrapper.put({
- cacheName: this._getTempCacheName(),
- request: precacheEntry._cacheRequest,
- response,
- event,
- plugins,
- });
-
- await this._precacheDetailsModel._addEntry(precacheEntry);
-
- return true;
- }
-
- /**
- * Compare the URLs and determines which assets are no longer required
- * in the cache.
- *
- * This should be called in the service worker activate event.
- *
- * @return {
- * Promise<workbox.precaching.CleanupResult>}
- * Resolves with an object containing details of the deleted cache requests
- * and precache revision details.
- *
- * @private
- */
- async _cleanup() {
- const expectedCacheUrls = [];
- this._entriesToCacheMap.forEach((entry) => {
- const fullUrl = new URL(entry._cacheRequest.url, location).toString();
- expectedCacheUrls.push(fullUrl);
- });
-
- const [deletedCacheRequests, deletedRevisionDetails] = await Promise.all([
- this._cleanupCache(expectedCacheUrls),
- this._cleanupDetailsModel(expectedCacheUrls),
- ]);
-
- if (process.env.NODE_ENV !== 'production') {
- printCleanupDetails(deletedCacheRequests, deletedRevisionDetails);
- }
-
- return {
- deletedCacheRequests,
- deletedRevisionDetails,
- };
- }
-
- /**
- * Goes through all the cache entries and removes any that are
- * outdated.
- *
- * @private
- * @param {Array<string>} expectedCacheUrls Array of URLs that are
- * expected to be cached.
- * @return {Promise<Array<string>>} Resolves to an array of URLs
- * of cached requests that were deleted.
- */
- async _cleanupCache(expectedCacheUrls) {
- if (!await caches.has(this._cacheName)) {
- // Cache doesn't exist, so nothing to delete
- return [];
- }
-
- const cache = await caches.open(this._cacheName);
- const cachedRequests = await cache.keys();
- const cachedRequestsToDelete = cachedRequests.filter((cachedRequest) => {
- return !expectedCacheUrls.includes(
- new URL(cachedRequest.url, location).toString()
- );
- });
-
- await Promise.all(
- cachedRequestsToDelete.map((cacheUrl) => cache.delete(cacheUrl))
- );
-
- return cachedRequestsToDelete.map((request) => request.url);
- }
-
- /**
- * Goes through all entries in indexedDB and removes any that are outdated.
- *
- * @private
- * @param {Array<string>} expectedCacheUrls Array of URLs that are
- * expected to be cached.
- * @return {Promise<Array<string>>} Resolves to an array of URLs removed
- * from indexedDB.
- */
- async _cleanupDetailsModel(expectedCacheUrls) {
- const revisionedEntries = await this._precacheDetailsModel._getAllEntries();
- const detailsToDelete = revisionedEntries
- .filter((entry) => {
- const fullUrl = new URL(entry.value.url, location).toString();
- return !expectedCacheUrls.includes(fullUrl);
- });
-
- await Promise.all(
- detailsToDelete.map(
- (entry) => this._precacheDetailsModel._deleteEntry(entry.primaryKey)
- )
- );
- return detailsToDelete.map((entry) => {
- return entry.value.url;
- });
- }
-
- /**
- * Returns an array of fully qualified URL's that will be precached.
- *
- * @return {Array<string>} An array of URLs.
- */
- getCachedUrls() {
- return Array.from(this._entriesToCacheMap.keys())
- .map((url) => new URL(url, location).href);
- }
- }
-
- export default PrecacheController;
|