Skip to content

Custom transports

When none of the built-in transports matches your channel, wrap it in a plain object with emit and receive. Anything that can move a message between two contexts — a BroadcastChannel, a text socket, a native bridge — becomes an osra transport.

A custom transport is a plain object with emit and/or receive, plus an optional isJson flag. Each of emit and receive may be a platform transport (a Worker, a WebSocket, a window, …) or a function:

type
type EmitHandler = (message: Message, transferables?: Transferable[]) => void
EmitHandler
= (
message: Message
message
:
type Message<TModules extends readonly RevivableModule[] = readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), ... 25 more ..., { ...; }]> = MessageBase & MessageVariant<TModules>
Message
,
transferables: Transferable[] | undefined
transferables
?:
type Transferable = MessagePort | ArrayBuffer | ImageBitmap | OffscreenCanvas | MediaSourceHandle | ReadableStream<any> | WritableStream<any> | TransformStream<any, any> | AudioData | VideoFrame | RTCDataChannel
Transferable
[]) => void
type
type ReceiveHandler = (listener: (message: Message, context: MessageContext) => void) => void | (() => void)
ReceiveHandler
= (
listener: (message: Message, context: MessageContext) => void
listener
: (
message: Message
message
:
type Message<TModules extends readonly RevivableModule[] = readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), ... 25 more ..., { ...; }]> = MessageBase & MessageVariant<TModules>
Message
,
context: MessageContext
context
:
type MessageContext = {
port?: MessagePort | WebExtPort;
sender?: WebExtSender;
receiveTransport?: ReceivePlatformTransport;
source?: MessageEventSource | null;
origin?: string;
}
MessageContext
) => void
) => void | (() => void)
  • An emit function is called with the ready-to-send envelope and the collected transfer list. Serialization is yours; osra does not stringify for function emitters.
  • A receive function is called once with osra’s listener; invoke it with parsed envelope objects. Key and remoteName filtering are applied for you. Optionally return an unsubscribe function; it runs when unregisterSignal aborts.

A transport that can’t both emit and receive rejects expose() immediately: a bare { emit } or { receive } alone is a configuration error.

Custom transports must be plain objects — prototype exactly Object.prototype, or a null prototype. This is deliberate: prototype-based objects like Node EventEmitters have inherited emit members and are intentionally not detected as custom transports, so passing one never silently misclassifies.

isJson: true forces JSON-safe boxing: base64 buffers, synthetic ports, no transfer. Set it whenever the channel can’t carry transferables.

Without it, JSON mode is auto-detected from the embedded platform transports — { emit: webSocket } is JSON-only even though the wrapper isn’t. See JSON vs clone for what degrades in JSON mode.

The second argument to osra’s receive listener carries whatever the channel knows: { port?, sender?, receiveTransport?, source?, origin? }origin and source from window events, sender and port from WebExtension messaging, and receiveTransport.

BroadcastChannel can’t carry transferables, so mark the transport isJson: true:

const
const channel: BroadcastChannel
channel
= new
var BroadcastChannel: new (name: string) => BroadcastChannel

The BroadcastChannel interface represents a named channel that any browsing context of a given origin can subscribe to.

MDN Reference

BroadcastChannel class is a global reference for import { BroadcastChannel } from 'worker_threads' https://nodejs.org/api/globals.html#broadcastchannel

@sincev18.0.0

