// A menu can outlive its MV3 service worker while the user runs the installer. // Reopen the UI port on the next command; never replay requests already sent. export function createUiConnection({ onEvent, onDisconnect, runtime = chrome.runtime }) { const requests = new Map(); const disconnected = () => new Error("Связь с расширением прервана. Нажмите «Проверить подключение» или «Запустить» для повторного подключения."); let port = null; function drop(connection) { if (port !== connection) return; port = null; for (const [id, request] of requests) { if (request.port !== connection) continue; requests.delete(id); request.reject(disconnected()); } onDisconnect?.(); } function connect() { if (port) return port; const connection = runtime.connect({ name: "ytdl-ui" }); port = connection; connection.onMessage.addListener(message => { if (port !== connection) return; if (message.event) { onEvent?.(message); return; } const request = requests.get(message.id); if (!request || request.port !== connection) return; requests.delete(message.id); message.ok ? request.resolve(message.result) : request.reject(new Error(message.error)); }); connection.onDisconnect.addListener(() => { // Read lastError in the callback to avoid an unchecked runtime error. void runtime.lastError?.message; drop(connection); }); return connection; } function call(action, payload = {}) { return new Promise((resolve, reject) => { for (let attempt = 0; attempt < 2; attempt++) { let connection, id; try { connection = connect(); id = crypto.randomUUID(); requests.set(id, { resolve, reject, port: connection }); connection.postMessage({ id, action, payload }); return; } catch (error) { if (id) requests.delete(id); if (connection) drop(connection); // A synchronous disconnected-port error means this command was not sent. if (attempt === 0 && /disconnected port/i.test(error.message)) continue; reject(/disconnected port/i.test(error.message) ? disconnected() : error); return; } } }); } return { call }; }