Files
tubestation/waterfox/browser/components/sidebar/common/sidebar-connection.js
2025-11-06 14:13:52 +00:00

306 lines
9.4 KiB
JavaScript

/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
mapAndFilterUniq,
configs,
wait,
} from './common.js';
import * as Constants from './constants.js';
import * as TabsStore from './tabs-store.js';
function log(...args) {
internalLogger('common/sidebar-connection', ...args);
}
export const onMessage = new EventListenerManager();
export const onConnected = new EventListenerManager();
export const onDisconnected = new EventListenerManager();
const mConnections = new Map();
const mReceivers = new Map();
const mFocusState = new Map();
let mIsListening = false;
export function isInitialized() {
return mIsListening;
}
export function isSidebarOpen(windowId) {
if (!mIsListening ||
Constants.IS_SIDEBAR)
return false;
// for automated tests
if (configs.sidebarVirtuallyOpenedWindows.length > 0 &&
configs.sidebarVirtuallyOpenedWindows.includes(windowId))
return true;
if (configs.sidebarVirtuallyClosedWindows.length > 0 &&
configs.sidebarVirtuallyClosedWindows.includes(windowId))
return false;
if (windowId in configs.sidebarWidthInWindow &&
configs.sidebarWidthInWindow[windowId] == 0)
return false;
const connections = mConnections.get(windowId);
if (!connections)
return false;
for (const connection of connections) {
if (connection.type == 'sidebar')
return true;
}
return false;
}
export function isOpen(windowId) {
if (!mIsListening ||
Constants.IS_SIDEBAR)
return false;
// for automated tests
if (configs.sidebarVirtuallyOpenedWindows.length > 0 &&
configs.sidebarVirtuallyOpenedWindows.includes(windowId))
return true;
if (configs.sidebarVirtuallyClosedWindows.length > 0 &&
configs.sidebarVirtuallyClosedWindows.includes(windowId))
return false;
if (windowId in configs.sidebarWidthInWindow &&
configs.sidebarWidthInWindow[windowId] == 0)
return false;
const connections = mConnections.get(windowId);
return connections && connections.size > 0;
}
export function hasFocus(windowId) {
return mFocusState.has(windowId)
}
export const counts = {
broadcast: {}
};
export function getOpenWindowIds() {
return mIsListening ? Array.from(mConnections.keys()) : [];
}
export function sendMessage(message) {
if (!mIsListening ||
Constants.IS_SIDEBAR)
return false;
if (Array.isArray(message))
log('Sending ', message.length, ' messages');
if (message.windowId) {
if (configs.loggingConnectionMessages) {
counts[message.windowId] = counts[message.windowId] || {};
const localCounts = counts[message.windowId];
localCounts[message.type] = localCounts[message.type] || 0;
localCounts[message.type]++;
}
const connections = mConnections.get(message.windowId);
if (!connections || connections.size == 0)
return false;
for (const connection of connections) {
sendMessageToPort(connection.port, message);
}
//port.postMessage(message);
return true;
}
// broadcast
counts.broadcast[message.type] = counts.broadcast[message.type] || 0;
counts.broadcast[message.type]++;
for (const connections of mConnections.values()) {
if (!connections || connections.size == 0)
continue;
for (const connection of connections) {
sendMessageToPort(connection.port, message);
}
//port.postMessage(message);
}
return true;
}
const mReservedTasks = new WeakMap();
// Se should not send messages immediately, instead we should throttle
// it and bulk-send multiple messages, for better user experience.
// Sending too much messages in one event loop may block everything
// and makes Firefox like frozen.
function sendMessageToPort(port, message) {
const task = mReservedTasks.get(port) || { messages: [] };
task.messages.push(message);
mReservedTasks.set(port, task);
if (!task.onFrame) {
task.onFrame = () => {
delete task.onFrame;
const messages = task.messages;
task.messages = [];
port.postMessage(messages);
if (configs.debug) {
const types = mapAndFilterUniq(messages,
message => message.type || undefined).join(', ');
log(`${messages.length} messages sent (${types}):`, messages);
}
};
// We should not use window.requestAnimationFrame for throttling,
// because it is quite lagged on some environment. Firefox may
// decelerate the method for an invisible document (the background
// page).
//window.requestAnimationFrame(task.onFrame);
setTimeout(task.onFrame, 0);
}
}
if (Constants.IS_BACKGROUND) {
const matcher = new RegExp(`^${Constants.kCOMMAND_REQUEST_CONNECT_PREFIX}([0-9]+):(.+)$`);
browser.runtime.onConnect.addListener(port => {
if (!mIsListening ||
!matcher.test(port.name))
return;
const windowId = parseInt(RegExp.$1);
const type = RegExp.$2;
const connection = { port, type };
const connections = mConnections.get(windowId) || new Set();
connections.add(connection);
mConnections.set(windowId, connections);
let connectionTimeoutTimer = null;
const updateTimeoutTimer = () => {
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
// On slow situation (like having too many tabs - 5000 or more)
// we should wait more for the pong. Otherwise the vital check
// may produce needless reloadings even if the sidebar is still alive.
// See also https://github.com/piroor/treestyletab/issues/3130
const timeout = configs.heartbeatInterval + Math.max(configs.connectionTimeoutDelay, TabsStore.tabs.size);
connectionTimeoutTimer = setTimeout(async () => {
log(`Missing heartbeat from window ${windowId}. Maybe disconnected or resumed.`);
try {
const pong = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_PING_TO_SIDEBAR,
windowId
});
if (pong) {
log(`Sidebar for the window ${windowId} responded. Keep connected.`);
return;
}
}
catch(_error) {
}
log(`Sidebar for the window ${windowId} did not respond. Disconnect now.`);
cleanup(); // eslint-disable-line no-use-before-define
port.disconnect();
}, timeout);
};
const cleanup = _diconnectedPort => {
if (!port.onMessage.hasListener(receiver)) // eslint-disable-line no-use-before-define
return;
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
connections.delete(connection);
if (connections.size == 0) {
mConnections.delete(windowId);
const sidebarWidthInWindow = { ...configs.sidebarWidthInWindow };
delete sidebarWidthInWindow[windowId];
configs.sidebarWidthInWindow = sidebarWidthInWindow;
}
port.onMessage.removeListener(receiver); // eslint-disable-line no-use-before-define
mReceivers.delete(windowId);
mFocusState.delete(windowId);
onDisconnected.dispatch(windowId, connections.size);
// We need to notify this to some conetnt scripts, to destroy themselves.
/*
browser.runtime.sendMessage({
type: Constants.kCOMMAND_NOTIFY_SIDEBAR_CLOSED,
windowId,
});
*/
browser.windows.get(windowId, { populate: true }).then(async win => {
let count = 0;
for (const tab of win.tabs) {
count++;
if (count >= 20) {
// We should not block too long seconds on too much tabs case.
await wait(10);
count = 0;
}
try {
browser.tabs.sendMessage(tab.id, {
type: Constants.kCOMMAND_NOTIFY_SIDEBAR_CLOSED,
});
}
catch (_error) {
}
}
});
};
const receiver = message => {
if (Array.isArray(message))
return message.forEach(receiver);
if (message.type == Constants.kCONNECTION_HEARTBEAT)
updateTimeoutTimer();
else
onMessage.dispatch(windowId, message);
};
port.onMessage.addListener(receiver);
mReceivers.set(windowId, receiver);
onConnected.dispatch(windowId, connections.size);
port.onDisconnect.addListener(cleanup);
});
onMessage.addListener(async (windowId, message) => {
switch (message.type) {
case Constants.kNOTIFY_SIDEBAR_FOCUS:
mFocusState.set(windowId, true);
break;
case Constants.kNOTIFY_SIDEBAR_BLUR:
mFocusState.delete(windowId);
break;
}
});
}
export function init() {
if (mIsListening)
return;
mIsListening = true;
}
//===================================================================
// Logging
//===================================================================
browser.runtime.onMessage.addListener((message, _sender) => {
if (!mIsListening ||
!message ||
typeof message != 'object' ||
message.type != Constants.kCOMMAND_REQUEST_CONNECTION_MESSAGE_LOGS ||
!Constants.IS_BACKGROUND)
return;
browser.runtime.sendMessage({
type: Constants.kCOMMAND_RESPONSE_CONNECTION_MESSAGE_LOGS,
logs: JSON.parse(JSON.stringify(counts))
});
});