A userscript library for cross-window communication via the userscript storage
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/470418/1217207/CommLinkjs.js
/* CommLink.js - Version: 1.0.1 - Author: Haka - Description: A userscript library for cross-window communication via the userscript storage - GitHub: https://github.com/AugmentedWeb/CommLink */ class CommLinkHandler { constructor(commlinkID, configObj) { this.commlinkID = commlinkID; this.singlePacketResponseWaitTime = configObj?.singlePacketResponseWaitTime || 1500; this.maxSendAttempts = configObj?.maxSendAttempts || 3; this.statusCheckInterval = configObj?.statusCheckInterval || 1; this.silentMode = configObj?.silentMode || false; this.commlinkValueIndicator = 'commlink-packet-'; this.commands = {}; this.listeners = []; const missingGrants = ['GM_getValue', 'GM_setValue', 'GM_deleteValue', 'GM_listValues'] .filter(grant => !GM_info.script.grant.includes(grant)); if(missingGrants.length > 0 && !this.silentMode) { alert(`[CommLink] The following userscript grants are missing: ${missingGrants.join(', ')}. CommLink will not work.`); } this.getStoredPackets() .filter(packet => Date.now() - packet.date > 2e4) .forEach(packet => this.removePacketByID(packet.id)); } setIntervalAsync(callback, interval = this.statusCheckInterval) { let running = true; async function loop() { while(running) { try { await callback(); await new Promise((resolve) => setTimeout(resolve, interval)); } catch (e) { continue; } } }; loop(); return { stop: () => running = false }; } getUniqueID() { return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ) } getCommKey(packetID) { return this.commlinkValueIndicator + packetID; } getStoredPackets() { return GM_listValues() .filter(key => key.includes(this.commlinkValueIndicator)) .map(key => GM_getValue(key)); } addPacket(packet) { GM_setValue(this.getCommKey(packet.id), packet); } removePacketByID(packetID) { GM_deleteValue(this.getCommKey(packetID)); } findPacketByID(packetID) { return GM_getValue(this.getCommKey(packetID)); } editPacket(newPacket) { GM_setValue(this.getCommKey(newPacket.id), newPacket); } send(platform, cmd, d) { return new Promise(async resolve => { const packetWaitTimeMs = this.singlePacketResponseWaitTime; const maxAttempts = this.maxSendAttempts; let attempts = 0; for (;;) { attempts++; const packetID = this.getUniqueID(); const attemptStartDate = Date.now(); const packet = { sender: platform, id: packetID, command: cmd, data: d, date: attemptStartDate }; if(!this.silentMode) console.log(`[CommLink Sender] Sending packet! (#${attempts} attempt):`, packet); this.addPacket(packet); for (;;) { const poolPacket = this.findPacketByID(packetID); const packetResult = poolPacket?.result; if (poolPacket && packetResult) { if(!this.silentMode) console.log(`[CommLink Sender] Got result for a packet (${packetID}):`, packetResult); resolve(poolPacket.result); attempts = maxAttempts; // stop main loop break; } if (!poolPacket || Date.now() - attemptStartDate > packetWaitTimeMs) { break; } await new Promise(res => setTimeout(res, this.statusCheckInterval)); } this.removePacketByID(packetID); if (attempts == maxAttempts) { break; } } return resolve(null); }); } registerSendCommand(name, obj) { this.commands[name] = async data => await this.send(obj?.commlinkID || this.commlinkID , name, obj?.data || data); } registerListener(sender, commandHandler) { const listener = { sender, commandHandler, intervalObj: this.setIntervalAsync(this.receivePackets.bind(this), this.statusCheckInterval), }; this.listeners.push(listener); } receivePackets() { this.getStoredPackets().forEach(packet => { this.listeners.forEach(listener => { if(packet.sender === listener.sender && !packet.hasOwnProperty('result')) { const result = listener.commandHandler(packet); packet.result = result; this.editPacket(packet); if(!this.silentMode) { if(packet.result == null) console.log('[CommLink Receiver] Possibly failed to handle packet:', packet); else console.log('[CommLink Receiver] Successfully handled a packet:', packet); } } }); }); } kill() { this.listeners.forEach(listener => listener.intervalObj.stop()); } }