libresilient/plugins/alt-fetch/index.js

146 wiersze
5.5 KiB
JavaScript

/* ========================================================================= *\
|* === 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
*/
const PLUGIN_NAME = 'alt-fetch';
export function altFetchPlugin (LRPC) {
let config = {};
LRPC.set(PLUGIN_NAME, (LR, init={})=> {
// sane defaults
let defaultConfig = {
// endpoints to use
//
// they have to respond to requests formatted like:
// <endpoint-url>/<path>
//
// let's say the endpoint is:
// https://example.com/api/endpoint/
// ...and that we are trying to get:
// <original-domain>/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
};
config = { ...defaultConfig, ...init };
// 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;
}
});
return {
name: PLUGIN_NAME,
description: 'HTTP(S) fetch() using preconfigured alternative endpoints',
version: 'COMMIT_UNKNOWN',
fetch: (url, init) => fetchContentFromAlternativeEndpoints(url, init, config)
};
}
export function fetchContentFromAlternativeEndpoints (url, init = {}, config) {
// remove the https://original.domain/ bit to get the relative path
// TODO: this assumes that URLs we handle are always relative to the root
// TODO: of the original domain, this needs to be documented
var path = url.replace(/https?:\/\/[^/]+\//, '');
// we really want to make fetch happen, Regina!
// TODO: this change should *probably* be handled on the Service Worker level
// init.cache = 'reload';
// we don't want to modify the original endpoints array
var sourceEndpoints = [...config.endpoints];
let useEndpoints = [];
// if we have fewer than the configured concurrency or just as many, use all of them
if (sourceEndpoints.length <= config.concurrency) {
useEndpoints = sourceEndpoints;
// otherwise get `config.concurrency` endpoints at random
} else {
while (useEndpoints.length < config.concurrency) {
useEndpoints.push(
sourceEndpoints
.splice(Math.floor(Math.random() * sourceEndpoints.length), 1)[0]
);
}
}
// add the rest of the path to each endpoint
useEndpoints.forEach((endpoint, index) => {
useEndpoints[index] = endpoint + path;
});
// debug log
console.log(PLUGIN_NAME, `fetching from alternative endpoints:\n ${useEndpoints.join('\n ')}`);
return Promise.any(
useEndpoints.map(
u=>fetch(u, init)
))
.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
console.log(PLUGIN_NAME, 'fetched:', response.url);
// we need to create a new Response object
// with all the headers added explicitly,
// since response.headers is immutable
var responseInit = {
status: response.status,
statusText: response.statusText,
headers: {},
url: url
};
response.headers.forEach(function (val, header){
responseInit.headers[header] = val;
});
// add the X-LibResilient-* headers to the mix
responseInit.headers['X-LibResilient-Method'] = PLUGIN_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
responseInit.headers['X-LibResilient-ETag'] = response.headers.get('ETag');
if (responseInit.headers['X-LibResilient-ETag'] === null) {
// far from perfect, but what are we going to do, eh?
responseInit.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,
responseInit
);
});
});
}