🏠 

CommLink.js

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());
}
}