Exteranto

The browser extension framework

View the Project on GitHub exteranto/exteranto.github.io

Getting Started
Installation
Directory Structure
Application Configuration

Tutorials
Your First Exteranto

Concepts
Browser Extension Basics
Handling the IOC Container
Typed Message Passing
Service Providers

Typed message passing

Prerequisites

This article assumes you are familiar with a basic extension architecture. If you are just getting started with browser extensions, see this overview by Chrome.

It is also beneficial to understand the IOC Container and App booting. Especially if you experiment with the code samples.

Events

Events are messages fired only within the context of a single script. If you fire an event in tab A’s content, other scripts will have no knowledge of it. Events are baked into Exteranto. They are

Firing an event

Creating a new event is simple. Any class can be an event in exteranto as long as it extends Event class.

The SampleEvent.ts file:

import { Event } from '@exteranto/core'

export class SampleEvent extends Event {

  /**
   * @param message Payload to be sent with an event
   */
  public constructor (public message: string) {
    //
  }

}

We have to trigger the SampleEvent somewhere. Let’s create a listener that listens to AppBootedEvent and then fires SampleEvent every second. The FiresSampleEventAfterBoot.ts file:

import { SampleEvent } from './SampleEvent'
import { AppBootedEvent, Dispatcher, Listener } from '@exteranto/core'

export class FiresSampleEventAfterBoot implements Listener {

  /**
   * Dispatcher to fire events with.
   */
  @Autowired
  private dispatcher: Dispatcher

  /**
   * Starts firing events after app boot.
   *
   * @param event Exteranto app is ready
   */
  public handle (event: AppBootedEvent) : void {
    setInterval(() => {
      this.dispatcher.fire(new SampleEvent('Hello world!'))
    }, 1000)
  }

}

Handling an event

Now we have to create another listener that handles the event. The PrintsContent.ts file:

import { Listener } from '@exteranto/core'
import { SampleEvent } from './SampleEvent'

export class PrintsContent implements Listener {

  /**
   * Prints an event message.
   *
   * @param event Typed event
   */
  public handle (event: SampleEvent) : void {
    console.log(`[${new Date}] ${event.message}`)
  }

}

Let’s route the events to their listeners. The events.ts file:

import { SampleEvent } from './SampleEvent'
import { PrintsContent } from './PrintsContent'
import { AppBootedEvent } from '@exteranto/core'
import { FiresSampleEventAfterBoot } from './FiresSampleEventAfterBoot'

export default (touch: (event: Class<Event>) => ListenerBag) => {

  touch(AppBootedEvent)
    .addListener(new FiresSampleEventAfterBoot)

  touch(SampleEvent)
    .addListener(new PrintsContent)

}

Putting it together

And finally booting the app. After we run this code, we have to open the background console. Every second, a new Hello world! string is printed along with the timestamp. The main.js file:

import events from './events'
import { App, Script } from '@exteranto/core'

const app: App = new App(
  Script.BACKGROUND,
  {
    providers: [],
    bound: {},
  },
  events,
)

app.bootstrap()

Messaging

There are two actor types. Background, server-like master script, and content. There is always one background and an arbitrary number of contents.

Background can send a message to any content, and each content can only send messages to the background. This is not different to the usual extension architecture.

Communication channels

Before we can start sending messages around, we have to boot the channels. Messaging directly depends on MessagingProvider. Sending messages from background to content depends on TabsProvider.

Create a new listener BootsMessaging.ts.

import { Messaging } from '@exteranto/api'
import { Autowired, Listener } from '@exteranto/core'

export class BootsMessageListener implements Listener {

  /**
   * The messaging API implementation.
   */
  @Autowired
  private messaging: Messaging

  /**
   * Handle the fired event.
   */
  public handle () : void {
    this.messaging.listen()
  }

}

And let it listen to AppBootedEvent in each script that should have messaging active. This will usually be both background and contents.

Messages

All messages have to extend Message class, which is a child of Event. A message has to have public payload: T constructor parameter which is not cyclic. This payload property goes through JSON.stringify and is sent to the receiving script. There the message is instantiated again with the JSON.parsed payload which is passed into the constructor.

The whole process looks like this:

  1. SampleMessage class accepts object into its constructor
  2. We send(new SampleMessage({ text: 'hello' }))
  3. Payload { text: 'hello' } is stringified to "{"text":"hello"}" and sent to the receiving script
  4. Receiving script JSON.parses the payload which results in { text: 'hello' }
  5. Receiving script instantiates new SampleMessage({ text: 'hello' }) and it is routed to the appropriate listeners.

The SampleMessage.ts file has to be somewhere in between background and content files as it’s a common language between the two:

import { Message } from `@exteranto/api`

export interface SampleMessagePayload {

  /**
   * Text field!
   */
  text: string

}

export class SampleMessage extends Message {

  /**
   * We can also define methods that work with the data in the payload.
   *
   * @return Upper-cased text
   */
  public get textUppercase () : string {
    return this.payload.text.toUpperCase()
  }

  public constructor (public payload: SampleMessagePayload) {
    //
  }

}

A background listener in the PrintsContent.ts file:

import { Listener } from '@exteranto/core'
import { SampleMessage } from '...'

export class PrintsContent {

  /**
   * Prints out uppercase text.
   *
   * @param message Typed message
   */
  public handle (message: SampleMessage) : void {
    console.log(`Message sent from tab ${message.context.tabId}:`)
    console.log(`[${new Date}] ${message.textUppercase}`)
  }

}

And now we can send the message from content in a similar manner we have sent the events in previous section:

import { SampleMessage } from '...'
import { Messaging } from '@exteranto/api'
import {
  Autowired,
  Listener,
  WindowLoadedEvent,
} from '@exteranto/core'

export class FiresSampleMessagesOnWindowLoad {

  /**
   * Messaging instance to send the messages.
   */
  @Autowired
  private messaging: Messaging

  /**
   * Once the window object is loaded in the content script, we fire SampleMessage.
   *
   * @param _ Typed event
   */
  public handle (_: WindowLoadedEvent) : void {
    setTimeout(() => {
      this.messaging.send(new SampleMessage({
        text: 'Hello world from content script!',
      }))
    }, 1000)
  }

}

We boot the app in both content and background script and route the messages in events.ts-like file as we did in the previous section.

Background script to content script

In order to send a message to a certain content script, the tab id has to be known. Then we use the Tabs APIs provided by @exteranto/api.

import { SampleMessage } from '...'
import { Tabs } from '@exteranto/api'

const tabs: Tabs = ...
const tabId: number = 1
const message: SampleMessage = new SampleMessage({
  text: 'Hello',
})

tabs.get(tabId).then(tab => tab.send(message))

Content script to background script

Since there is only one background script, no additional context information is needed to send a message (unlike the tab id in background -> content communication).

import { SampleMessage } from '...'
import { Messaging } from '@exteranto/api'

const messaging: Messaging = ...
const message: SampleMessage = new SampleMessage({
  text: 'Hello',
})

messaging.send(message)

Replying to messages

Middleware

Routing

Known drawbacks