Implement basic proxy providing easy access to tabs from background

This commit implements a basic proxy enabling access to arbitrary
values/functions in tabs from the background script, like this:

tri.tabs[3].document.title.get().then(console.log)
tri.tabs[3].document.title.set("New title!").then(console.log)
tri.tabs[9].alert.apply("Hello world!")
tri.tabs[12].tri.excmds.js.apply("alert('Hello world!')")

Ease of implementation was chosen above ease of use. Enabling reading,
writing and calling directly through property accesses instead of
forcing the use of .get()/.set()/.apply() will be attempted in another
commit.
This commit is contained in:
glacambre 2024-01-20 14:54:02 +01:00
parent d59a9d96bb
commit 3806445d85
5 changed files with 94 additions and 1 deletions

View file

@ -3,7 +3,6 @@
/* tslint:disable:import-spacing */
import * as proxy_background from "@src/lib/browser_proxy_background"
import * as controller from "@src/lib/controller"
import * as perf from "@src/perf"
import { listenForCounters } from "@src/perf"
@ -30,6 +29,7 @@ import * as commands from "@src/background/commands"
import * as meta from "@src/background/meta"
import * as Logging from "@src/lib/logging"
import * as Proxy from "@src/lib/proxy"
import { tabsProxy } from "@src/lib/tabs"
// Add various useful modules to the window for debugging
;(window as any).tri = Object.assign(Object.create(null), {
@ -52,6 +52,7 @@ import * as Proxy from "@src/lib/proxy"
R,
perf,
meta,
tabs: tabsProxy,
})
import { HintingCmds } from "@src/background/hinting"

View file

@ -83,6 +83,7 @@ try {
}
const controller = await import("@src/lib/controller")
const { omniscient_controller } = await import("@src/lib/omniscient_controller")
const excmds_content = await import("@src/.excmds_content.generated")
const hinting_content = await import("@src/content/hinting")
// Hook the keyboard up to the controller
@ -124,6 +125,7 @@ messaging.addListener(
"controller_content",
messaging.attributeCaller(controller),
)
messaging.addListener("omniscient_content", messaging.attributeCaller(omniscient_controller))
// eslint-disable-next-line @typescript-eslint/require-await
messaging.addListener("alive", async () => true)

View file

@ -9,6 +9,7 @@ export type TabMessageType =
| "controller_content"
| "commandline_content"
| "finding_content"
| "omniscient_content"
| "commandline_cmd"
| "commandline_frame"
| "state"

View file

@ -0,0 +1,38 @@
export const omniscient_controller = {
set: ({ target, value }) => {
let result
try {
const the_last_one = target[target.length - 1]
const everything_except_the_last_one = target.slice(
0,
target.length - 1,
)
const second_to_last = everything_except_the_last_one.reduce(
(acc, prop) => acc[prop],
window,
)
result = second_to_last[the_last_one] = value
} catch (e) {
console.error(e)
}
return result
},
get: ({ target, _ }) => {
let result
try {
result = target.reduce((acc, prop) => acc[prop], window)
} catch (e) {
console.error(e)
}
return result
},
apply: ({ target, value }) => {
let result
try {
result = target.reduce((acc, prop) => acc[prop], window)(...value)
} catch (e) {
console.error(e)
}
return result
},
}

51
src/lib/tabs.ts Normal file
View file

@ -0,0 +1,51 @@
import * as Messaging from "@src/lib/messaging"
const allTabs = -1;
// 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}]);
},
})
export const tabsProxy = new Proxy(Object.create(null), {
get(target, p, receiver) {
let 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.";
// 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, []);
}
})