BroadcastChannel
('app')
const
const remote: {
ping: () => Promise<string>;
}
remote
= await
expose<PeerApi, readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), typeof import("osra/build/revivables/array-buffer"), typeof import("osra/build/revivables/date"), typeof import("osra/build/revivables/headers"), typeof import("osra/build/revivables/error"), typeof import("osra/build/revivables/typed-array"), ... 20 more ..., {
...;
}], Transport, Capable<...>>(value: Capable<...>, options: StartConnectionsOptions<...> & {
...;
}): Promise<...>
expose
<
type PeerApi = {
ping: () => Promise<string>;
}
PeerApi
>(
const localApi: {
echo: (text: string) => Promise<string>;
}
localApi
, {
transport: Transport
transport
: {
isJson?: boolean | undefined
isJson
: true,
emit: (message: Message) => void
emit
:
message: Message
message
=>
const channel: BroadcastChannel
channel
.
BroadcastChannel.postMessage(message: any): void

The postMessage() method of the BroadcastChannel interface sends a message, which can be of any kind of Object, to each listener in any browsing context with the same origin.

MDN Reference

postMessage
(
message: Message
message
),
receive: (listener: (event: Message, messageContext: MessageContext) => void) => () => void
receive
:
listener: (event: Message, messageContext: MessageContext) => void
listener
=> {
const
const handler: (event: MessageEvent) => void
handler
= (
event: MessageEvent<any>
event
:
interface MessageEvent<T = any>

The MessageEvent interface represents a message received by a target object.

MDN Reference

MessageEvent
) =>
listener: (event: Message, messageContext: MessageContext) => void
listener
(
event: MessageEvent<any>
event
.
MessageEvent<any>.data: any

The data read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.

MDN Reference

data
, {})
const channel: BroadcastChannel
channel
.
BroadcastChannel.addEventListener<"message">(type: "message", listener: (this: BroadcastChannel, ev: MessageEvent<any>) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('message',
const handler: (event: MessageEvent) => void
handler
)
return () =>
const channel: BroadcastChannel
channel
.
BroadcastChannel.removeEventListener<"message">(type: "message", listener: (this: BroadcastChannel, ev: MessageEvent<any>) => any, options?: boolean | EventListenerOptions): void (+1 overload)

The removeEventListener() method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target.

MDN Reference

removeEventListener
('message',
const handler: (event: MessageEvent) => void
handler
)
},
},
})

A function transport owns its own serialization — here, stringifying every envelope across a MessagePort:

const
const makeJsonTransport: (port: MessagePort) => {
isJson: true;
emit: (message: Message) => void;
receive: (listener: (message: Message, ctx: MessageContext) => void) => void;
}
makeJsonTransport
= (
port: MessagePort
port
:
interface MessagePort

The MessagePort interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other.

MDN Reference

MessagePort class is a global reference for import { MessagePort } from 'worker_threads' https://nodejs.org/api/globals.html#messageport

@sincev15.0.0

MessagePort
) => ({
isJson: true
isJson
: true as
type const = true
const
,
emit: (message: Message) => void
emit
: (
message: Message
message
:
type Message<TModules extends readonly RevivableModule[] = readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), ... 25 more ..., { ...; }]> = MessageBase & MessageVariant<TModules>
Message
) =>
port: MessagePort
port
.
MessagePort.postMessage(message: any, options?: StructuredSerializeOptions): void (+1 overload)

The postMessage() method of the transfers ownership of objects to other browsing contexts.

MDN Reference

postMessage
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
(
message: Message
message
)),
receive: (listener: (message: Message, ctx: MessageContext) => void) => void
receive
: (
listener: (message: Message, ctx: MessageContext) => void
listener
: (
message: Message
message
:
type Message<TModules extends readonly RevivableModule[] = readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), ... 25 more ..., { ...; }]> = MessageBase & MessageVariant<TModules>
Message
,
ctx: MessageContext
ctx
:
type MessageContext = {
port?: MessagePort | WebExtPort;
sender?: WebExtSender;
receiveTransport?: ReceivePlatformTransport;
source?: MessageEventSource | null;
origin?: string;
}
MessageContext
) => void) => {
port: MessagePort
port
.
MessagePort.start(): void

The start() method of the MessagePort interface starts the sending of messages queued on the port.

MDN Reference

start
()
port: MessagePort
port
.
MessagePort.addEventListener<"message">(type: "message", listener: (this: MessagePort, ev: MessageEvent<any>) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('message',
event: MessageEvent<any>
event
=>
listener: (message: Message, ctx: MessageContext) => void
listener
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any

Converts a JavaScript Object Notation (JSON) string into an object.

@paramtext A valid JSON string.

@paramreviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is.

@throws{SyntaxError} If text is not valid JSON.

parse
(
event: MessageEvent<any>
event
.
MessageEvent<any>.data: any

The data read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.

MDN Reference

data
as string) as
type Message<TModules extends readonly RevivableModule[] = readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), ... 25 more ..., { ...; }]> = MessageBase & MessageVariant<TModules>
Message
, {}),
)
},
})

emit and receive don’t have to be functions; you can compose two platform halves. The canonical case is a page talking to its service worker — a ServiceWorker can only emit and a ServiceWorkerContainer can only receive:

const
const registration: ServiceWorkerRegistration
registration
= await
var navigator: Navigator

The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.

MDN Reference

navigator
.
Navigator.serviceWorker: ServiceWorkerContainer

The serviceWorker read-only property of the Navigator interface returns the ServiceWorkerContainer object for the associated document, which provides access to registration, removal, upgrade, and communication with the ServiceWorker. Available only in secure contexts.

MDN Reference

serviceWorker
.
ServiceWorkerContainer.ready: Promise<ServiceWorkerRegistration>

The ready read-only property of the ServiceWorkerContainer interface provides a way of delaying code execution until a service worker is active.

MDN Reference

ready
const
const remote: unknown
remote
= await
expose<unknown, readonly [typeof import("osra/build/revivables/transfer"), typeof import("osra/build/revivables/identity"), typeof import("osra/build/revivables/array-buffer"), typeof import("osra/build/revivables/date"), typeof import("osra/build/revivables/headers"), typeof import("osra/build/revivables/error"), typeof import("osra/build/revivables/typed-array"), ... 20 more ..., {
...;
}], {
...;
}, {
...;
}>(value: {
...;
}, options: StartConnectionsOptions<...> & {
...;
}): Promise<...>
expose
(
const value: {
getAssets: () => Promise<string[]>;
}
value
, {
transport: Transport & {
readonly emit: ServiceWorker;
readonly receive: ServiceWorkerContainer;
}
transport
: {
emit: ServiceWorker
emit
:
const registration: ServiceWorkerRegistration
registration
.
ServiceWorkerRegistration.active: ServiceWorker | null

The active read-only property of the This property is initially set to null.

MDN Reference

active
!,
receive: ServiceWorkerContainer
receive
:
var navigator: Navigator

The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.

MDN Reference

navigator
.
Navigator.serviceWorker: ServiceWorkerContainer

The serviceWorker read-only property of the Navigator interface returns the ServiceWorkerContainer object for the associated document, which provides access to registration, removal, upgrade, and communication with the ServiceWorker. Available only in secure contexts.

MDN Reference

serviceWorker
},
})

(registration.active is typed ServiceWorker | null, hence the assertion; after navigator.serviceWorker.ready it is non-null.)

To forward osra envelopes between two transports without terminating a connection of your own, use relay(). The primitives underneath every transport — sendOsraMessage and registerOsraMessageListener — are covered in low-level messaging.