source: frontend/node_modules/workbox-routing/src/Router.ts@ cdcff72

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

Fix frontend appearance

  • Property mode set to 100644
File size: 15.7 KB
Line 
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 {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
11import {
12 RouteHandler,
13 RouteHandlerObject,
14 RouteHandlerCallbackOptions,
15 RouteMatchCallbackOptions,
16} from 'workbox-core/types.js';
17import {HTTPMethod, defaultMethod} from './utils/constants.js';
18import {logger} from 'workbox-core/_private/logger.js';
19import {normalizeHandler} from './utils/normalizeHandler.js';
20import {Route} from './Route.js';
21import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
22
23import './_version.js';
24
25type RequestArgs = string | [string, RequestInit?];
26
27interface CacheURLsMessageData {
28 type: string;
29 payload: {
30 urlsToCache: RequestArgs[];
31 };
32}
33
34/**
35 * The Router can be used to process a `FetchEvent` using one or more
36 * {@link workbox-routing.Route}, responding with a `Response` if
37 * a matching route exists.
38 *
39 * If no route matches a given a request, the Router will use a "default"
40 * handler if one is defined.
41 *
42 * Should the matching Route throw an error, the Router will use a "catch"
43 * handler if one is defined to gracefully deal with issues and respond with a
44 * Request.
45 *
46 * If a request matches multiple routes, the **earliest** registered route will
47 * be used to respond to the request.
48 *
49 * @memberof workbox-routing
50 */
51class Router {
52 private readonly _routes: Map<HTTPMethod, Route[]>;
53 private readonly _defaultHandlerMap: Map<HTTPMethod, RouteHandlerObject>;
54 private _catchHandler?: RouteHandlerObject;
55
56 /**
57 * Initializes a new Router.
58 */
59 constructor() {
60 this._routes = new Map();
61 this._defaultHandlerMap = new Map();
62 }
63
64 /**
65 * @return {Map<string, Array<workbox-routing.Route>>} routes A `Map` of HTTP
66 * method name ('GET', etc.) to an array of all the corresponding `Route`
67 * instances that are registered.
68 */
69 get routes(): Map<HTTPMethod, Route[]> {
70 return this._routes;
71 }
72
73 /**
74 * Adds a fetch event listener to respond to events when a route matches
75 * the event's request.
76 */
77 addFetchListener(): void {
78 // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
79 self.addEventListener('fetch', ((event: FetchEvent) => {
80 const {request} = event;
81 const responsePromise = this.handleRequest({request, event});
82 if (responsePromise) {
83 event.respondWith(responsePromise);
84 }
85 }) as EventListener);
86 }
87
88 /**
89 * Adds a message event listener for URLs to cache from the window.
90 * This is useful to cache resources loaded on the page prior to when the
91 * service worker started controlling it.
92 *
93 * The format of the message data sent from the window should be as follows.
94 * Where the `urlsToCache` array may consist of URL strings or an array of
95 * URL string + `requestInit` object (the same as you'd pass to `fetch()`).
96 *
97 * ```
98 * {
99 * type: 'CACHE_URLS',
100 * payload: {
101 * urlsToCache: [
102 * './script1.js',
103 * './script2.js',
104 * ['./script3.js', {mode: 'no-cors'}],
105 * ],
106 * },
107 * }
108 * ```
109 */
110 addCacheListener(): void {
111 // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
112 self.addEventListener('message', ((event: ExtendableMessageEvent) => {
113 // event.data is type 'any'
114 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
115 if (event.data && event.data.type === 'CACHE_URLS') {
116 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
117 const {payload}: CacheURLsMessageData = event.data;
118
119 if (process.env.NODE_ENV !== 'production') {
120 logger.debug(`Caching URLs from the window`, payload.urlsToCache);
121 }
122
123 const requestPromises = Promise.all(
124 payload.urlsToCache.map((entry: string | [string, RequestInit?]) => {
125 if (typeof entry === 'string') {
126 entry = [entry];
127 }
128
129 const request = new Request(...entry);
130 return this.handleRequest({request, event});
131
132 // TODO(philipwalton): TypeScript errors without this typecast for
133 // some reason (probably a bug). The real type here should work but
134 // doesn't: `Array<Promise<Response> | undefined>`.
135 }) as any[],
136 ); // TypeScript
137
138 event.waitUntil(requestPromises);
139
140 // If a MessageChannel was used, reply to the message on success.
141 if (event.ports && event.ports[0]) {
142 void requestPromises.then(() => event.ports[0].postMessage(true));
143 }
144 }
145 }) as EventListener);
146 }
147
148 /**
149 * Apply the routing rules to a FetchEvent object to get a Response from an
150 * appropriate Route's handler.
151 *
152 * @param {Object} options
153 * @param {Request} options.request The request to handle.
154 * @param {ExtendableEvent} options.event The event that triggered the
155 * request.
156 * @return {Promise<Response>|undefined} A promise is returned if a
157 * registered route can handle the request. If there is no matching
158 * route and there's no `defaultHandler`, `undefined` is returned.
159 */
160 handleRequest({
161 request,
162 event,
163 }: {
164 request: Request;
165 event: ExtendableEvent;
166 }): Promise<Response> | undefined {
167 if (process.env.NODE_ENV !== 'production') {
168 assert!.isInstance(request, Request, {
169 moduleName: 'workbox-routing',
170 className: 'Router',
171 funcName: 'handleRequest',
172 paramName: 'options.request',
173 });
174 }
175
176 const url = new URL(request.url, location.href);
177 if (!url.protocol.startsWith('http')) {
178 if (process.env.NODE_ENV !== 'production') {
179 logger.debug(
180 `Workbox Router only supports URLs that start with 'http'.`,
181 );
182 }
183 return;
184 }
185
186 const sameOrigin = url.origin === location.origin;
187 const {params, route} = this.findMatchingRoute({
188 event,
189 request,
190 sameOrigin,
191 url,
192 });
193 let handler = route && route.handler;
194
195 const debugMessages = [];
196 if (process.env.NODE_ENV !== 'production') {
197 if (handler) {
198 debugMessages.push([`Found a route to handle this request:`, route]);
199
200 if (params) {
201 debugMessages.push([
202 `Passing the following params to the route's handler:`,
203 params,
204 ]);
205 }
206 }
207 }
208
209 // If we don't have a handler because there was no matching route, then
210 // fall back to defaultHandler if that's defined.
211 const method = request.method as HTTPMethod;
212 if (!handler && this._defaultHandlerMap.has(method)) {
213 if (process.env.NODE_ENV !== 'production') {
214 debugMessages.push(
215 `Failed to find a matching route. Falling ` +
216 `back to the default handler for ${method}.`,
217 );
218 }
219 handler = this._defaultHandlerMap.get(method);
220 }
221
222 if (!handler) {
223 if (process.env.NODE_ENV !== 'production') {
224 // No handler so Workbox will do nothing. If logs is set of debug
225 // i.e. verbose, we should print out this information.
226 logger.debug(`No route found for: ${getFriendlyURL(url)}`);
227 }
228 return;
229 }
230
231 if (process.env.NODE_ENV !== 'production') {
232 // We have a handler, meaning Workbox is going to handle the route.
233 // print the routing details to the console.
234 logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
235
236 debugMessages.forEach((msg) => {
237 if (Array.isArray(msg)) {
238 logger.log(...msg);
239 } else {
240 logger.log(msg);
241 }
242 });
243
244 logger.groupEnd();
245 }
246
247 // Wrap in try and catch in case the handle method throws a synchronous
248 // error. It should still callback to the catch handler.
249 let responsePromise;
250 try {
251 responsePromise = handler.handle({url, request, event, params});
252 } catch (err) {
253 responsePromise = Promise.reject(err);
254 }
255
256 // Get route's catch handler, if it exists
257 const catchHandler = route && route.catchHandler;
258
259 if (
260 responsePromise instanceof Promise &&
261 (this._catchHandler || catchHandler)
262 ) {
263 responsePromise = responsePromise.catch(async (err) => {
264 // If there's a route catch handler, process that first
265 if (catchHandler) {
266 if (process.env.NODE_ENV !== 'production') {
267 // Still include URL here as it will be async from the console group
268 // and may not make sense without the URL
269 logger.groupCollapsed(
270 `Error thrown when responding to: ` +
271 ` ${getFriendlyURL(
272 url,
273 )}. Falling back to route's Catch Handler.`,
274 );
275 logger.error(`Error thrown by:`, route);
276 logger.error(err);
277 logger.groupEnd();
278 }
279
280 try {
281 return await catchHandler.handle({url, request, event, params});
282 } catch (catchErr) {
283 if (catchErr instanceof Error) {
284 err = catchErr;
285 }
286 }
287 }
288
289 if (this._catchHandler) {
290 if (process.env.NODE_ENV !== 'production') {
291 // Still include URL here as it will be async from the console group
292 // and may not make sense without the URL
293 logger.groupCollapsed(
294 `Error thrown when responding to: ` +
295 ` ${getFriendlyURL(
296 url,
297 )}. Falling back to global Catch Handler.`,
298 );
299 logger.error(`Error thrown by:`, route);
300 logger.error(err);
301 logger.groupEnd();
302 }
303 return this._catchHandler.handle({url, request, event});
304 }
305
306 throw err;
307 });
308 }
309
310 return responsePromise;
311 }
312
313 /**
314 * Checks a request and URL (and optionally an event) against the list of
315 * registered routes, and if there's a match, returns the corresponding
316 * route along with any params generated by the match.
317 *
318 * @param {Object} options
319 * @param {URL} options.url
320 * @param {boolean} options.sameOrigin The result of comparing `url.origin`
321 * against the current origin.
322 * @param {Request} options.request The request to match.
323 * @param {Event} options.event The corresponding event.
324 * @return {Object} An object with `route` and `params` properties.
325 * They are populated if a matching route was found or `undefined`
326 * otherwise.
327 */
328 findMatchingRoute({
329 url,
330 sameOrigin,
331 request,
332 event,
333 }: RouteMatchCallbackOptions): {
334 route?: Route;
335 params?: RouteHandlerCallbackOptions['params'];
336 } {
337 const routes = this._routes.get(request.method as HTTPMethod) || [];
338 for (const route of routes) {
339 let params: Promise<any> | undefined;
340 // route.match returns type any, not possible to change right now.
341 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
342 const matchResult = route.match({url, sameOrigin, request, event});
343 if (matchResult) {
344 if (process.env.NODE_ENV !== 'production') {
345 // Warn developers that using an async matchCallback is almost always
346 // not the right thing to do.
347 if (matchResult instanceof Promise) {
348 logger.warn(
349 `While routing ${getFriendlyURL(url)}, an async ` +
350 `matchCallback function was used. Please convert the ` +
351 `following route to use a synchronous matchCallback function:`,
352 route,
353 );
354 }
355 }
356
357 // See https://github.com/GoogleChrome/workbox/issues/2079
358 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
359 params = matchResult;
360 if (Array.isArray(params) && params.length === 0) {
361 // Instead of passing an empty array in as params, use undefined.
362 params = undefined;
363 } else if (
364 matchResult.constructor === Object && // eslint-disable-line
365 Object.keys(matchResult).length === 0
366 ) {
367 // Instead of passing an empty object in as params, use undefined.
368 params = undefined;
369 } else if (typeof matchResult === 'boolean') {
370 // For the boolean value true (rather than just something truth-y),
371 // don't set params.
372 // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353
373 params = undefined;
374 }
375
376 // Return early if have a match.
377 return {route, params};
378 }
379 }
380 // If no match was found above, return and empty object.
381 return {};
382 }
383
384 /**
385 * Define a default `handler` that's called when no routes explicitly
386 * match the incoming request.
387 *
388 * Each HTTP method ('GET', 'POST', etc.) gets its own default handler.
389 *
390 * Without a default handler, unmatched requests will go against the
391 * network as if there were no service worker present.
392 *
393 * @param {workbox-routing~handlerCallback} handler A callback
394 * function that returns a Promise resulting in a Response.
395 * @param {string} [method='GET'] The HTTP method to associate with this
396 * default handler. Each method has its own default.
397 */
398 setDefaultHandler(
399 handler: RouteHandler,
400 method: HTTPMethod = defaultMethod,
401 ): void {
402 this._defaultHandlerMap.set(method, normalizeHandler(handler));
403 }
404
405 /**
406 * If a Route throws an error while handling a request, this `handler`
407 * will be called and given a chance to provide a response.
408 *
409 * @param {workbox-routing~handlerCallback} handler A callback
410 * function that returns a Promise resulting in a Response.
411 */
412 setCatchHandler(handler: RouteHandler): void {
413 this._catchHandler = normalizeHandler(handler);
414 }
415
416 /**
417 * Registers a route with the router.
418 *
419 * @param {workbox-routing.Route} route The route to register.
420 */
421 registerRoute(route: Route): void {
422 if (process.env.NODE_ENV !== 'production') {
423 assert!.isType(route, 'object', {
424 moduleName: 'workbox-routing',
425 className: 'Router',
426 funcName: 'registerRoute',
427 paramName: 'route',
428 });
429
430 assert!.hasMethod(route, 'match', {
431 moduleName: 'workbox-routing',
432 className: 'Router',
433 funcName: 'registerRoute',
434 paramName: 'route',
435 });
436
437 assert!.isType(route.handler, 'object', {
438 moduleName: 'workbox-routing',
439 className: 'Router',
440 funcName: 'registerRoute',
441 paramName: 'route',
442 });
443
444 assert!.hasMethod(route.handler, 'handle', {
445 moduleName: 'workbox-routing',
446 className: 'Router',
447 funcName: 'registerRoute',
448 paramName: 'route.handler',
449 });
450
451 assert!.isType(route.method, 'string', {
452 moduleName: 'workbox-routing',
453 className: 'Router',
454 funcName: 'registerRoute',
455 paramName: 'route.method',
456 });
457 }
458
459 if (!this._routes.has(route.method)) {
460 this._routes.set(route.method, []);
461 }
462
463 // Give precedence to all of the earlier routes by adding this additional
464 // route to the end of the array.
465 this._routes.get(route.method)!.push(route);
466 }
467
468 /**
469 * Unregisters a route with the router.
470 *
471 * @param {workbox-routing.Route} route The route to unregister.
472 */
473 unregisterRoute(route: Route): void {
474 if (!this._routes.has(route.method)) {
475 throw new WorkboxError('unregister-route-but-not-found-with-method', {
476 method: route.method,
477 });
478 }
479
480 const routeIndex = this._routes.get(route.method)!.indexOf(route);
481 if (routeIndex > -1) {
482 this._routes.get(route.method)!.splice(routeIndex, 1);
483 } else {
484 throw new WorkboxError('unregister-route-route-not-registered');
485 }
486 }
487}
488
489export {Router};
Note: See TracBrowser for help on using the repository browser.