The First Setback

I might have been a little untruthful at first; after all, chrome extensions really are just sandboxed web applications that have access to certain components of the user’s browser. It’s just that actually tying all the moving parts together can be rather challenging.

Due to the sandboxed nature of the application you can’t simply “share code” between components or use a standard event bus to handle actions within the browser. As with most sandbox implementations you can be quite limited in terms of communication with the host.

I was tasked with architecturing the communication platform that TronLink would be built on, along with producing the first version of the TronLink Web API.

By having chrome extensions sandboxed, websites cannot freely access the internals of the application without creating a communication channel. That’s where contentScript and the other communication modules come in to play.

Let’s first discuss the four main components of the application:

backgroundScript

The core of the application, backgroundScript manages a persisted state that the main popup interface ties into. This is the place where the magic happens — transactions are signed, accounts are created, and API events are dispatched. Anything related to core control of the extension occurs here.

The core of the application, manages a persisted state that the main popup interface ties into. This is the place where the magic happens — transactions are signed, accounts are created, and API events are dispatched. Anything related to core control of the extension occurs here. pageHook

Whilst not as drastic as the backgroundScript , pageHook still weighs up its importance. pageHook ’s task is to inject and expose TronLink’s versioned Web API into participating sites. This is what handles connecting websites to the extension itself.

Whilst not as drastic as the , still weighs up its importance. ’s task is to inject and expose TronLink’s versioned Web API into participating sites. This is what handles connecting websites to the extension itself. contentScript

Compared to the other modules one could argue that contentScript is just another tiny moving piece in a large application. Indeed — it might be small but it is not tiny by any definition. contentScript actually tunnels and pairs all communication between pageHook and backgroundScript.

Compared to the other modules one could argue that contentScript is just another tiny moving piece in a large application. Indeed — it might be small but it is not tiny by any definition. actually tunnels and pairs all communication between and popup

An entire application in itself, popup is how we refer to the user interface of the extension. This is where confirmations occur along with general browsing and control of the extension. It can be triggered by pressing TronLink’s icon in the browser navigation bar or by websites’ requesting a confirmation.

Now that’s done, let’s discuss the challenge of linking the four modules.

As briefly explained earlier, the use of a sandbox limits the extension and web page from communicating freely. contentScript runs in the context of the web page whereas backgroundScript runs from within the extension’s sandbox. This means that all communication between the two has to be passed between registered channels.

For a content script to communicate with the background page both scripts must first establish and listen to a communication channel.

These channels provide the two with the ability to freely pass JSON objects from one end to the other. It’s important to note that these are JSON objects, and not regular JavaScript objects.

This means that we couldn’t pass a promise for instance with the message — which would have allowed us to simply resolve (or reject) the promise to return both acknowledgement and/or a response to the request.

Basic demonstration on how the communication channels are structured

Pictured above is a rather high-level example of how the channels are formed. Each tab will have it’s own contentScript that will establish a communication channel with backgroundScript. It can then dispatch messages to each individual tab by referencing the incoming tabID.

When a tab is created it is assigned a unique ID — usually a 3 digit integer. This is accessed through the port (the channel handler) that each channel is assigned. By binding each port to its tabID we have unique reference to each individual tab.

Let’s create a quick event listener backgroundScript:

chrome.extension.onconnect.addListener(port => {

const source = `${port.name}-${port.sender.tab.id}`;

this._channels[source] = port;

});

Now any time we receive an event from contentScript we can simply return an event by dispatching a message on the port assigned to it’s unique reference. We can extend the above to implement this:

chrome.extension.onconnect.addListener(port => {

const source = `${port.name}-${port.sender.tab.id}`;

this.channels[source] = port; port.onMessage.addListener(({ action, data } = event) => {

this.emit(event.action, { data, source });

});

});

Each time a message is received we emit a new event with the supplied data and the source of the communication channel. To reply to a message we would simply do:

communication.on('someEvent', ({ data, source }) => {

this.channels[source].postMessage({

action: 'response',

data: { ts: Date.now() }

});

});

Easy, right?

Wrong again.

High-level demonstration of how the events propagate through the communication channels

Sure, we could simply track and reference each incoming message and the event source for every single event but what’s the fun in that? Not only would it bloat the codebase but it would also make further adaption of the application difficult, and debugging even more daunting of a task.

To overcome this I implemented an abstraction layer that I call LinkedResponse — contentScript similarity has its own layer called LinkedRequest . backgroundScript will forward all events that occur under the action of tunnel to this LinkedResponse which will then pair replies to the original events.

I’ve implemented this by dispatching a unique ID for each message — LinkedResponse will take this unique ID, emit an event with a promise, and upon the promise being resolved, (or rejected) emit a new event to contentScript that will contain the same unique ID.

Remember when I mentioned contentScript earlier? It also listens for all tunnel events and will take this unique ID, pair it to the original request and resolve the promise that it first generated.

The basic usage is as follows:

# contentScript linkedRequest.build({

method: 'testMethod',

data: { ts: Date.now() }

}).then(response => {

logger.info(`Received from backgroundScript: ${response}`);

}).catch(error => {

logger.info(`Error from backgroundScript: ${error}`);

}); # backgroundScript linkedResponse.on('request', ({ reject, resolve, payload }) => {

switch(payload.method) {

case 'testMethod':

resolve(payload.data);

}

});

In the demonstration above, contentScript will emit an event to backgroundScript called testMethod that contains the current timestamp as its data.

backgroundScript will receive the event, handle the testMethod being triggered and return the same timestamp that was originally dispatched.

This is a much more simple method of communicating between the two.

If you’d like to see how these specific methods are implemented you can take a look at our github repository.