source: frontend/node_modules/workbox-expiration/src/ExpirationPlugin.ts

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 13 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 10.5 KB
RevLine 
[9af201e]1/*
2 Copyright 2018 Google LLC
3
4 Use of this source code is governed by an MIT-style
5 license that can be found in the LICENSE file or at
6 https://opensource.org/licenses/MIT.
7*/
8
9import {assert} from 'workbox-core/_private/assert.js';
10import {cacheNames} from 'workbox-core/_private/cacheNames.js';
11import {dontWaitFor} from 'workbox-core/_private/dontWaitFor.js';
12import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
13import {logger} from 'workbox-core/_private/logger.js';
14import {registerQuotaErrorCallback} from 'workbox-core/registerQuotaErrorCallback.js';
15import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
16import {WorkboxPlugin} from 'workbox-core/types.js';
17
18import {CacheExpiration} from './CacheExpiration.js';
19
20import './_version.js';
21
22export interface ExpirationPluginOptions {
23 maxEntries?: number;
24 maxAgeSeconds?: number;
25 matchOptions?: CacheQueryOptions;
26 purgeOnQuotaError?: boolean;
27}
28
29/**
30 * This plugin can be used in a `workbox-strategy` to regularly enforce a
31 * limit on the age and / or the number of cached requests.
32 *
33 * It can only be used with `workbox-strategy` instances that have a
34 * [custom `cacheName` property set](/web/tools/workbox/guides/configure-workbox#custom_cache_names_in_strategies).
35 * In other words, it can't be used to expire entries in strategy that uses the
36 * default runtime cache name.
37 *
38 * Whenever a cached response is used or updated, this plugin will look
39 * at the associated cache and remove any old or extra responses.
40 *
41 * When using `maxAgeSeconds`, responses may be used *once* after expiring
42 * because the expiration clean up will not have occurred until *after* the
43 * cached response has been used. If the response has a "Date" header, then
44 * a light weight expiration check is performed and the response will not be
45 * used immediately.
46 *
47 * When using `maxEntries`, the entry least-recently requested will be removed
48 * from the cache first.
49 *
50 * @memberof workbox-expiration
51 */
52class ExpirationPlugin implements WorkboxPlugin {
53 private readonly _config: ExpirationPluginOptions;
54 private readonly _maxAgeSeconds?: number;
55 private _cacheExpirations: Map<string, CacheExpiration>;
56
57 /**
58 * @param {ExpirationPluginOptions} config
59 * @param {number} [config.maxEntries] The maximum number of entries to cache.
60 * Entries used the least will be removed as the maximum is reached.
61 * @param {number} [config.maxAgeSeconds] The maximum age of an entry before
62 * it's treated as stale and removed.
63 * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters)
64 * that will be used when calling `delete()` on the cache.
65 * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to
66 * automatic deletion if the available storage quota has been exceeded.
67 */
68 constructor(config: ExpirationPluginOptions = {}) {
69 if (process.env.NODE_ENV !== 'production') {
70 if (!(config.maxEntries || config.maxAgeSeconds)) {
71 throw new WorkboxError('max-entries-or-age-required', {
72 moduleName: 'workbox-expiration',
73 className: 'Plugin',
74 funcName: 'constructor',
75 });
76 }
77
78 if (config.maxEntries) {
79 assert!.isType(config.maxEntries, 'number', {
80 moduleName: 'workbox-expiration',
81 className: 'Plugin',
82 funcName: 'constructor',
83 paramName: 'config.maxEntries',
84 });
85 }
86
87 if (config.maxAgeSeconds) {
88 assert!.isType(config.maxAgeSeconds, 'number', {
89 moduleName: 'workbox-expiration',
90 className: 'Plugin',
91 funcName: 'constructor',
92 paramName: 'config.maxAgeSeconds',
93 });
94 }
95 }
96
97 this._config = config;
98 this._maxAgeSeconds = config.maxAgeSeconds;
99 this._cacheExpirations = new Map();
100
101 if (config.purgeOnQuotaError) {
102 registerQuotaErrorCallback(() => this.deleteCacheAndMetadata());
103 }
104 }
105
106 /**
107 * A simple helper method to return a CacheExpiration instance for a given
108 * cache name.
109 *
110 * @param {string} cacheName
111 * @return {CacheExpiration}
112 *
113 * @private
114 */
115 private _getCacheExpiration(cacheName: string): CacheExpiration {
116 if (cacheName === cacheNames.getRuntimeName()) {
117 throw new WorkboxError('expire-custom-caches-only');
118 }
119
120 let cacheExpiration = this._cacheExpirations.get(cacheName);
121 if (!cacheExpiration) {
122 cacheExpiration = new CacheExpiration(cacheName, this._config);
123 this._cacheExpirations.set(cacheName, cacheExpiration);
124 }
125 return cacheExpiration;
126 }
127
128 /**
129 * A "lifecycle" callback that will be triggered automatically by the
130 * `workbox-strategies` handlers when a `Response` is about to be returned
131 * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
132 * the handler. It allows the `Response` to be inspected for freshness and
133 * prevents it from being used if the `Response`'s `Date` header value is
134 * older than the configured `maxAgeSeconds`.
135 *
136 * @param {Object} options
137 * @param {string} options.cacheName Name of the cache the response is in.
138 * @param {Response} options.cachedResponse The `Response` object that's been
139 * read from a cache and whose freshness should be checked.
140 * @return {Response} Either the `cachedResponse`, if it's
141 * fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
142 *
143 * @private
144 */
145 cachedResponseWillBeUsed: WorkboxPlugin['cachedResponseWillBeUsed'] = async ({
146 event,
147 request,
148 cacheName,
149 cachedResponse,
150 }) => {
151 if (!cachedResponse) {
152 return null;
153 }
154
155 const isFresh = this._isResponseDateFresh(cachedResponse);
156
157 // Expire entries to ensure that even if the expiration date has
158 // expired, it'll only be used once.
159 const cacheExpiration = this._getCacheExpiration(cacheName);
160 dontWaitFor(cacheExpiration.expireEntries());
161
162 // Update the metadata for the request URL to the current timestamp,
163 // but don't `await` it as we don't want to block the response.
164 const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
165 if (event) {
166 try {
167 event.waitUntil(updateTimestampDone);
168 } catch (error) {
169 if (process.env.NODE_ENV !== 'production') {
170 // The event may not be a fetch event; only log the URL if it is.
171 if ('request' in event) {
172 logger.warn(
173 `Unable to ensure service worker stays alive when ` +
174 `updating cache entry for ` +
175 `'${getFriendlyURL((event as FetchEvent).request.url)}'.`,
176 );
177 }
178 }
179 }
180 }
181
182 return isFresh ? cachedResponse : null;
183 };
184
185 /**
186 * @param {Response} cachedResponse
187 * @return {boolean}
188 *
189 * @private
190 */
191 private _isResponseDateFresh(cachedResponse: Response): boolean {
192 if (!this._maxAgeSeconds) {
193 // We aren't expiring by age, so return true, it's fresh
194 return true;
195 }
196
197 // Check if the 'date' header will suffice a quick expiration check.
198 // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
199 // discussion.
200 const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
201 if (dateHeaderTimestamp === null) {
202 // Unable to parse date, so assume it's fresh.
203 return true;
204 }
205
206 // If we have a valid headerTime, then our response is fresh iff the
207 // headerTime plus maxAgeSeconds is greater than the current time.
208 const now = Date.now();
209 return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
210 }
211
212 /**
213 * This method will extract the data header and parse it into a useful
214 * value.
215 *
216 * @param {Response} cachedResponse
217 * @return {number|null}
218 *
219 * @private
220 */
221 private _getDateHeaderTimestamp(cachedResponse: Response): number | null {
222 if (!cachedResponse.headers.has('date')) {
223 return null;
224 }
225
226 const dateHeader = cachedResponse.headers.get('date');
227 const parsedDate = new Date(dateHeader!);
228 const headerTime = parsedDate.getTime();
229
230 // If the Date header was invalid for some reason, parsedDate.getTime()
231 // will return NaN.
232 if (isNaN(headerTime)) {
233 return null;
234 }
235
236 return headerTime;
237 }
238
239 /**
240 * A "lifecycle" callback that will be triggered automatically by the
241 * `workbox-strategies` handlers when an entry is added to a cache.
242 *
243 * @param {Object} options
244 * @param {string} options.cacheName Name of the cache that was updated.
245 * @param {string} options.request The Request for the cached entry.
246 *
247 * @private
248 */
249 cacheDidUpdate: WorkboxPlugin['cacheDidUpdate'] = async ({
250 cacheName,
251 request,
252 }) => {
253 if (process.env.NODE_ENV !== 'production') {
254 assert!.isType(cacheName, 'string', {
255 moduleName: 'workbox-expiration',
256 className: 'Plugin',
257 funcName: 'cacheDidUpdate',
258 paramName: 'cacheName',
259 });
260 assert!.isInstance(request, Request, {
261 moduleName: 'workbox-expiration',
262 className: 'Plugin',
263 funcName: 'cacheDidUpdate',
264 paramName: 'request',
265 });
266 }
267
268 const cacheExpiration = this._getCacheExpiration(cacheName);
269 await cacheExpiration.updateTimestamp(request.url);
270 await cacheExpiration.expireEntries();
271 };
272
273 /**
274 * This is a helper method that performs two operations:
275 *
276 * - Deletes *all* the underlying Cache instances associated with this plugin
277 * instance, by calling caches.delete() on your behalf.
278 * - Deletes the metadata from IndexedDB used to keep track of expiration
279 * details for each Cache instance.
280 *
281 * When using cache expiration, calling this method is preferable to calling
282 * `caches.delete()` directly, since this will ensure that the IndexedDB
283 * metadata is also cleanly removed and open IndexedDB instances are deleted.
284 *
285 * Note that if you're *not* using cache expiration for a given cache, calling
286 * `caches.delete()` and passing in the cache's name should be sufficient.
287 * There is no Workbox-specific method needed for cleanup in that case.
288 */
289 async deleteCacheAndMetadata(): Promise<void> {
290 // Do this one at a time instead of all at once via `Promise.all()` to
291 // reduce the chance of inconsistency if a promise rejects.
292 for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
293 await self.caches.delete(cacheName);
294 await cacheExpiration.delete();
295 }
296
297 // Reset this._cacheExpirations to its initial state.
298 this._cacheExpirations = new Map();
299 }
300}
301
302export {ExpirationPlugin};
Note: See TracBrowser for help on using the repository browser.