/** * This file is part of Haketilo. * * Function: Facilitate broadcasting messages between different execution * contexts of the extension * * Copyright (C) 2021 Wojtek Kosior * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * As additional permission under GNU GPL version 3 section 7, you * may distribute forms of that code without the copy of the GNU * GPL normally required by section 4, provided you include this * license notice and, in case of non-source distribution, a URL * through which recipients can access the Corresponding Source. * If you modify file(s) with this exception, you may extend this * exception to your version of the file(s), but you are not * obligated to do so. If you do not wish to do so, delete this * exception statement from your version. * * As a special exception to the GPL, any HTML file which merely * makes function calls to this code, and for that purpose * includes it by reference shall be deemed a separate work for * copyright law purposes. If you modify this code, you may extend * this exception to your version of the code, but you are not * obligated to do so. If you do not wish to do so, delete this * exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * I, Wojtek Kosior, thereby promise not to sue for violation of this file's * license. Although I request that you do not make use of this code in a * proprietary program, I am not going to enforce this in court. */ #FROM common/message_server.js IMPORT listen_for_connection let next_id = 1; const listeners_by_channel = new Map(); function new_broadcast_listener(port) { const listener_ctx = {port, id: ++next_id, channels: new Set()}; port.onMessage.addListener(msg => listener_command(msg, listener_ctx)); const on_disconnect = () => remove_broadcast_listener(listener_ctx); port.onDisconnect.addListener(on_disconnect); } function listener_command(msg, listener_ctx) { const [disposition, name] = msg; if (disposition === "subscribe") subscribe_channel(name, listener_ctx); else if (disposition === "unsubscribe") unsubscribe_channel(name, listener_ctx); else throw `bad broadcast listener disposition '${disposition}'`; } function subscribe_channel(channel_name, listener_ctx) { if (!listeners_by_channel.has(channel_name)) listeners_by_channel.set(channel_name, new Map()); listeners_by_channel.get(channel_name).set(listener_ctx.id, listener_ctx); listener_ctx.channels.add(channel_name); } function unsubscribe_channel(channel_name, listener_ctx) { const channel_listeners = listeners_by_channel.get(channel_name) || new Map(); channel_listeners.delete(listener_ctx.id); if (channel_listeners.size == 0) listeners_by_channel.delete(channel_name); listener_ctx.channels.delete(channel_name); } function remove_broadcast_listener(listener_ctx) { for (const channel_name of [...listener_ctx.channels.keys()]) unsubscribe_channel(channel_name, listener_ctx); } function new_broadcast_sender(port) { const sender_ctx = {prepared_broadcasts: new Set()}; port.onMessage.addListener(msg => sender_command(msg, sender_ctx)); port.onDisconnect.addListener(msg => flush(sender_ctx)); } function sender_command(msg, sender_ctx) { const [disposition, name, value, timeout] = msg; if (disposition === "prepare") prepare(sender_ctx, name, value, timeout) else if (disposition === "discard") sender_ctx.prepared_broadcasts = new Set(); else if (disposition === "flush") flush(sender_ctx); else if (disposition === "broadcast") broadcast(name, value); else throw `bad broadcast sender disposition '${disposition}'`; } function prepare(sender_ctx, channel_name, value, timeout) { const broadcast_data = [channel_name, value]; sender_ctx.prepared_broadcasts.add(broadcast_data); if (timeout === 0) return; setTimeout(() => prepare_timeout_cb(sender_ctx, broadcast_data), timeout); } function prepare_timeout_cb(sender_ctx, broadcast_data) { if (sender_ctx.prepared_broadcasts.has(broadcast_data)) { sender_ctx.prepared_broadcasts.delete(broadcast_data); broadcast(...broadcast_data); } } function flush(sender_ctx) { sender_ctx.prepared_broadcasts.forEach(nv => broadcast(...nv)); sender_ctx.prepared_broadcasts = new Set(); } function broadcast(channel_name, value) { const listeners = listeners_by_channel.get(channel_name); if (listeners == undefined) return; for (const listener_ctx of [...listeners.values()]) { try { listener_ctx.port.postMessage([channel_name, value]); } catch (e) { console.error("Haketilo:", e); remove_broadcast_listener(listener_ctx); } } } function remove_broadcast_sender(sender_ctx) { sender_ctx.prepared_broadcasts.forEach(nv => broadcast(...nv)); } function start() { listen_for_connection("broadcast_send", new_broadcast_sender); listen_for_connection("broadcast_listen", new_broadcast_listener); } #EXPORT start