"use strict"; const HOST_NAME = "com.easygoingaming.firefox_agent_bridge"; const RECONNECT_MS = 2000; const ALLOWED = { "meta.ping": async () => ({ ok: true, extension: "firefox-agent-bridge" }), "meta.methods": async () => Object.keys(ALLOWED).sort(), "bookmarks.getTree": () => browser.bookmarks.getTree(), "bookmarks.getSubTree": (id) => browser.bookmarks.getSubTree(id), "bookmarks.getChildren": (id) => browser.bookmarks.getChildren(id), "bookmarks.get": (idOrIds) => browser.bookmarks.get(idOrIds), "bookmarks.search": (query) => browser.bookmarks.search(query), "bookmarks.create": (details) => browser.bookmarks.create(details), "bookmarks.update": (id, changes) => browser.bookmarks.update(id, changes), "bookmarks.move": (id, destination) => browser.bookmarks.move(id, destination), "bookmarks.remove": (id) => browser.bookmarks.remove(id), "bookmarks.removeTree": (id) => browser.bookmarks.removeTree(id), "bookmarks.getRecent": (numberOfItems) => browser.bookmarks.getRecent(numberOfItems), }; let port = null; let reconnectTimer = null; let hostUp = false; const adminWait = new Map(); function asArray(value) { if (value === undefined || value === null) { return []; } return Array.isArray(value) ? value : [value]; } async function dispatchBookmark(message) { const method = message && message.method; const fn = ALLOWED[method]; if (!fn) { throw new Error(`method not allowed: ${method || "(missing)"}`); } return fn(...asArray(message.args)); } function adminCall(method, args) { if (!port) { return Promise.reject(new Error("native host is not connected")); } const id = crypto.randomUUID(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { adminWait.delete(id); reject(new Error("host did not answer")); }, 8000); adminWait.set(id, { resolve, reject, timer }); port.postMessage({ type: "admin", id, method, args: args || [] }); }); } function attach(nextPort) { port = nextPort; hostUp = true; port.onMessage.addListener(async (message) => { if (message && message.type === "admin-result") { const pending = adminWait.get(message.id); if (!pending) { return; } adminWait.delete(message.id); clearTimeout(pending.timer); if (message.ok) { pending.resolve(message.result); } else { pending.reject(new Error(message.error || "admin failed")); } return; } const id = message && message.id; try { const result = await dispatchBookmark(message); port.postMessage({ id, ok: true, result }); } catch (err) { port.postMessage({ id, ok: false, error: err && err.message ? err.message : String(err), }); } }); port.onDisconnect.addListener(() => { port = null; hostUp = false; for (const [id, pending] of adminWait.entries()) { clearTimeout(pending.timer); pending.reject(new Error("native host disconnected")); adminWait.delete(id); } scheduleReconnect(); }); } function scheduleReconnect() { if (reconnectTimer) { return; } reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, RECONNECT_MS); } function connect() { try { attach(browser.runtime.connectNative(HOST_NAME)); } catch (err) { hostUp = false; console.error("native host connect failed", err); scheduleReconnect(); } } browser.runtime.onMessage.addListener((message) => { const kind = message && message.type; if (kind === "status") { return Promise.resolve({ hostUp, port: 17634 }); } if (kind === "admin") { return adminCall(message.method, message.args).then( (result) => ({ ok: true, result }), (err) => ({ ok: false, error: err.message }) ); } return undefined; }); connect();