Enforce request policies for fetch() or GM_xmlhttpRequest methods with fallback routing
Tätä skriptiä ei tulisi asentaa suoraan. Se on kirjasto muita skriptejä varten sisällytettäväksi metadirektiivillä // @require https://update.greasyfork.org/scripts/591898/1906160/Transport%20policy%20enforcer.js.
// ==UserScript==
// @name Transport policy enforcer
// @namespace 861ddd094884eac5bea7a3b12e074f34
// @author Anonymous, Codex
// @version 1.0.0
// @license BSD-0
// @grant none
// ==/UserScript==
(function (global) {
'use strict';
const TRANSPORTS = ['gm-first', 'fetch-first', 'gm-only', 'fetch-only'];
const config = {
transport: 'gm-first',
fallbackOn: {
transportError: true,
statuses: [403, 429, [500, 599]],
predicate: null,
},
gmProvider: () =>
(typeof GM !== 'undefined' && GM.xmlHttpRequest) ||
(typeof GM_xmlhttpRequest === 'function' && GM_xmlhttpRequest) ||
null,
fetchProvider: () =>
(typeof window !== 'undefined' && window.fetch)
? window.fetch.bind(window)
: (typeof fetch === 'function' ? fetch : null),
timeoutMs: 30000,
onTransportEvent: null,
};
function configure(options) {
return Object.assign(config, options || {});
}
function matchesStatus(status, list) {
if (!Array.isArray(list)) return false;
const value = Number(status);
return list.some((entry) => {
if (Array.isArray(entry)) {
return entry.length === 2 && value >= Number(entry[0]) && value <= Number(entry[1]);
}
return value === Number(entry);
});
}
function emit(event) {
if (typeof config.onTransportEvent !== 'function') return;
try { config.onTransportEvent(event); } catch (e) { /* host hook is best-effort */ }
}
function asError(value, message) {
if (value instanceof Error) return value;
const error = new Error(value && value.message ? value.message : message);
if (value !== undefined) error.cause = value;
return error;
}
function transportError(value, transport, details, kind) {
const error = asError(value, `TransportPolicy: ${kind} using ${transport} for ${details.url}`);
try { error.transport = transport; } catch (e) { /* a frozen provider error is replaced below */ }
if (error.transport !== transport) {
const replacement = new Error(error.message);
replacement.cause = error;
replacement.transport = transport;
if (kind === 'timeout') replacement.timedOut = true;
return replacement;
}
if (kind === 'timeout') error.timedOut = true;
return error;
}
function normaliseGmResponse(raw, details) {
raw = raw || {};
const responseText = typeof raw.responseText === 'string' ? raw.responseText : '';
return {
status: Number(raw.status) || 0,
statusText: raw.statusText || '',
responseText,
response: raw.response !== undefined ? raw.response : responseText,
responseHeaders: raw.responseHeaders || '',
finalUrl: raw.finalUrl || details.url,
transport: 'gm',
fellBack: false,
};
}
function gmRequest(details, provider) {
return new Promise((resolve, reject) => {
const options = Object.assign({}, details, {
timeout: details.timeout ?? config.timeoutMs,
onload: (response) => resolve(normaliseGmResponse(response, details)),
onerror: (error) => reject(transportError(error, 'gm', details, 'network error')),
ontimeout: (error) => reject(transportError(error, 'gm', details, 'timeout')),
onabort: (error) => reject(transportError(error, 'gm', details, 'abort')),
});
try {
provider(options);
} catch (error) {
reject(transportError(error, 'gm', details, 'provider error'));
}
});
}
function headerString(headers) {
let lines = '';
if (headers && typeof headers.forEach === 'function') {
headers.forEach((value, name) => { lines += `${name}: ${value}\r\n`; });
}
return lines;
}
async function fetchRequest(details, provider) {
const timeout = details.timeout ?? config.timeoutMs;
const init = {
method: details.method || 'GET',
credentials: 'include',
cache: 'no-store',
redirect: 'follow',
};
if (details.headers) init.headers = Object.assign({}, details.headers);
if (details.data != null) init.body = details.data;
let controller;
let timer;
if (timeout > 0 && typeof AbortController === 'function') {
controller = new AbortController();
init.signal = controller.signal;
timer = setTimeout(() => controller.abort(), timeout);
}
try {
const raw = await provider(details.url, init);
const responseType = details.responseType || 'text';
let response;
let responseText = '';
if (responseType === 'json') {
responseText = await raw.text();
response = JSON.parse(responseText);
} else if (responseType === 'arraybuffer') {
response = await raw.arrayBuffer();
} else if (responseType === 'blob') {
response = await raw.blob();
} else {
responseText = await raw.text();
response = responseText;
}
return {
status: Number(raw.status) || 0,
statusText: raw.statusText || '',
responseText,
response,
responseHeaders: headerString(raw.headers),
finalUrl: raw.url || details.url,
transport: 'fetch',
fellBack: false,
};
} catch (error) {
const timedOut = !!(controller && controller.signal.aborted);
throw transportError(error, 'fetch', details, timedOut ? 'timeout' : 'transport error');
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
function providerFor(transport) {
const factory = transport === 'gm' ? config.gmProvider : config.fetchProvider;
if (typeof factory !== 'function') return null;
return factory();
}
async function attempt(transport, details) {
emit({ type: 'attempt', transport, url: details.url, method: details.method || 'GET' });
let provider;
try {
provider = providerFor(transport);
} catch (error) {
throw transportError(error, transport, details, 'provider error');
}
if (typeof provider !== 'function') {
const error = transportError(undefined, transport, details, 'unavailable');
error.unavailable = true;
throw error;
}
return transport === 'gm'
? gmRequest(details, provider)
: fetchRequest(details, provider);
}
function ordering(policy) {
switch (policy) {
case 'gm-first': return ['gm', 'fetch'];
case 'fetch-first': return ['fetch', 'gm'];
case 'gm-only': return ['gm'];
case 'fetch-only': return ['fetch'];
default: throw new Error(`TransportPolicy: unknown transport policy ${policy}`);
}
}
function fallbackDecision(outcome, rules, transport) {
if (typeof rules.predicate === 'function') {
const verdict = rules.predicate(outcome.response, transport);
if (typeof verdict === 'boolean') {
return verdict ? { fallback: true, reason: 'predicate' } : { fallback: false };
}
}
if (outcome.error) {
return rules.transportError
? { fallback: true, reason: 'transport-error' }
: { fallback: false };
}
return matchesStatus(outcome.response.status, rules.statuses)
? { fallback: true, reason: 'status' }
: { fallback: false };
}
function primarySummary(transport, outcome) {
if (outcome.error) return { transport, error: outcome.error };
return { transport, status: outcome.response.status };
}
function finalEvent(type, transport, details, value, reason) {
const event = { type, transport, url: details.url, method: details.method || 'GET' };
if (value && typeof value.status === 'number') event.status = value.status;
if (value instanceof Error) event.error = value;
if (reason) event.reason = reason;
emit(event);
}
async function request(input) {
const details = input || {};
const policy = details.transport || config.transport;
const order = ordering(policy);
const rules = Object.assign({}, config.fallbackOn || {}, details.fallbackOn || {});
const { transport: ignoredTransport, fallbackOn: ignoredFallback, ...networkDetails } = details;
const primaryTransport = order[0];
let primary;
try {
primary = { response: await attempt(primaryTransport, networkDetails) };
} catch (error) {
primary = { error };
}
if (order.length === 1) {
if (primary.error) {
finalEvent('rejected', primaryTransport, networkDetails, primary.error,
primary.error.unavailable ? 'unavailable' : 'transport-error');
throw primary.error;
}
finalEvent('resolved', primaryTransport, networkDetails, primary.response);
return primary.response;
}
let decision;
if (primary.error && primary.error.unavailable) {
decision = { fallback: true, reason: 'unavailable' };
} else {
try {
decision = fallbackDecision(primary, rules, primaryTransport);
} catch (error) {
const rejection = transportError(error, primaryTransport, networkDetails, 'predicate error');
finalEvent('rejected', primaryTransport, networkDetails, rejection);
throw rejection;
}
}
if (!decision.fallback) {
if (primary.error) {
finalEvent('rejected', primaryTransport, networkDetails, primary.error, 'transport-error');
throw primary.error;
}
finalEvent('resolved', primaryTransport, networkDetails, primary.response);
return primary.response;
}
const fallbackEvent = {
type: 'fallback',
transport: primaryTransport,
url: networkDetails.url,
method: networkDetails.method || 'GET',
reason: decision.reason,
};
if (primary.response) fallbackEvent.status = primary.response.status;
if (primary.error) fallbackEvent.error = primary.error;
emit(fallbackEvent);
const secondaryTransport = order[1];
try {
const response = await attempt(secondaryTransport, networkDetails);
response.fellBack = true;
response.primary = primarySummary(primaryTransport, primary);
finalEvent('resolved', secondaryTransport, networkDetails, response);
return response;
} catch (error) {
error.transport = secondaryTransport;
if (primary.error) error.primaryError = primary.error;
else error.primaryResponse = primary.response;
finalEvent('rejected', secondaryTransport, networkDetails, error,
error.unavailable ? 'unavailable' : 'transport-error');
throw error;
}
}
function xmlHttpRequest(details) {
const { onload, onerror, ontimeout, ...requestDetails } = details;
request(requestDetails).then(
(response) => { if (typeof onload === 'function') onload(response); },
(error) => {
if (error.timedOut && typeof ontimeout === 'function') ontimeout(error);
else if (typeof onerror === 'function') onerror(error);
}
);
return { abort() {} };
}
global.TransportPolicy = {
configure,
config,
request,
xmlHttpRequest,
TRANSPORTS,
matchesStatus,
};
}(typeof globalThis !== 'undefined' ? globalThis : this));