UNPKG

@openziti/ziti-browzer-sw

Version:

Service Worker used as part of the OpenZiti browZer stack

569 lines (546 loc) 22.4 kB
define(['exports'], (function (exports) { 'use strict'; // @ts-ignore try { self['workbox:core:6.5.4'] && _(); } catch (e) { } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ const fallback = (code, ...args) => { let msg = code; if (args.length > 0) { msg += ` :: ${JSON.stringify(args)}`; } return msg; }; const messageGenerator = fallback ; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Workbox errors should be thrown with this class. * This allows use to ensure the type easily in tests, * helps developers identify errors from workbox * easily and allows use to optimise error * messages correctly. * * @private */ class WorkboxError extends Error { /** * * @param {string} errorCode The error code that * identifies this particular error. * @param {Object=} details Any relevant arguments * that will help developers identify issues should * be added as a key on the context object. */ constructor(errorCode, details) { const message = messageGenerator(errorCode, details); super(message); this.name = errorCode; this.details = details; } } // @ts-ignore try { self['workbox:routing:6.5.4'] && _(); } catch (e) { } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The default HTTP method, 'GET', used when there's no specific method * configured for a route. * * @type {string} * * @private */ const defaultMethod = 'GET'; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * @param {function()|Object} handler Either a function, or an object with a * 'handle' method. * @return {Object} An object with a handle method. * * @private */ const normalizeHandler = (handler) => { if (handler && typeof handler === 'object') { return handler; } else { return { handle: handler }; } }; /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * A `Route` consists of a pair of callback functions, "match" and "handler". * The "match" callback determine if a route should be used to "handle" a * request by returning a non-falsy value if it can. The "handler" callback * is called when there is a match and should return a Promise that resolves * to a `Response`. * * @memberof workbox-routing */ class Route { /** * Constructor for Route class. * * @param {workbox-routing~matchCallback} match * A callback function that determines whether the route matches a given * `fetch` event by returning a non-falsy value. * @param {workbox-routing~handlerCallback} handler A callback * function that returns a Promise resolving to a Response. * @param {string} [method='GET'] The HTTP method to match the Route * against. */ constructor(match, handler, method = defaultMethod) { // These values are referenced directly by Router so cannot be // altered by minificaton. this.handler = normalizeHandler(handler); this.match = match; this.method = method; } /** * * @param {workbox-routing-handlerCallback} handler A callback * function that returns a Promise resolving to a Response */ setCatchHandler(handler) { this.catchHandler = normalizeHandler(handler); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * RegExpRoute makes it easy to create a regular expression based * {@link workbox-routing.Route}. * * For same-origin requests the RegExp only needs to match part of the URL. For * requests against third-party servers, you must define a RegExp that matches * the start of the URL. * * @memberof workbox-routing * @extends workbox-routing.Route */ class RegExpRoute extends Route { /** * If the regular expression contains * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, * the captured values will be passed to the * {@link workbox-routing~handlerCallback} `params` * argument. * * @param {RegExp} regExp The regular expression to match against URLs. * @param {workbox-routing~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * @param {string} [method='GET'] The HTTP method to match the Route * against. */ constructor(regExp, handler, method) { const match = ({ url }) => { const result = regExp.exec(url.href); // Return immediately if there's no match. if (!result) { return; } // Require that the match start at the first character in the URL string // if it's a cross-origin request. // See https://github.com/GoogleChrome/workbox/issues/281 for the context // behind this behavior. if (url.origin !== location.origin && result.index !== 0) { return; } // If the route matches, but there aren't any capture groups defined, then // this will return [], which is truthy and therefore sufficient to // indicate a match. // If there are capture groups, then it will return their values. return result.slice(1); }; super(match, handler, method); } } /* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * The Router can be used to process a `FetchEvent` using one or more * {@link workbox-routing.Route}, responding with a `Response` if * a matching route exists. * * If no route matches a given a request, the Router will use a "default" * handler if one is defined. * * Should the matching Route throw an error, the Router will use a "catch" * handler if one is defined to gracefully deal with issues and respond with a * Request. * * If a request matches multiple routes, the **earliest** registered route will * be used to respond to the request. * * @memberof workbox-routing */ class Router { /** * Initializes a new Router. */ constructor() { this._routes = new Map(); this._defaultHandlerMap = new Map(); } /** * @return {Map<string, Array<workbox-routing.Route>>} routes A `Map` of HTTP * method name ('GET', etc.) to an array of all the corresponding `Route` * instances that are registered. */ get routes() { return this._routes; } /** * Adds a fetch event listener to respond to events when a route matches * the event's request. */ addFetchListener() { // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 self.addEventListener('fetch', ((event) => { const { request } = event; const responsePromise = this.handleRequest({ request, event }); if (responsePromise) { event.respondWith(responsePromise); } })); } /** * Adds a message event listener for URLs to cache from the window. * This is useful to cache resources loaded on the page prior to when the * service worker started controlling it. * * The format of the message data sent from the window should be as follows. * Where the `urlsToCache` array may consist of URL strings or an array of * URL string + `requestInit` object (the same as you'd pass to `fetch()`). * * ``` * { * type: 'CACHE_URLS', * payload: { * urlsToCache: [ * './script1.js', * './script2.js', * ['./script3.js', {mode: 'no-cors'}], * ], * }, * } * ``` */ addCacheListener() { // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 self.addEventListener('message', ((event) => { // event.data is type 'any' // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (event.data && event.data.type === 'CACHE_URLS') { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { payload } = event.data; const requestPromises = Promise.all(payload.urlsToCache.map((entry) => { if (typeof entry === 'string') { entry = [entry]; } const request = new Request(...entry); return this.handleRequest({ request, event }); // TODO(philipwalton): TypeScript errors without this typecast for // some reason (probably a bug). The real type here should work but // doesn't: `Array<Promise<Response> | undefined>`. })); // TypeScript event.waitUntil(requestPromises); // If a MessageChannel was used, reply to the message on success. if (event.ports && event.ports[0]) { void requestPromises.then(() => event.ports[0].postMessage(true)); } } })); } /** * Apply the routing rules to a FetchEvent object to get a Response from an * appropriate Route's handler. * * @param {Object} options * @param {Request} options.request The request to handle. * @param {ExtendableEvent} options.event The event that triggered the * request. * @return {Promise<Response>|undefined} A promise is returned if a * registered route can handle the request. If there is no matching * route and there's no `defaultHandler`, `undefined` is returned. */ handleRequest({ request, event, }) { const url = new URL(request.url, location.href); if (!url.protocol.startsWith('http')) { return; } const sameOrigin = url.origin === location.origin; const { params, route } = this.findMatchingRoute({ event, request, sameOrigin, url, }); let handler = route && route.handler; // If we don't have a handler because there was no matching route, then // fall back to defaultHandler if that's defined. const method = request.method; if (!handler && this._defaultHandlerMap.has(method)) { handler = this._defaultHandlerMap.get(method); } if (!handler) { return; } // Wrap in try and catch in case the handle method throws a synchronous // error. It should still callback to the catch handler. let responsePromise; try { responsePromise = handler.handle({ url, request, event, params }); } catch (err) { responsePromise = Promise.reject(err); } // Get route's catch handler, if it exists const catchHandler = route && route.catchHandler; if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { responsePromise = responsePromise.catch(async (err) => { // If there's a route catch handler, process that first if (catchHandler) { try { return await catchHandler.handle({ url, request, event, params }); } catch (catchErr) { if (catchErr instanceof Error) { err = catchErr; } } } if (this._catchHandler) { return this._catchHandler.handle({ url, request, event }); } throw err; }); } return responsePromise; } /** * Checks a request and URL (and optionally an event) against the list of * registered routes, and if there's a match, returns the corresponding * route along with any params generated by the match. * * @param {Object} options * @param {URL} options.url * @param {boolean} options.sameOrigin The result of comparing `url.origin` * against the current origin. * @param {Request} options.request The request to match. * @param {Event} options.event The corresponding event. * @return {Object} An object with `route` and `params` properties. * They are populated if a matching route was found or `undefined` * otherwise. */ findMatchingRoute({ url, sameOrigin, request, event, }) { const routes = this._routes.get(request.method) || []; for (const route of routes) { let params; // route.match returns type any, not possible to change right now. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const matchResult = route.match({ url, sameOrigin, request, event }); if (matchResult) { // See https://github.com/GoogleChrome/workbox/issues/2079 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment params = matchResult; if (Array.isArray(params) && params.length === 0) { // Instead of passing an empty array in as params, use undefined. params = undefined; } else if (matchResult.constructor === Object && // eslint-disable-line Object.keys(matchResult).length === 0) { // Instead of passing an empty object in as params, use undefined. params = undefined; } else if (typeof matchResult === 'boolean') { // For the boolean value true (rather than just something truth-y), // don't set params. // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 params = undefined; } // Return early if have a match. return { route, params }; } } // If no match was found above, return and empty object. return {}; } /** * Define a default `handler` that's called when no routes explicitly * match the incoming request. * * Each HTTP method ('GET', 'POST', etc.) gets its own default handler. * * Without a default handler, unmatched requests will go against the * network as if there were no service worker present. * * @param {workbox-routing~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. * @param {string} [method='GET'] The HTTP method to associate with this * default handler. Each method has its own default. */ setDefaultHandler(handler, method = defaultMethod) { this._defaultHandlerMap.set(method, normalizeHandler(handler)); } /** * If a Route throws an error while handling a request, this `handler` * will be called and given a chance to provide a response. * * @param {workbox-routing~handlerCallback} handler A callback * function that returns a Promise resulting in a Response. */ setCatchHandler(handler) { this._catchHandler = normalizeHandler(handler); } /** * Registers a route with the router. * * @param {workbox-routing.Route} route The route to register. */ registerRoute(route) { if (!this._routes.has(route.method)) { this._routes.set(route.method, []); } // Give precedence to all of the earlier routes by adding this additional // route to the end of the array. this._routes.get(route.method).push(route); } /** * Unregisters a route with the router. * * @param {workbox-routing.Route} route The route to unregister. */ unregisterRoute(route) { if (!this._routes.has(route.method)) { throw new WorkboxError('unregister-route-but-not-found-with-method', { method: route.method, }); } const routeIndex = this._routes.get(route.method).indexOf(route); if (routeIndex > -1) { this._routes.get(route.method).splice(routeIndex, 1); } else { throw new WorkboxError('unregister-route-route-not-registered'); } } } /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ let defaultRouter; /** * Creates a new, singleton Router instance if one does not exist. If one * does already exist, that instance is returned. * * @private * @return {Router} */ const getOrCreateDefaultRouter = () => { if (!defaultRouter) { defaultRouter = new Router(); // The helpers that use the default Router assume these listeners exist. defaultRouter.addFetchListener(); defaultRouter.addCacheListener(); } return defaultRouter; }; /* Copyright 2019 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ /** * Easily register a RegExp, string, or function with a caching * strategy to a singleton Router instance. * * This method will generate a Route for you if needed and * call {@link workbox-routing.Router#registerRoute}. * * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture * If the capture param is a `Route`, all other arguments will be ignored. * @param {workbox-routing~handlerCallback} [handler] A callback * function that returns a Promise resulting in a Response. This parameter * is required if `capture` is not a `Route` object. * @param {string} [method='GET'] The HTTP method to match the Route * against. * @return {workbox-routing.Route} The generated `Route`. * * @memberof workbox-routing */ function registerRoute(capture, handler, method) { let route; if (typeof capture === 'string') { const captureUrl = new URL(capture, location.href); const matchCallback = ({ url }) => { return url.href === captureUrl.href; }; // If `capture` is a string then `handler` and `method` must be present. route = new Route(matchCallback, handler, method); } else if (capture instanceof RegExp) { // If `capture` is a `RegExp` then `handler` and `method` must be present. route = new RegExpRoute(capture, handler, method); } else if (typeof capture === 'function') { // If `capture` is a function then `handler` and `method` must be present. route = new Route(capture, handler, method); } else if (capture instanceof Route) { route = capture; } else { throw new WorkboxError('unsupported-route-type', { moduleName: 'workbox-routing', funcName: 'registerRoute', paramName: 'capture', }); } const defaultRouter = getOrCreateDefaultRouter(); defaultRouter.registerRoute(route); return route; } exports.registerRoute = registerRoute; }));