From 37d841df2caa8b11a447b068fc94e1e8cbe7d2a4 Mon Sep 17 00:00:00 2001 From: glacambre Date: Sat, 20 Jan 2024 16:05:04 +0100 Subject: [PATCH] Enable accessorless access to tab processes This commit enables accessing content script values directly, like this: (await tri.tabs[3].document.location.href) tri.tabs[6].document.title = "New title!" tri.tabs[9].tri.excmds.js("document.getElementById('blah').textContent").then(console.log) Note that setting values, as shown in the above document.title example above, cannot be synchronously, i.e. there is no way to wait for the value to have been written is the content process before the background process moves on to the next instruction. This is considered okay as writing to the content process synchronously can be done with (await tri.excmds.js("document.title = 'New title!'")) instead. At the moment, accessing all tabs by not specifying an index in tri.tabs is still not supported. --- src/lib/tabs.ts | 103 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/src/lib/tabs.ts b/src/lib/tabs.ts index fb38bac6..63ea917a 100644 --- a/src/lib/tabs.ts +++ b/src/lib/tabs.ts @@ -1,51 +1,90 @@ import * as Messaging from "@src/lib/messaging" -const allTabs = -1; +const allTabs = -1 + +// Small wrapper meant to enable sending a message either to a single tab or +// multiple ones. Note that for now, sending messages to all tabs does not +// work, for reasons unknown. +const msg = (tabId, ...args) => { + if (tabId === allTabs) { + return Messaging.messageAllTabs("omniscient_content", ...args) + } else { + return Messaging.messageTab(tabId, "omniscient_content", ...args) + } +} + +// This function is used to generate proxies. We use a function rather than an +// object created through Object.create(null) in order to make our proxy +// callable. As all proxy calls should be handled through the proxy's "apply" +// function, we make ItWouldBeAMistakeToCallThis throw an error, to make bugs +// as obvious as possible. +const ItWouldBeAMistakeToCallThis = () => { + throw Error("Error, base function ItWouldBeAMistakeToCallThis was called!") +} // tabProxy accumulates all accesses to a tab's properties and then, when the // value of said properties are read, set or called, sends a request to the // corresponding tab to either retrieve or set the value. -const tabProxy = (tabId, props) => new Proxy(()=>undefined, { - get(target, p, receiver) { - if (p === Symbol.toPrimitive) { - const prop = `tabs[${tabId}].${props.join(".")}`; - throw `${prop} cannot be used directly - use ${prop}.get() instead`; - } - return tabProxy(tabId, props.concat(p)); - }, - apply(target, thisArg, argArray) { - const last = props[props.length -1]; - switch (last) { - case "get": - case "set": - case "apply": - break; - default: - const call = `tabs[${tabId}].${props.join(".")}`; - const args = `(${argArray.join(", ")})`; - throw `${call}${args} cannot be called directly, use ${call}.apply${args} instead`; - }; - let msg = Messaging.messageAllTabs; - if (tabId !== allTabs) { - msg = (...args) => Messaging.messageTab(tabId, ...args); - } - return msg("omniscient_content", last, [{target: props.slice(0, props.length - 1), value: argArray}]); - }, -}) +const tabProxy = (tabId, props) => + new Proxy(ItWouldBeAMistakeToCallThis, { + get(target, p) { + if (p === Symbol.toPrimitive) { + // Symbol.toPrimitive will be accessed when the user attempts + // to use a content process value without awaiting it first, + // e.g.: + // + // tri.tabs[3].document.title + "!" + // + // It is a mistake to do this, so we throw an error - see the + // if condition checking for "then" for more details. + throw Error( + `TypeError: tabs[${tabId}].${props.join(".")} is a Promise, you need to await its value.`, + ) + } + if (p === "then") { + // Because we can only access content process values by + // messaging a tab, we can only get values as a promise. We + // take advantage of this fact to wait until we get an access + // to a "then" property before fetching anything - this enables + // rapid traversal of objects instead of having to perform slow + // back and forths for every property. + // + // This works with the "await" keyword too as awaits are turned + // into calls to "then" by the javascript engine. + // One drawback of this approach is that properties named + // + // "then" in the content process cannot be directly accessed. + // We consider this an okay trade-off, as there exists an + // alternative: using tri.tabs[3].eval("x.then") instead. + const promise = msg(tabId, "get", [ + { target: props, value: undefined }, + ]) + return promise.then.bind(promise) + } + return tabProxy(tabId, props.concat(p)) + }, + set(target, p, value) { + msg(tabId, "set", [{ target: props.concat(p), value }]) + return true + }, + apply(target, thisArg, argArray) { + return msg(tabId, "apply", [{ target: props, value: argArray }]) + }, + }) export const tabsProxy = new Proxy(Object.create(null), { - get(target, p, receiver) { - let id = Number(p); + get(target, p) { + const id = Number(p) if (typeof id === "number" && isFinite(id)) { // We're accessing tabs[i] - meaning that we should return a proxy // for a single tab return tabProxy(id, []) } - throw "Foreground tabs proxy needs to be indexed by tab ID."; + throw Error("Foreground tabs proxy needs to be indexed by tab ID.") // Ideally, if p is a string, we should construct a proxy for all // existing tabs. This unfortunately does not seem to work: // Messaging.messageAllTab seems to return an array of undefined when // running e.g. `tabs.document.title`. // return tabProxy(allTabs, []); - } + }, })