Greasy Fork is available in English.

LocalCDN

LocalCDN: Webresource manager to request and cache web resources, aiming to make web requests faster and more reliable.

이 스크립트는 직접 설치해서 쓰는 게 아닙니다. 다른 스크립트가 메타 명령 // @require https://update.greasyfork.org/scripts/449580/1081620/LocalCDN.js(으)로 포함하여 쓰는 라이브러리입니다.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               LocalCDN
// @namespace          LocalCDN
// @version            0.1.1
// @description        LocalCDN: Webresource manager to request and cache web resources, aiming to make web requests faster and more reliable.
// @author             PY-DNG
// @license            GPL-v3
// @grant              none
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_xmlhttpRequest
// ==/UserScript==




// Loads web resources and saves them to GM-storage
// Tries to load web resources from GM-storage in subsequent calls
// Updates resources every $(this.expire) hours, or use $(this.refresh) function to update all resources instantly
// Dependencies: GM_getValue(), GM_setValue(), requestText(), AsyncManager()
function LocalCDN(expire=72) {
	// Arguments: level=LogLevel.Info, logContent, asObject=false
	// Needs one call "DoLog();" to get it initialized before using it!
	function DoLog() {
		// Get window
		const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;

		// Global log levels set
		win.LogLevel = {
			None: 0,
			Error: 1,
			Success: 2,
			Warning: 3,
			Info: 4,
		}
		win.LogLevelMap = {};
		win.LogLevelMap[LogLevel.None] = {
			prefix: '',
			color: 'color:#ffffff'
		}
		win.LogLevelMap[LogLevel.Error] = {
			prefix: '[Error]',
			color: 'color:#ff0000'
		}
		win.LogLevelMap[LogLevel.Success] = {
			prefix: '[Success]',
			color: 'color:#00aa00'
		}
		win.LogLevelMap[LogLevel.Warning] = {
			prefix: '[Warning]',
			color: 'color:#ffa500'
		}
		win.LogLevelMap[LogLevel.Info] = {
			prefix: '[Info]',
			color: 'color:#888888'
		}
		win.LogLevelMap[LogLevel.Elements] = {
			prefix: '[Elements]',
			color: 'color:#000000'
		}

		// Current log level
		DoLog.logLevel = win.isPY_DNG ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error

		// Log counter
		DoLog.logCount === undefined && (DoLog.logCount = 0);

		// Get args
		let level, logContent, asObject;
		switch (arguments.length) {
			case 1:
				level = LogLevel.Info;
				logContent = arguments[0];
				asObject = false;
				break;
			case 2:
				level = arguments[0];
				logContent = arguments[1];
				asObject = false;
				break;
			case 3:
				level = arguments[0];
				logContent = arguments[1];
				asObject = arguments[2];
				break;
			default:
				level = LogLevel.Info;
				logContent = 'DoLog initialized.';
				asObject = false;
				break;
		}

		// Log when log level permits
		if (level <= DoLog.logLevel) {
			let msg = '%c' + LogLevelMap[level].prefix;
			let subst = LogLevelMap[level].color;

			if (asObject) {
				msg += ' %o';
			} else {
				switch (typeof(logContent)) {
					case 'string':
						msg += ' %s';
						break;
					case 'number':
						msg += ' %d';
						break;
					case 'object':
						msg += ' %o';
						break;
				}
			}

			if (++DoLog.logCount > 512) {
				console.clear();
				DoLog.logCount = 0;
			}
			console.log(msg, subst, logContent);
		}
	}
	DoLog();

	const LC = this;
	const _GM_getValue = GM_getValue;
	const _GM_setValue = GM_setValue;

	const KEY_LOCALCDN = 'LOCAL-CDN';
	const KEY_LOCALCDN_VERSION = 'version';
	const VALUE_LOCALCDN_VERSION = '0.3';

	// Default expire time (by hour)
	LC.expire = expire;

	// Try to get resource content from loaclCDN first, if failed/timeout, request from web && save to LocalCDN
	// Accepts callback only: onload & onfail(optional)
	// Returns true if got from LocalCDN, false if got from web
	LC.get = function(url, onload, args=[], onfail=function(){}) {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		const resource = CDN[url];
		const time = (new Date()).getTime();

		if (resource && resource.content !== null && !expired(time, resource.time)) {
			onload.apply(null, [resource.content].concat(args));
			return true;
		} else {
			LC.request(url, _onload, [], onfail);
			return false;
		}

		function _onload(content) {
			onload.apply(null, [content].concat(args));
		}
	}

	// Generate resource obj and set to CDN[url]
	// Returns resource obj
	// Provide content means load success, provide null as content means load failed
	LC.set = function(url, content) {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		const time = (new Date()).getTime();
		const resource = {
			url: url,
			time: time,
			content: content,
			success: content !== null ? (CDN[url] ? CDN[url].success + 1 : 1) : (CDN[url] ? CDN[url].success : 0),
			fail: content === null ? (CDN[url] ? CDN[url].fail + 1 : 1) : (CDN[url] ? CDN[url].fail : 0),
		};
		CDN[url] = resource;
		_GM_setValue(KEY_LOCALCDN, CDN);
		return resource;
	}

	// Delete one resource from LocalCDN
	LC.delete = function(url) {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		if (!CDN[url]) {
			return false;
		} else {
			delete CDN[url];
			_GM_setValue(KEY_LOCALCDN, CDN);
			return true;
		}
	}

	// Delete all resources in LocalCDN
	LC.clear = function() {
		_GM_setValue(KEY_LOCALCDN, {});
		upgradeConfig();
	}

	// List all resource saved in LocalCDN
	LC.list = function() {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		const urls = LC.listurls();
		return LC.listurls().map((url) => (CDN[url]));
	}

	// List all resource's url saved in LocalCDN
	LC.listurls = function() {
		return Object.keys(_GM_getValue(KEY_LOCALCDN, {})).filter((url) => (url !== KEY_LOCALCDN_VERSION));
	}

	// Request content from web and save it to CDN[url]
	// Accepts callbacks only: onload & onfail(optional)
	LC.request = function(url, onload, args=[], onfail=function(){}) {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		requestText(url, _onload, [], _onfail);

		function _onload(content) {
			LC.set(url, content);
			onload.apply(null, [content].concat(args));
		}

		function _onfail() {
			LC.set(url, null);
			onfail(url);
		}
	}

	// Re-request all resources in CDN instantly, ignoring LC.expire
	LC.refresh = function(callback, args=[]) {
		const urls = LC.listurls();

		const AM = new AsyncManager();
		AM.onfinish = function() {
			callback.apply(null, [].concat(args))
		};

		for (const url of urls) {
			AM.add();
			LC.request(url, function() {
				AM.finish();
			});
		}

		AM.finishEvent = true;
	}

	// Sort src && srcset, to get a best request sorting
	LC.sort = function(srcset) {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		const result = {srclist: [], lists: []};
		const lists = result.lists;
		const srclist = result.srclist;
		const suc_rec = lists[0] = []; // Recent successes take second (not expired yet)
		const suc_old = lists[1] = []; // Old successes take third
		const fails   = lists[2] = []; // Fails & unused take the last place
		const time = (new Date()).getTime();

		// Make lists
		for (const s of srcset) {
			const resource = CDN[s];
			if (resource && resource.content !== null) {
				if (!expired(resource.time, time)) {
					suc_rec.push(s);
				} else {
					suc_old.push(s);
				}
			} else {
				fails.push(s);
			}
		}

		// Sort lists
		// Recently successed: Choose most recent ones
		suc_rec.sort((res1, res2) => (res2.time - res1.time));
		// Successed long ago or failed: Sort by success rate & tried time
		[suc_old, fails].forEach((arr) => (arr.sort(sorting)));

		// Push all resources into seclist
		[suc_rec, suc_old, fails].forEach((arr) => (arr.forEach((res) => (srclist.push(res)))));

		return result;

		function sorting(res1, res2) {
			const sucRate1 = (res1.success+1) / (res1.fail+1);
			const sucRate2 = (res2.success+1) / (res2.fail+1);

			if (sucRate1 !== sucRate2) {
				// Success rate: high to low
				return sucRate2 - sucRate1;
			} else {
				// Tried time: less to more
				// Less tried time means newer added source
				return (res1.success+res1.fail) - (res2.success+res2.fail);
			}
		}
	}

	function upgradeConfig() {
		const CDN = _GM_getValue(KEY_LOCALCDN, {});
		switch(CDN[KEY_LOCALCDN_VERSION]) {
			case undefined:
				init();
				break;
			case '0.1':
				v01_To_v02();
				logUpgrade();
				break;
			case '0.2':
				v01_To_v02();
				v02_To_v03();
				logUpgrade();
				break;
			case VALUE_LOCALCDN_VERSION:
				DoLog('LocalCDN is in latest version.');
				break;
			default:
				DoLog(LogLevel.Error, 'LocalCDN.upgradeConfig: Invalid config version({V}) for LocalCDN. '.replace('{V}', CDN[KEY_LOCALCDN_VERSION]));
		}
		CDN[KEY_LOCALCDN_VERSION] = VALUE_LOCALCDN_VERSION;
		_GM_setValue(KEY_LOCALCDN, CDN);

		function logUpgrade() {
			DoLog(LogLevel.Success, 'LocalCDN successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', CDN[KEY_LOCALCDN_VERSION]).replaceAll('{V2}', VALUE_LOCALCDN_VERSION));
		}

		function init() {
			// Nothing to do here
		}

		function v01_To_v02() {
			const urls = LC.listurls();
			for (const url of urls) {
				if (url === KEY_LOCALCDN_VERSION) {continue;}
				CDN[url] = {
					url: url,
					time: 0,
					content: CDN[url]
				};
			}
		}

		function v02_To_v03() {
			const urls = LC.listurls();
			for (const url of urls) {
				CDN[url].success = CDN[url].fail = 0;
			}
		}
	}

	function clearExpired() {
		const resources = LC.list();
		const time = (new Date()).getTime();

		for (const resource of resources) {
			expired(resource.time, time) && LC.delete(resource.url);
		}
	}

	function expired(t1, t2) {
		return (t1 - t2) > (LC.expire * 60 * 60 * 1000);
	}

	upgradeConfig();
	clearExpired();


	function requestText(url, callback, args=[], onfail=function(){}) {
		GM_xmlhttpRequest({
			method:       'GET',
			url:          url,
			responseType: 'text',
			timeout:      45*1000,
			onload:       function(response) {
				const text = response.responseText;
				const argvs = [text].concat(args);
				callback.apply(null, argvs);
			},
			onerror:      onfail,
			ontimeout:    onfail,
			onabort:      onfail,
		})
	}

	function AsyncManager() {
		const AM = this;

		// Ongoing xhr count
		this.taskCount = 0;

		// Whether generate finish events
		let finishEvent = false;
		Object.defineProperty(this, 'finishEvent', {
			configurable: true,
			enumerable: true,
			get: () => (finishEvent),
			set: (b) => {
				finishEvent = b;
				b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
			}
		});

		// Add one task
		this.add = () => (++AM.taskCount);

		// Finish one task
		this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
	}
}