CommLink.js

A userscript library for cross-window communication via the userscript storage

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/470418/1582512/CommLinkjs.js

  1. /* CommLink.js
  2. - Version: 1.0.3
  3. - Author: Haka
  4. - Description: A userscript library for cross-window communication via the userscript storage
  5. - GitHub: https://github.com/AugmentedWeb/CommLink
  6. */
  7.  
  8. class CommLinkHandler {
  9. constructor(commlinkID, configObj) {
  10. this.commlinkID = commlinkID;
  11. this.singlePacketResponseWaitTime = configObj?.singlePacketResponseWaitTime || 1500;
  12. this.maxSendAttempts = configObj?.maxSendAttempts || 3;
  13. this.statusCheckInterval = configObj?.statusCheckInterval || 1;
  14. this.silentMode = configObj?.silentMode || false;
  15.  
  16. this.commlinkValueIndicator = 'commlink-packet-';
  17. this.commands = {};
  18. this.listeners = [];
  19.  
  20. this.greasy = typeof GM === 'object' ? GM : {};
  21.  
  22. const getFunction = (funcNames, methodName) => {
  23. for(const func of funcNames) {
  24. if(typeof func === 'function') {
  25. return func;
  26. }
  27. }
  28. if(!this.silentMode) {
  29. throw new Error(`No valid method found for ${methodName}`);
  30. }
  31. };
  32. const getValueMethod = getFunction(
  33. [
  34. typeof GM_getValue !== 'undefined' ? GM_getValue : undefined,
  35. this.greasy?.getValue,
  36. configObj?.functions?.getValue
  37. ],
  38. 'getValue'
  39. );
  40.  
  41. const setValueMethod = getFunction(
  42. [
  43. typeof GM_setValue !== 'undefined' ? GM_setValue : undefined,
  44. this.greasy?.setValue,
  45. configObj?.functions?.setValue
  46. ],
  47. 'setValue'
  48. );
  49.  
  50. const deleteValueMethod = getFunction(
  51. [
  52. typeof GM_deleteValue !== 'undefined' ? GM_deleteValue : undefined,
  53. this.greasy?.deleteValue,
  54. configObj?.functions?.deleteValue
  55. ],
  56. 'deleteValue'
  57. );
  58.  
  59. const listValuesMethod = getFunction(
  60. [
  61. typeof GM_listValues !== 'undefined' ? GM_listValues : undefined,
  62. this.greasy?.listValues,
  63. configObj?.functions?.listValues
  64. ],
  65. 'listValues'
  66. );
  67. this.storage = {
  68. getValue: async (key) => {
  69. return await getValueMethod(key);
  70. },
  71. setValue: (key, value) => {
  72. return setValueMethod(key, value);
  73. },
  74. deleteValue: (key) => {
  75. return deleteValueMethod(key);
  76. },
  77. listValues: async () => {
  78. return await listValuesMethod();
  79. }
  80. };
  81.  
  82. if(typeof GM_info !== 'undefined') {
  83. const grants = (GM_info?.script?.grant) || [];
  84. const missingGrants = ['getValue', 'setValue', 'deleteValue', 'listValues']
  85. .filter(grant => !grants.some(g => g.endsWith(grant)));
  86. if(missingGrants.length > 0 && !this.silentMode)
  87. alert(`[CommLink] The following userscript grants are missing: ${missingGrants.join(', ')}. CommLink might not work.`);
  88. }
  89.  
  90. this.removeOldPackets();
  91. }
  92.  
  93. async removeOldPackets() {
  94. const packets = await this.getStoredPackets();
  95.  
  96. packets.filter(packet => Date.now() - packet?.date > 2e4)
  97. .forEach(packet => this.removePacketByID(packet.id));
  98. }
  99.  
  100. setIntervalAsync(callback, interval = this.statusCheckInterval) {
  101. let running = true;
  102.  
  103. async function loop() {
  104. while(running) {
  105. try {
  106. await callback();
  107.  
  108. await new Promise((resolve) => setTimeout(resolve, interval));
  109. } catch (e) {
  110. continue;
  111. }
  112. }
  113. };
  114.  
  115. loop();
  116.  
  117. return { stop: () => running = false };
  118. }
  119.  
  120. getUniqueID() {
  121. return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c =>
  122. (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  123. )
  124. }
  125.  
  126. getCommKey(packetID) {
  127. return this.commlinkValueIndicator + packetID;
  128. }
  129.  
  130. async getStoredPackets() {
  131. const keys = await this.storage.listValues();
  132. const storedPackets = [];
  133. for(const key of keys) {
  134. if(key.includes(this.commlinkValueIndicator)) {
  135. const value = await this.storage.getValue(key);
  136. storedPackets.push(value);
  137. }
  138. }
  139. return storedPackets;
  140. }
  141. addPacket(packet) {
  142. this.storage.setValue(this.getCommKey(packet.id), packet);
  143. }
  144.  
  145. removePacketByID(packetID) {
  146. this.storage.deleteValue(this.getCommKey(packetID));
  147. }
  148.  
  149. async findPacketByID(packetID) {
  150. return await this.storage.getValue(this.getCommKey(packetID));
  151. }
  152.  
  153. editPacket(newPacket) {
  154. this.storage.setValue(this.getCommKey(newPacket.id), newPacket);
  155. }
  156.  
  157. send(platform, cmd, d) {
  158. return new Promise(async resolve => {
  159. const packetWaitTimeMs = this.singlePacketResponseWaitTime;
  160. const maxAttempts = this.maxSendAttempts;
  161.  
  162. let attempts = 0;
  163.  
  164. for(;;) {
  165. attempts++;
  166.  
  167. const packetID = this.getUniqueID();
  168. const attemptStartDate = Date.now();
  169.  
  170. const packet = { sender: platform, id: packetID, command: cmd, data: d, date: attemptStartDate };
  171.  
  172. if(!this.silentMode)
  173. console.log(`[CommLink Sender] Sending packet! (#${attempts} attempt):`, packet);
  174.  
  175. this.addPacket(packet);
  176.  
  177. for(;;) {
  178. const poolPacket = await this.findPacketByID(packetID);
  179. const packetResult = poolPacket?.result;
  180.  
  181. if(poolPacket && packetResult) {
  182. if(!this.silentMode)
  183. console.log(`[CommLink Sender] Got result for a packet (${packetID}):`, packetResult);
  184.  
  185. resolve(poolPacket.result);
  186.  
  187. attempts = maxAttempts; // stop main loop
  188.  
  189. break;
  190. }
  191.  
  192. if(!poolPacket || Date.now() - attemptStartDate > packetWaitTimeMs) {
  193. break;
  194. }
  195.  
  196. await new Promise(res => setTimeout(res, this.statusCheckInterval));
  197. }
  198.  
  199. this.removePacketByID(packetID);
  200.  
  201. if(attempts == maxAttempts) {
  202. break;
  203. }
  204. }
  205.  
  206. return resolve(null);
  207. });
  208. }
  209.  
  210. registerSendCommand(name, obj) {
  211. this.commands[name] = async data => await this.send(obj?.commlinkID || this.commlinkID , name, obj?.data || data);
  212. }
  213.  
  214. registerListener(sender, commandHandler) {
  215. const listener = {
  216. sender,
  217. commandHandler,
  218. intervalObj: this.setIntervalAsync(async () => {
  219. await this.receivePackets();
  220. }, this.statusCheckInterval),
  221. };
  222. this.listeners.push(listener);
  223. }
  224.  
  225. async receivePackets() {
  226. const packets = await this.getStoredPackets();
  227.  
  228. for(const packet of packets) {
  229. for(const listener of this.listeners) {
  230. if(packet.sender === listener.sender && !packet.hasOwnProperty('result')) {
  231. try {
  232. const result = await listener.commandHandler(packet);
  233. packet.result = result;
  234.  
  235. this.editPacket(packet);
  236. if(!this.silentMode) {
  237. if(packet.result == null)
  238. console.log('[CommLink Receiver] Possibly failed to handle packet:', packet);
  239. else
  240. console.log('[CommLink Receiver] Successfully handled a packet:', packet);
  241. }
  242. } catch(error) {
  243. console.error('[CommLink Receiver] Error handling packet:', error);
  244. }
  245. }
  246. }
  247. }
  248. }
  249.  
  250. kill() {
  251. this.listeners.forEach(listener => listener.intervalObj.stop());
  252. }
  253. }