You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

394 regels
12 KiB

  1. /*
  2. Copyright 2017 Google Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. import {cacheNames} from 'workbox-core/_private/cacheNames.mjs';
  14. import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
  15. import {fetchWrapper} from 'workbox-core/_private/fetchWrapper.mjs';
  16. import {cacheWrapper} from 'workbox-core/_private/cacheWrapper.mjs';
  17. import {assert} from 'workbox-core/_private/assert.mjs';
  18. import PrecacheEntry from '../models/PrecacheEntry.mjs';
  19. import PrecachedDetailsModel from '../models/PrecachedDetailsModel.mjs';
  20. import showWarningsIfNeeded from '../utils/showWarningsIfNeeded.mjs';
  21. import printInstallDetails from '../utils/printInstallDetails.mjs';
  22. import printCleanupDetails from '../utils/printCleanupDetails.mjs';
  23. import cleanRedirect from '../utils/cleanRedirect.mjs';
  24. import '../_version.mjs';
  25. /**
  26. * Performs efficient precaching of assets.
  27. *
  28. * @memberof workbox.precaching
  29. */
  30. class PrecacheController {
  31. /**
  32. * Create a new PrecacheController.
  33. *
  34. * @param {string} cacheName
  35. */
  36. constructor(cacheName) {
  37. this._cacheName = cacheNames.getPrecacheName(cacheName);
  38. this._entriesToCacheMap = new Map();
  39. this._precacheDetailsModel = new PrecachedDetailsModel(this._cacheName);
  40. }
  41. /**
  42. * This method will add items to the precache list, removing duplicates
  43. * and ensuring the information is valid.
  44. *
  45. * @param {
  46. * Array<module:workbox-precaching.PrecacheController.PrecacheEntry|string>
  47. * } entries Array of entries to
  48. * precache.
  49. */
  50. addToCacheList(entries) {
  51. if (process.env.NODE_ENV !== 'production') {
  52. assert.isArray(entries, {
  53. moduleName: 'workbox-precaching',
  54. className: 'PrecacheController',
  55. funcName: 'addToCacheList',
  56. paramName: 'entries',
  57. });
  58. }
  59. entries.map((userEntry) => {
  60. this._addEntryToCacheList(
  61. this._parseEntry(userEntry)
  62. );
  63. });
  64. }
  65. /**
  66. * This method returns a precache entry.
  67. *
  68. * @private
  69. * @param {string|Object} input
  70. * @return {PrecacheEntry}
  71. */
  72. _parseEntry(input) {
  73. switch (typeof input) {
  74. case 'string': {
  75. if (process.env.NODE_ENV !== 'production') {
  76. if (input.length === 0) {
  77. throw new WorkboxError(
  78. 'add-to-cache-list-unexpected-type', {
  79. entry: input,
  80. }
  81. );
  82. }
  83. }
  84. return new PrecacheEntry(input, input, input);
  85. }
  86. case 'object': {
  87. if (process.env.NODE_ENV !== 'production') {
  88. if (!input || !input.url) {
  89. throw new WorkboxError(
  90. 'add-to-cache-list-unexpected-type', {
  91. entry: input,
  92. }
  93. );
  94. }
  95. }
  96. return new PrecacheEntry(
  97. input, input.url, input.revision || input.url, !!input.revision);
  98. }
  99. default:
  100. throw new WorkboxError('add-to-cache-list-unexpected-type', {
  101. entry: input,
  102. });
  103. }
  104. }
  105. /**
  106. * Adds an entry to the precache list, accounting for possible duplicates.
  107. *
  108. * @private
  109. * @param {PrecacheEntry} entryToAdd
  110. */
  111. _addEntryToCacheList(entryToAdd) {
  112. // Check if the entry is already part of the map
  113. const existingEntry = this._entriesToCacheMap.get(entryToAdd._entryId);
  114. if (!existingEntry) {
  115. this._entriesToCacheMap.set(entryToAdd._entryId, entryToAdd);
  116. return;
  117. }
  118. // Duplicates are fine, but make sure the revision information
  119. // is the same.
  120. if (existingEntry._revision !== entryToAdd._revision) {
  121. throw new WorkboxError('add-to-cache-list-conflicting-entries', {
  122. firstEntry: existingEntry._originalInput,
  123. secondEntry: entryToAdd._originalInput,
  124. });
  125. }
  126. }
  127. /**
  128. * Call this method from a service work install event to start
  129. * precaching assets.
  130. *
  131. * @param {Object} options
  132. * @param {boolean} [options.suppressWarnings] Suppress warning messages.
  133. * @param {Event} [options.event] The install event (if needed).
  134. * @param {Array<Object>} [options.plugins] Plugins to be used for fetching
  135. * and caching during install.
  136. * @return {Promise<workbox.precaching.InstallResult>}
  137. */
  138. async install({suppressWarnings = false, event, plugins} = {}) {
  139. if (process.env.NODE_ENV !== 'production') {
  140. if (suppressWarnings !== true) {
  141. showWarningsIfNeeded(this._entriesToCacheMap);
  142. }
  143. if (plugins) {
  144. assert.isArray(plugins, {
  145. moduleName: 'workbox-precaching',
  146. className: 'PrecacheController',
  147. funcName: 'install',
  148. paramName: 'plugins',
  149. });
  150. }
  151. }
  152. // Empty the temporary cache.
  153. // NOTE: We remove all entries instead of deleting the cache as the cache
  154. // may be marked for deletion but still exist until a later stage
  155. // resulting in unexpected behavior of being deletect when all references
  156. // are dropped.
  157. // https://github.com/GoogleChrome/workbox/issues/1368
  158. const tempCache = await caches.open(this._getTempCacheName());
  159. const requests = await tempCache.keys();
  160. await Promise.all(requests.map((request) => {
  161. return tempCache.delete(request);
  162. }));
  163. const entriesToPrecache = [];
  164. const entriesAlreadyPrecached = [];
  165. for (const precacheEntry of this._entriesToCacheMap.values()) {
  166. if (await this._precacheDetailsModel._isEntryCached(
  167. this._cacheName, precacheEntry)) {
  168. entriesAlreadyPrecached.push(precacheEntry);
  169. } else {
  170. entriesToPrecache.push(precacheEntry);
  171. }
  172. }
  173. // Wait for all requests to be cached.
  174. await Promise.all(entriesToPrecache.map((precacheEntry) => {
  175. return this._cacheEntryInTemp({event, plugins, precacheEntry});
  176. }));
  177. if (process.env.NODE_ENV !== 'production') {
  178. printInstallDetails(entriesToPrecache, entriesAlreadyPrecached);
  179. }
  180. return {
  181. updatedEntries: entriesToPrecache,
  182. notUpdatedEntries: entriesAlreadyPrecached,
  183. };
  184. }
  185. /**
  186. * Takes the current set of temporary files and moves them to the final
  187. * cache, deleting the temporary cache once copying is complete.
  188. *
  189. * @param {Object} options
  190. * @param {Array<Object>} options.plugins Plugins to be used for fetching
  191. * and caching during install.
  192. * @return {
  193. * Promise<workbox.precaching.CleanupResult>}
  194. * Resolves with an object containing details of the deleted cache requests
  195. * and precache revision details.
  196. */
  197. async activate(options = {}) {
  198. const tempCache = await caches.open(this._getTempCacheName());
  199. const requests = await tempCache.keys();
  200. // Process each request/response one at a time, deleting the temporary entry
  201. // when done, to help avoid triggering quota errors.
  202. for (const request of requests) {
  203. const response = await tempCache.match(request);
  204. await cacheWrapper.put({
  205. cacheName: this._cacheName,
  206. request,
  207. response,
  208. plugins: options.plugins,
  209. });
  210. await tempCache.delete(request);
  211. }
  212. return this._cleanup();
  213. }
  214. /**
  215. * Returns the name of the temporary cache.
  216. *
  217. * @return {string}
  218. *
  219. * @private
  220. */
  221. _getTempCacheName() {
  222. return `${this._cacheName}-temp`;
  223. }
  224. /**
  225. * Requests the entry and saves it to the cache if the response
  226. * is valid.
  227. *
  228. * @private
  229. * @param {Object} options
  230. * @param {BaseCacheEntry} options.precacheEntry The entry to fetch and cache.
  231. * @param {Event} [options.event] The install event (if passed).
  232. * @param {Array<Object>} [options.plugins] An array of plugins to apply to
  233. * fetch and caching.
  234. * @return {Promise<boolean>} Returns a promise that resolves once the entry
  235. * has been fetched and cached or skipped if no update is needed. The
  236. * promise resolves with true if the entry was cached / updated and
  237. * false if the entry is already cached and up-to-date.
  238. */
  239. async _cacheEntryInTemp({precacheEntry, event, plugins}) {
  240. let response = await fetchWrapper.fetch({
  241. request: precacheEntry._networkRequest,
  242. event,
  243. fetchOptions: null,
  244. plugins,
  245. });
  246. if (response.redirected) {
  247. response = await cleanRedirect(response);
  248. }
  249. await cacheWrapper.put({
  250. cacheName: this._getTempCacheName(),
  251. request: precacheEntry._cacheRequest,
  252. response,
  253. event,
  254. plugins,
  255. });
  256. await this._precacheDetailsModel._addEntry(precacheEntry);
  257. return true;
  258. }
  259. /**
  260. * Compare the URLs and determines which assets are no longer required
  261. * in the cache.
  262. *
  263. * This should be called in the service worker activate event.
  264. *
  265. * @return {
  266. * Promise<workbox.precaching.CleanupResult>}
  267. * Resolves with an object containing details of the deleted cache requests
  268. * and precache revision details.
  269. *
  270. * @private
  271. */
  272. async _cleanup() {
  273. const expectedCacheUrls = [];
  274. this._entriesToCacheMap.forEach((entry) => {
  275. const fullUrl = new URL(entry._cacheRequest.url, location).toString();
  276. expectedCacheUrls.push(fullUrl);
  277. });
  278. const [deletedCacheRequests, deletedRevisionDetails] = await Promise.all([
  279. this._cleanupCache(expectedCacheUrls),
  280. this._cleanupDetailsModel(expectedCacheUrls),
  281. ]);
  282. if (process.env.NODE_ENV !== 'production') {
  283. printCleanupDetails(deletedCacheRequests, deletedRevisionDetails);
  284. }
  285. return {
  286. deletedCacheRequests,
  287. deletedRevisionDetails,
  288. };
  289. }
  290. /**
  291. * Goes through all the cache entries and removes any that are
  292. * outdated.
  293. *
  294. * @private
  295. * @param {Array<string>} expectedCacheUrls Array of URLs that are
  296. * expected to be cached.
  297. * @return {Promise<Array<string>>} Resolves to an array of URLs
  298. * of cached requests that were deleted.
  299. */
  300. async _cleanupCache(expectedCacheUrls) {
  301. if (!await caches.has(this._cacheName)) {
  302. // Cache doesn't exist, so nothing to delete
  303. return [];
  304. }
  305. const cache = await caches.open(this._cacheName);
  306. const cachedRequests = await cache.keys();
  307. const cachedRequestsToDelete = cachedRequests.filter((cachedRequest) => {
  308. return !expectedCacheUrls.includes(
  309. new URL(cachedRequest.url, location).toString()
  310. );
  311. });
  312. await Promise.all(
  313. cachedRequestsToDelete.map((cacheUrl) => cache.delete(cacheUrl))
  314. );
  315. return cachedRequestsToDelete.map((request) => request.url);
  316. }
  317. /**
  318. * Goes through all entries in indexedDB and removes any that are outdated.
  319. *
  320. * @private
  321. * @param {Array<string>} expectedCacheUrls Array of URLs that are
  322. * expected to be cached.
  323. * @return {Promise<Array<string>>} Resolves to an array of URLs removed
  324. * from indexedDB.
  325. */
  326. async _cleanupDetailsModel(expectedCacheUrls) {
  327. const revisionedEntries = await this._precacheDetailsModel._getAllEntries();
  328. const detailsToDelete = revisionedEntries
  329. .filter((entry) => {
  330. const fullUrl = new URL(entry.value.url, location).toString();
  331. return !expectedCacheUrls.includes(fullUrl);
  332. });
  333. await Promise.all(
  334. detailsToDelete.map(
  335. (entry) => this._precacheDetailsModel._deleteEntry(entry.primaryKey)
  336. )
  337. );
  338. return detailsToDelete.map((entry) => {
  339. return entry.value.url;
  340. });
  341. }
  342. /**
  343. * Returns an array of fully qualified URL's that will be precached.
  344. *
  345. * @return {Array<string>} An array of URLs.
  346. */
  347. getCachedUrls() {
  348. return Array.from(this._entriesToCacheMap.keys())
  349. .map((url) => new URL(url, location).href);
  350. }
  351. }
  352. export default PrecacheController;