Skip to content

Custom transports

If osra’s default does not fit your needs, osra support custom transports. The custom transport interface is simplified as such in the expose(value, options)’s options.transport parameter.

type CustomTransport = {
emit?: (message: Message, transferables?: Transferable[]) => void
receive?: (listener: (event: Message, messageContext: MessageContext) => void) => void
isJson?: boolean
}

In truth though, the emit and receive functions allows a wider range of type such as any of the platform transports allowed values. This expanded type allows us to properly handle connections through Iframes.

For example, emit needs to window.parent.postMessage to the parent window, while receive listens for messages on window.addEventListener('message', ...).

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"), typeof import("osra/build/revivables/promise"), typeof import("osra/build/revivables/function"), typeof import("osra/build/revivables/message-port"), typeof import("osra/build/revivables/readable-stream"), typeof import("osra/build/revivables/writable-stream"), ... 15 more ..., {
...;
}], {
...;
}, {}, unknown>(value: {}, options: Omit<...> & {
...;
}): Exposed<...>

Expose a value to whoever connects, and get back what they exposed.

Wrap value in context to build it once per connection, which is what lets one server answer each realm differently (scoped resolvers per app) instead of sharing one object across all of them. A bare function stays a plain exposed endpoint, so the wrapper is what disambiguates the two.

The result is both awaitable and async-iterable: awaiting gives the first peer, iterating gives every peer as it connects. Both hand back the same shape.

const remote = await expose(resolvers, { transport }) // the first peer's value
for await (const remote of expose(resolvers, { transport })) { } // every peer's value

connection decides what that shape is. Omit it and it is the peer's value, which is what expose has always resolved to. Return whatever a connection should mean instead:

const { value, context } = await expose(resolvers, {
transport,
connection: ({ value, context }) => ({ value, context }),
})
for await (const peer of expose(resolvers, {
transport,
connection: ({ value, context }) => ({ value, context }),
})) {
if (!allowed(peer.context.origin)) peer.context.abort?.()
}

A peer's identity is whatever the transport can observe merged over whatever the caller declared in context. Only a window message carries a browser-set origin and source; a MessagePort message carries neither, so a port-based server declares what it learned when it received the port. Observed fields win over declared ones, so a declaration can never spoof a real origin.

expose
({}, {
transport: Transport & {
readonly emit: Window;
readonly receive: Window & typeof globalThis;
}
transport
: {
emit: Window
emit
:
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
parent: Window

The Window.parent property is a reference to the parent of the current window or subframe.

MDN Reference

parent
,
receive: Window & typeof globalThis
receive
:
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
} })

isJson tells osra whether your channel can carry structured-clone values.

The second argument to emit is the list of values that should be moved instead of copied. Forward it if your channel supports transfer: If you do not pass this option, transferable values will be copied instead of moved.

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"), typeof import("osra/build/revivables/promise"), typeof import("osra/build/revivables/function"), typeof import("osra/build/revivables/message-port"), typeof import("osra/build/revivables/readable-stream"), typeof import("osra/build/revivables/writable-stream"), ... 15 more ..., {
...;
}], {
...;
}, {}, unknown>(value: {}, options: Omit<...> & {
...;
}): Exposed<...>

Expose a value to whoever connects, and get back what they exposed.

Wrap value in context to build it once per connection, which is what lets one server answer each realm differently (scoped resolvers per app) instead of sharing one object across all of them. A bare function stays a plain exposed endpoint, so the wrapper is what disambiguates the two.

The result is both awaitable and async-iterable: awaiting gives the first peer, iterating gives every peer as it connects. Both hand back the same shape.

const remote = await expose(resolvers, { transport }) // the first peer's value
for await (const remote of expose(resolvers, { transport })) { } // every peer's value

connection decides what that shape is. Omit it and it is the peer's value, which is what expose has always resolved to. Return whatever a connection should mean instead:

const { value, context } = await expose(resolvers, {
transport,
connection: ({ value, context }) => ({ value, context }),
})
for await (const peer of expose(resolvers, {
transport,
connection: ({ value, context }) => ({ value, context }),
})) {
if (!allowed(peer.context.origin)) peer.context.abort?.()
}

A peer's identity is whatever the transport can observe merged over whatever the caller declared in context. Only a window message carries a browser-set origin and source; a MessagePort message carries neither, so a port-based server declares what it learned when it received the port. Observed fields win over declared ones, so a declaration can never spoof a real origin.

expose
(
{},
{
transport: Transport & {
readonly receive: Window & typeof globalThis;
readonly emit: (message: Message, transferables: Transferable[] | undefined) => void;
}
transport
: {
receive: Window & typeof globalThis
receive
:
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
,
emit: (message: Message, transferables: Transferable[] | undefined) => void
emit
: (
message: Message
message
,
transferables: Transferable[] | undefined
transferables
) =>
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
parent: Window

The Window.parent property is a reference to the parent of the current window or subframe.

MDN Reference

parent
.
Window.postMessage(message: any, options?: WindowPostMessageOptions): void (+1 overload)

The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.

MDN Reference

postMessage
(
message: Message
message
, {
StructuredSerializeOptions.transfer?: Transferable[] | undefined
transfer
:
transferables: Transferable[] | undefined
transferables
})
}
}
)

If you ever need to connect two contexts that cannot see each other directly, use relay() to forward osra traffic. For example, you could forward osra traffic from a worker to an iframe.

const
const worker: Worker
worker
= new
var Worker: new (scriptURL: string | URL, options?: WorkerOptions) => Worker

The Worker interface of the Web Workers API represents a background task that can be created via script, which can send messages back to its creator.

MDN Reference

Worker
('')
const
const iframe: HTMLIFrameElement
iframe
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<"iframe">(selectors: "iframe"): HTMLIFrameElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('iframe')!
function relay(transportA: Transport, transportB: Transport, { key, origin, originA, originB, nameA, nameB, unregisterSignal, }?: RelayOptions): void
relay
(
const worker: Worker
worker
, {
emit: Window
emit
:
const iframe: HTMLIFrameElement
iframe
.
HTMLIFrameElement.contentWindow: Window | null

The contentWindow property returns the Window object of an HTMLIFrameElement.

MDN Reference

contentWindow
!,
receive: Window & typeof globalThis
receive
:
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
})