Jsdelivr Auto Fallback

修复 cdn.jsdelivr.net 无法访问的问题

目前为 2022-05-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Jsdelivr Auto Fallback
  3. // @namespace https://github.com/PipecraftNet/jsdelivr-auto-fallback
  4. // @version 0.2.0
  5. // @author PipecraftNet&DreamOfIce
  6. // @description 修复 cdn.jsdelivr.net 无法访问的问题
  7. // @homepage https://github.com//jsdelivr-auto-fallback
  8. // @supportURL https://github.com/PipecraftNet/jsdelivr-auto-fallback/issues
  9. // @license MIT
  10. // @match *
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. ((document) => {
  15. 'use strict';
  16. let fastNode;
  17. let failed;
  18. let isRunning;
  19. const DEST_LIST = [
  20. 'cdn.jsdelivr.net',
  21. 'fastly.jsdelivr.net',
  22. 'gcore.jsdelivr.net',
  23. 'testingcf.jsdelivr.net',
  24. 'test1.jsdelivr.net'
  25. ];
  26. const PREFIX = '//';
  27. const SOURCE = DEST_LIST[0];
  28. const starTime = Date.now();
  29. const TIMEOUT = 2000;
  30. const STORE_KEY = 'jsdelivr-auto-fallback';
  31. const TEST_PATH = '/gh/PipecraftNet/jsdelivr-auto-fallback@main/empty.css?';
  32. const shouldReplace = (text) => text && text.includes(PREFIX + SOURCE);
  33. const replace = (text) => text.replace(PREFIX + SOURCE, PREFIX + fastNode);
  34. const setTimeout = window.setTimeout;
  35. const $ = document.querySelectorAll.bind(document);
  36.  
  37. const replaceElementSrc = () => {
  38. let element;
  39. let value;
  40. for (element of $('link[rel="stylesheet"]')) {
  41. value = element.href;
  42. if (shouldReplace(value) && !value.includes(TEST_PATH)) {
  43. element.href = replace(value);
  44. }
  45. }
  46.  
  47. for (element of $('script')) {
  48. value = element.src;
  49. if (shouldReplace(value)) {
  50. const newNode = document.createElement('script');
  51. newNode.src = replace(value);
  52. element.defer = true;
  53. element.src = '';
  54. element.before(newNode);
  55. element.remove();
  56. }
  57. }
  58.  
  59. for (element of $('img')) {
  60. value = element.src;
  61. if (shouldReplace(value)) {
  62. // Used to cancel loading. Without this line it will remain pending status.
  63. element.src = '';
  64. element.src = replace(value);
  65. }
  66. }
  67.  
  68. // All elements that have a style attribute
  69. for (element of $('*[style]')) {
  70. value = element.getAttribute('style');
  71. if (shouldReplace(value)) {
  72. element.setAttribute('style', replace(value));
  73. }
  74. }
  75.  
  76. for (element of $('style')) {
  77. value = element.innerHTML;
  78. if (shouldReplace(value)) {
  79. element.innerHTML = replace(value);
  80. }
  81. }
  82. };
  83.  
  84. const tryReplace = () => {
  85. if (!isRunning && failed && fastNode) {
  86. console.warn(SOURCE + ' is not available. Use ' + fastNode);
  87. isRunning = true;
  88. setTimeout(replaceElementSrc, 0);
  89. // Some need to wait for a while
  90. setTimeout(replaceElementSrc, 20);
  91. // Replace dynamically added elements
  92. setInterval(replaceElementSrc, 500);
  93. }
  94. };
  95.  
  96. const checkAvailable = (url, callback) => {
  97. let timeoutId;
  98. const newNode = document.createElement('link');
  99. const handleResult = (isSuccess) => {
  100. if (!timeoutId) {
  101. return;
  102. }
  103.  
  104. clearTimeout(timeoutId);
  105. timeoutId = 0;
  106. // Used to cancel loading. Without this line it will remain pending status.
  107. if (!isSuccess) newNode.href = 'data:text/plain;base64,';
  108. newNode.remove();
  109. callback(isSuccess);
  110. };
  111.  
  112. timeoutId = setTimeout(handleResult, TIMEOUT);
  113.  
  114. newNode.addEventListener('error', () => handleResult(false));
  115. newNode.addEventListener('load', () => handleResult(true));
  116. newNode.rel = 'stylesheet';
  117. newNode.text = 'text/css';
  118. newNode.href = url + TEST_PATH + starTime;
  119. document.head.insertAdjacentElement('afterbegin', newNode);
  120. };
  121.  
  122. const cached = (() => {
  123. try {
  124. return Object.assign(
  125. {},
  126. JSON.parse(GM_getValue(STORE_KEY) || '{}')
  127. );
  128. } catch {
  129. return {};
  130. }
  131. })();
  132.  
  133. const main = () => {
  134. cached.time = starTime;
  135. cached.failed = false;
  136. cached.fastNode = null;
  137.  
  138. for (const url of DEST_LIST) {
  139. checkAvailable('https://' + url, (isAvailable) => {
  140. // console.log(url, Date.now() - starTime, Boolean(isAvailable));
  141. if (!isAvailable && url === SOURCE) {
  142. failed = true;
  143. cached.failed = true;
  144. }
  145.  
  146. if (isAvailable && !fastNode) {
  147. fastNode = url;
  148. }
  149.  
  150. if (isAvailable && !cached.fastNode) {
  151. cached.fastNode = url;
  152. }
  153.  
  154. tryReplace();
  155. });
  156. }
  157.  
  158. setTimeout(() => {
  159. // If all domains are timeout
  160. if (failed && !fastNode) {
  161. fastNode = DEST_LIST[1];
  162. tryReplace();
  163. }
  164.  
  165. GM_setValue(STORE_KEY, JSON.stringify(cached));
  166. }, TIMEOUT + 100);
  167. };
  168.  
  169. if (
  170. cached.time &&
  171. starTime - cached.time < 60 * 60 * 1000 &&
  172. cached.failed &&
  173. cached.fastNode
  174. ) {
  175. failed = true;
  176. fastNode = cached.fastNode;
  177. tryReplace();
  178. setTimeout(main, 1000);
  179. } else {
  180. main();
  181. }
  182. })(document);