Adapter Guide
Implement the adapter interface for Comctx communication channels.
An adapter lets Comctx work across different communication channels.
To adapt Comctx to a specific environment, implement the following interface:
interface Adapter<T extends MessageMeta = MessageMeta> {
/** Optional adapter name for debug logs and message diagnostics */
name?: string
/** Send a message to the other side */
sendMessage: (message: Message<T>, transfer: Transferable[]) => MaybePromise<void>
/** Register a message listener */
onMessage: (callback: (message?: Partial<Message<T>>) => void) => MaybePromise<OffMessage | void>
}What an adapter does
An adapter has two responsibilities:
- send messages to the other side
- register a listener for incoming messages
This is what makes Comctx work across environments such as:
- Web Workers
- Browser Extensions
- iframes
- Electron
- other JavaScript runtimes with message-based communication
Minimal adapter
A minimal adapter looks like this:
import type { Adapter, SendMessage, OnMessage } from 'comctx'
export default class CustomAdapter implements Adapter {
name = 'custom-adapter'
sendMessage: SendMessage = (message) => {
postMessage(message)
}
onMessage: OnMessage = (callback) => {
const handler = (event: MessageEvent) => callback(event.data)
addEventListener('message', handler)
return () => removeEventListener('message', handler)
}
}sendMessage
sendMessage is responsible for delivering the outgoing message to the other side.
sendMessage: (message, transfer) => {
postMessage(message, transfer)
}When transfer is enabled, transferable objects are automatically extracted from messages and passed as the transfer parameter.
onMessage
onMessage registers a listener for incoming messages.
onMessage: (callback) => {
const handler = (event: MessageEvent) => callback(event.data)
addEventListener('message', handler)
return () => removeEventListener('message', handler)
}The callback should receive the incoming Comctx message payload.
Transfer-enabled adapter
When transfer: true is enabled, your adapter should forward the transfer parameter to the underlying transport.
import type { Adapter, SendMessage } from 'comctx'
export default class TransferAdapter implements Adapter {
sendMessage: SendMessage = (message, transfer) => {
this.worker.postMessage(message, transfer)
}
// ... rest of implementation
}Web Worker example
Worker adapter
import { Adapter, SendMessage, OnMessage } from 'comctx'
export type WorkerEndpoint = Pick<Worker, 'postMessage' | 'addEventListener' | 'removeEventListener'>
export default class WorkerAdapter implements Adapter {
constructor(
private worker: WorkerEndpoint,
public name?: string
) {}
sendMessage: SendMessage = (message, transfer) => {
this.worker.postMessage(message, transfer)
}
onMessage: OnMessage = (callback) => {
const handler = (event: MessageEvent) => callback(event.data)
this.worker.addEventListener('message', handler)
return () => this.worker.removeEventListener('message', handler)
}
}Browser Extension example
Inject adapter
import browser from 'webextension-polyfill'
import { Adapter, Message, SendMessage, OnMessage } from 'comctx'
export interface MessageMeta {
url: string
}
export default class InjectAdapter implements Adapter<MessageMeta> {
sendMessage: SendMessage<MessageMeta> = (message) => {
browser.runtime.sendMessage(browser.runtime.id, {
...message,
meta: { url: document.location.href },
})
}
onMessage: OnMessage<MessageMeta> = (callback) => {
const handler = (message?: Partial<Message<MessageMeta>>) => {
callback(message)
}
browser.runtime.onMessage.addListener(handler)
return () => browser.runtime.onMessage.removeListener(handler)
}
}Provide adapter
import browser from 'webextension-polyfill'
import { Adapter, Message, SendMessage, OnMessage } from 'comctx'
export interface MessageMeta {
url: string
}
export default class ProvideAdapter implements Adapter<MessageMeta> {
sendMessage: SendMessage<MessageMeta> = async (message) => {
const tabs = await browser.tabs.query({ url: message.meta.url })
tabs.map((tab) => browser.tabs.sendMessage(tab.id!, message))
}
onMessage: OnMessage<MessageMeta> = (callback) => {
const handler = (message?: Partial<Message<MessageMeta>>) => {
callback(message)
}
browser.runtime.onMessage.addListener(handler)
return () => browser.runtime.onMessage.removeListener(handler)
}
}Iframe example
Iframe adapter
import { Adapter, SendMessage, OnMessage } from 'comctx'
export type WindowEndpoint = Pick<Window, 'postMessage' | 'addEventListener' | 'removeEventListener'>
export default class WindowAdapter implements Adapter {
constructor(
private window: WindowEndpoint,
public name?: string,
private targetOrigin = '*'
) {}
sendMessage: SendMessage = (message) => {
this.window.postMessage(message, this.targetOrigin)
}
onMessage: OnMessage = (callback) => {
const handler = (event: MessageEvent) => callback(event.data)
this.window.addEventListener('message', handler)
return () => this.window.removeEventListener('message', handler)
}
}Notes
sendMessageandonMessageare the only required adapter methods- the adapter should match the communication channel used by your runtime
- when transfer is enabled, forward the
transferparameter onMessageshould return a cleanup function when possible