New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using MessagePorts (+ Transferables) over ContextBridge #27024
Comments
Passing |
Thanks Jeremy, copying over my previous message for thread continuation: Issues I've observed:
The docs suggest this pattern: contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing')
}
) but it's not straightforward to use this pattern with a MessagePort, as its only accessible within the callback of ipcRender.on("port", ({ port }) => { ... }) e.g. type PortName = string
type Channel = string
type Payload = object
type Reply = (payload: Payload) => void
type Callback = (payload: Payload, reply: Reply, remove: () => void) => void
type Post = (payload: Payload, transferables: Transferable[]) => void
type Connection = (
payload: Payload,
post: Post,
reply: Reply,
disconnect: () => void,
) => void
contextBridge.exposeInMainWorld("electron", {
connect: (port: PortName, connection: Connection) => {
if (!validPorts.includes(port)) {
throw new Error(`Invalid Channel "${port}"`)
}
const wrappedCallback = ({ ports }: IpcRendererEvent, _: Payload) => {
const [messagePort] = ports
if (!messagePort) throw new Error("Connection requires (1) port")
const post: Post = (outgoing: Payload, transferables: Transferable[]) => {
messagePort.postMessage(outgoing, transferables)
}
const reply: Reply = (outgoing: Payload) => {
// same channel (channel should also be in outgoing payload)
messagePort.postMessage(outgoing)
}
const disconnect = () => {
ipcRenderer.removeListener(port, wrappedCallback)
}
messagePort.onmessage = ({ data }: MessageEvent<Payload>) => {
connection(data, post, reply, disconnect)
}
messagePort.onmessageerror = ({ data }: MessageEvent<Payload>) => {
connection(data, post, reply, disconnect)
}
messagePort.start()
}
ipcRenderer.on(`${port}:port`, wrappedCallback)
},
}) |
That's good to know and makes sense. In that case, I'm attempting to work out the best way to attach listeners to and post messages over MessagePorts by exposing methods on the ContextBridge. Unfortunately, that's complicated by the fact that the port is received in a callback, and that the "state" of the ContextBridge is immutable (can't add/remove methods once the port is attached) |
You might try e.g: https://gist.github.com/e95abf568696723caa06ba95c4cc2f78 // preload.js
window.onload = () => {
const {port1, port2} = new MessageChannel
window.postMessage('hi', '*', [port2])
port1.onmessage = (e) => {
console.log('isolated context got message:', e.data)
}
} // renderer.js
window.onmessage = (e) => {
e.ports[0].postMessage('hi from main world')
} |
Does that work with |
I just tried your example in the Fiddle, seems to work well. I hadn't realized you could send messages between the preload and the renderer using postMessage. So it seems like the message port communication would happen parallel to and outside of the contextBridge by chaining postMessage commands between main <-> preload <-> renderer |
there's no need to chain anything, you can just pass ports around. e.g. if the isolated world (i.e. preload) receives a message port, you can just |
The main reason to use |
is the use of So something like contextBridge.exposeInMainWorld(
'electron',
{
connect: () => {
ipcRenderer.on('port', ({ ports }) => {
window.postMessage('port', undefined, ports)
})
}
}
) (I understand this is still a synchronous pattern, but I'm wondering about being able to add/remove the listener from renderer which would be synchronous with connect/disconnect should work if the port is coming from Also curious about window.onload in preload.js. I've seen a link to process.once('loaded') but never window used in such a way |
the isolated world is connected to the DOM just the same as the main world, it's just the JS contexts that are separated. We could probably expand https://www.electronjs.org/docs/tutorial/context-isolation. Things that should work just fine from the isolated world:
Things that aren't shared:
|
From MDN:
is there a way to specify either the isolated (node-ish) preload context or the renderer (js) context as the targetOrigin when using |
@willium both contexts have the same origin. there's no need to specify the origin because they're guaranteed to be same-origin, so |
I suppose I'm more concerned about discriminating the target between the two than security so that we don't get caught in an infinite loop for back and forth communication (Gist) // preload.js
window.onload = () => {
window.postMessage('ping', '*')
window.onmessage = (e) => {
console.log(e.data)
window.postMessage("pang", "*")
}
} // renderer.js
window.onmessage = (e) => {
console.log(e.data)
window.postMessage("pong", "*")
} |
I'd suggest using a |
ah, clever. that would work. alas, so much plumbing 😄
|
What are you trying to accomplish? Communication between the main process and the main world in the renderer? |
Paradoxically, the main reason I want the ability to discriminate with The best I can come up with is: // preload.js
window.onload = () => {
window.postMessage('ping', '*')
window.onmessage = (e) => {
// this might get "ping" even though it's after
// because we have no guarentee about execution order
if (e.data === "ping") return
console.log("recieved reply", e.data)
}
} // renderer.js
window.onmessage = (e) => {
console.log("received message", e.data)
window.onmessage = undefined // unsub or we loop ourselves
window.postMessage("pong", "*")
} |
that, as well as communication between multiple renderer processes over message channels |
I'm not sure what this means exactly, there shouldn't be any trouble discriminating between messages from the browser process and messages from the renderer process. they don't have the same problem as between the isolated/main worlds in the renderer with both receiving all |
sorry, I'm mixing up my terminology—it's super hard to be clear about "main world" and "electron main process". When I've been saying "browser" I mean "main world" with DOM and vanilla JS on the other side of the contextBridge. I see you guys call it "main world" I'm creating a message channel in the preload, sending one port to the main world, and I'd like the main world to say "thanks, got the channel" to the preload. this would have to be over window.postMessage as I'm using contextIsolation. does that make sense? (the other port of the message channel goes to the electron main process to then be forwarded on to another renderer process, and you're right there is no trouble there except for just the sheer amount of plumbing needed) |
as an aside, it would be nice to not have "main" refer to two different things within electron-land. |
What's your motivation for this? There shouldn't be any reason for the message to not go through. You could send a message back along the message channel from the main world in order to indicate receipt, if you like. If the other end of the channel ends up getting garbage collected (because it wasn't received and handled), you'll eventually get a e.g. // isolated world
const {port1, port2} = new MessageChannel
window.postMessage({}, '*', [port2])
port1.onmessage = (e) => {
if (e.data.type === 'ack') {
// main world received the port, continue
}
}
port1.onclose = () => {
// the main world closed their end of the channel, either
// explicitly or implicitly (by dropping the reference)
} // main world
window.onmessage = (e) => {
const port = e.ports[0]
port.postMessage({type: 'ack'});
// ... etc
} |
yeah, I've noticed the onclose event—very neat.
should've been
? It's a bit confusing because the preload has access to both, right? I've only been using MessageChannel in the preload. |
It's not specifically motivated, so I understand an eye roll at my overuse of 🤝 haha |
MessageChannelMain is only relevant in the main process (i.e. the one with The |
I'm closing this issue as I don't think there's anything actionable here. I agree our docs could use improvement but I'm not exactly sure how. I'd be happy to hear some specific suggestions (or PRs) though! |
awesome, didn't know that! thanks for your help—I'll follow up with some doc PRs when I've had some time to stew and test |
Cool. I'll look into the feasibility of extending electron's typings to extend the dom.d.ts as well |
Please excuse me for bringing this already closed discussion back to life. Can transferable objects be transferred between a |
According to #27024 (comment) MessagePort cannot be sent over ContextBridge, is this still the case? |
Transferables aren't listed in the relevant documentation, but given their type, it seems like the preferred case would be that they are indeed "transferred" over the contexts. Is that possible?
In any case, is there a good path forward for using MessagePorts alongside contextBridge? How about passing transferables over MessagePorts without incurring the intermediary copy across the bridge?
Here is my best guess about how that might work (though, again, transferables don't seem to work as expected).
Preflight Checklist
The text was updated successfully, but these errors were encountered: