/* ========================================================================= *\ |* === HTTP(S) fetch() from alternative endpoints === *| \* ========================================================================= */ /** * this plugin does not implement any push method * * NOTICE: this plugin uses Promise.any() * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any * the polyfill is implemented in LibResilient's service-worker.js */ // no polluting of the global namespace please (function () { /* * plugin config settings */ // sane defaults let defaultConfig = { // name of this plugin // should not be changed name: "alt-fetch", // endpoints to use // // they have to respond to requests formatted like: // / // // let's say the endpoint is: // https://example.com/api/endpoint/ // ...and that we are trying to get: // /some/path/img.png // // the endpoint is supposed to return the expected image // when this URL is requested: // https://example.com/api/endpoint/some/path/img.png // // this has to be explicitly configured by the website admin endpoints: [], // how many simultaneous connections to different endpoints do we want // // more concurrency means higher chance of a request succeeding // but uses more bandwidth and other resources; // // 3 seems to be a reasonable default concurrency: 3 } // merge the defaults with settings from LibResilientConfig let config = {...defaultConfig, ...self.LibResilientConfig.plugins[defaultConfig.name]} // reality check: endpoints need to be set to an array of non-empty strings if (typeof(config.endpoints) !== "object" || !Array.isArray(config.endpoints)) { let err = new Error("endpoints not confgured") console.error(err) throw err } /** * getting content using regular HTTP(S) fetch() */ let fetchContentFromAlternativeEndpoints = (url) => { // we're going to try a random endpoint and building an URL of the form: // https://// var path = url.replace(/https?:\/\/[^/]+\//, '') // we don't want to modify the original endpoints array var sourceEndpoints = [...config.endpoints] // if we have fewer than the configured concurrency, use all of them if (sourceEndpoints.length <= config.concurrency) { var useEndpoints = sourceEndpoints // otherwise get `config.concurrency` endpoints at random } else { var useEndpoints = new Array() while (useEndpoints.length < config.concurrency) { // put in the address while we're at it useEndpoints.push( sourceEndpoints .splice(Math.floor(Math.random() * sourceEndpoints.length), 1)[0] + path ) } } // debug log self.log(config.name, `fetching from alternative endpoints:\n ${useEndpoints.join('\n ')}`) return Promise.any( useEndpoints.map( u=>fetch(u, {cache: "reload"}) )) .then((response) => { // 4xx? 5xx? that's a paddlin' if (response.status >= 400) { // throw an Error to fall back to other plugins: throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); } // all good, it seems self.log(config.name, "fetched:", response.url); // we need to create a new Response object // with all the headers added explicitly, // since response.headers is immutable var init = { status: response.status, statusText: response.statusText, headers: {} }; response.headers.forEach(function(val, header){ init.headers[header] = val; }); // add the X-LibResilient-* headers to the mix init.headers['X-LibResilient-Method'] = config.name // we will not have it most of the time, due to CORS rules: // https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header init.headers['X-LibResilient-ETag'] = response.headers.get('ETag') if (init.headers['X-LibResilient-ETag'] === null) { // far from perfect, but what are we going to do, eh? init.headers['X-LibResilient-ETag'] = response.headers.get('last-modified') } // return the new response, using the Blob from the original one return response .blob() .then((blob) => { return new Response( blob, init ) }) }) } // and add ourselves to it // with some additional metadata self.LibResilientPlugins.push({ name: config.name, description: 'HTTP(S) fetch() using alternative endpoints', version: 'COMMIT_UNKNOWN', fetch: fetchContentFromAlternativeEndpoints }) // done with not poluting the global namespace })()