Skip to content

guest271314/WebExtensionMessageStream

Repository files navigation

WebExtensionMessagingStream

Explainer

Problem

No standardized Web Extension API to send or transfer TypedArrays or ArrayBuffers or ReadableStreams or TransformStreams to and from MV3 Web Extensions from arbitrary Web pages.

Solution

Chromium based browsers (Chromium, Chrome, Brave, Opera, Edge)

Define "web_accessible_resources" in manifest.json; use iframe appended to arbitrary Web page to establish a WindowClient in ServiceWorker context; use Transferable Streams with postMessage(readable, "*", [readable]) to transfer a ReadableStreamfrom the Web page to the FetchEvent in ServiceWorker which becomes a full-duplex stream due to Chromium's use of Mojo between WindowClient and Client and fetch event to handle fetch().

Implementation details

Internally Chromium has full-duplex stream capabilities between WindowClient and Client of ServiceWorker via Mojo. We use this to initialize a TransformStream with writable side in the arbitrary Web page, and readable side sent to ServiceWorker as a streaming request with fetch() and duplex: "half" RequestInit option set.

When the writable side gets a WritableStreamDefaultWriter, data in the form of Uint8Array written to write() is sent to the streaming request and read in the ServiceWorker fetch event, where any data can be enqueued into the TransformStreamDefaultController and sent to the WindowClient (iframe) where we read the data sent from the ServiceWorker.

The ServiceWorker is persistent, remains active due to the fact we have a live WindowClient and an indefinite fetch() request being handled by fetch event handler.

Since we have a WindowClient we have an id for the iframe nested context, so we can send messages to the arbitrary Web page that initiated a fetch() request from the ServiceWorker by filtering client ids, without necessarily waiting on messages from the client.

Something like

for (const [id, controller] of messageClients) { try { if (await clients.get(id)) { controller.enqueue(new TextEncoder().encode(id)); } } catch (e) { console.log(e); } }

If we add a query string to the URL used to request the Web extension web accessible resources iframe, we add citeria to filter further, if needed

const { readable, writable, transferableWindow } = await WebExtensionMessageStream(location.host) // optional;

Multiple iframes can be appended to the same Web page, and multiple discrete Web pages.

Removing the iframe from the Web page closes the messaging session, and results in a network type error because we have abruptly aborted the fetch() request. We can probably handle that a little differently using AbortController, if we wanted to.

We'll use a modern design for full-duplex asynchronous messaging in the form of a WHATWG TransformStream, similar to the design used by WebSocketStream, WebTrasnport.

Usage

const encoder = new TextEncoder(); const { readable, writable, transferableWindow } = await WebExtensionMessageStream(location.host) // optional; const writer = writable.getWriter(); readable.pipeThrough(new TextDecoderStream()).pipeTo( new WritableStream({ write(message) { console.log(message); }, close() { console.log("Stream close"); }, abort(reason) { console.log(reason); }, }), ).then(() => console.log("Done streaming")) .catch((e) => { console.log(e); }) .finally(() => { transferableWindow.remove(); console.log("WebExtensionMessageStream closed"); }); await writer.ready; await writer.write(encoder.encode(`Message from ${document.title}`)); 

Later

await writer.close(); 

References

License

Do What the Fuck You Want to Public License WTFPLv2

About

Stream (ReadableStream/Uint8Array) messages to and from Web pages and MV3 Web extension

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published