Skip to content

Getting started

If you like to learn by examples, you’re in the right place.
In case you’d rather go through a more in-depth documentation, you can start at the overview.

We’ll go through some basic and more advanced examples to osra in this page.

Terminal window
npm install osra

If you have a web worker and you’d like to call a function from your main thread, using osra, it’s as simple as calling expose(value, options).
A Node.js worker works the same way, with parentPort on the worker side, see transports.

worker.ts
import {
const expose: <T = unknown, const TModules extends readonly RevivableModule[] = 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"), ... 16 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>, TResult = Remote<...>>(value: TValue extends Contextual<infer U> ? Contextual<CapableCheck<...>> : CapableCheck<...>, options: Omit<StartConnectionsOptions<TModules>, "connection"> & {
transport: TTransport;
connection?: (connected: Connected<Remote<T>>) => TResult;
}) => Exposed<TResult>

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
} from 'osra'
export const {
const mult: (a: number, b: number) => Promise<number>
mult
} = await
expose<Payload, 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 ..., {
...;
}], Transport, Capable<...>, {
...;
}>(value: Capable<...>, 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
<
type Payload = {
mult: (a: number, b: number) => number;
}
Payload
>(
{
add: (a: number, b: number) => number
add
: (
a: number
a
: number,
b: number
b
: number) =>
a: number
a
+
b: number
b
},
{
transport: Transport
transport
:
module globalThis
globalThis
}
)
await
const mult: (a: number, b: number) => Promise<number>
mult
(3, 7) // 21
main.ts
import {
const expose: <T = unknown, const TModules extends readonly RevivableModule[] = 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"), ... 16 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>, TResult = Remote<...>>(value: TValue extends Contextual<infer U> ? Contextual<CapableCheck<...>> : CapableCheck<...>, options: Omit<StartConnectionsOptions<TModules>, "connection"> & {
transport: TTransport;
connection?: (connected: Connected<Remote<T>>) => TResult;
}) => Exposed<TResult>

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
} from 'osra'
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
('/worker.ts', {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
export const {
const add: (a: number, b: number) => Promise<number>
add
} = await
expose<Payload, 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 ..., {
...;
}], Transport, Capable<...>, {
...;
}>(value: Capable<...>, 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
<
type Payload = {
add: (a: number, b: number) => number;
}
Payload
>(
{
mult: (a: number, b: number) => number
mult
: (
a: number
a
: number,
b: number
b
: number) =>
a: number
a
*
b: number
b
},
{
transport: Transport
transport
:
const worker: Worker
worker
}
)
await
const add: (a: number, b: number) => Promise<number>
add
(40, 2) // 42

Osra natively supports almost all of the types you’d encounter on the web platform, not just functions.
If you’d like to see every supported types, you can head over at the supported types page.

Functions aren’t limited to the top level of what you expose.
A function passed as an argument becomes a function the worker can call back, an async generator streams its items one at a time, and an AbortSignal cancels work on the other side.

worker.ts
const
const payload: {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}
payload
= {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>
countTo
: async (
n: number
n
: number,
onTick: (i: number) => void
onTick
: (
i: number
i
: number) => void) => {
for (let
let i: number
i
= 1;
let i: number
i
<=
n: number
n
;
let i: number
i
++)
onTick: (i: number) => void
onTick
(
let i: number
i
)
return 'done'
},
fibonacci: () => AsyncGenerator<number, never, unknown>
fibonacci
: async function* () {
let [
let a: number
a
,
let b: number
b
] = [0, 1]
while (true) {
yield
let a: number
a
;[
let a: number
a
,
let b: number
b
] = [
let b: number
b
,
let a: number
a
+
let b: number
b
]
}
},
wait: (ms: number, signal: AbortSignal) => Promise<string>
wait
: (
ms: number
ms
: number,
signal: AbortSignal
signal
:
interface AbortSignal

The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.

MDN Reference

AbortSignal
) =>
new
var Promise: PromiseConstructor
new <string>(executor: (resolve: (value: string | PromiseLike<string>) => void, reject: (reason?: any) => void) => void) => Promise<string>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.

Promise
<string>((
resolve: (value: string | PromiseLike<string>) => void
resolve
,
reject: (reason?: any) => void
reject
) => {
const
const timer: number
timer
=
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
setTimeout
(() =>
resolve: (value: string | PromiseLike<string>) => void
resolve
('finished'),
ms: number
ms
)
signal: AbortSignal
signal
.
AbortSignal.addEventListener<"abort">(type: "abort", listener: (this: AbortSignal, ev: Event) => 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
('abort', () => {
function clearTimeout(id: number | undefined): void
clearTimeout
(
const timer: number
timer
)
reject: (reason?: any) => void
reject
(
signal: AbortSignal
signal
.
AbortSignal.reason: any

The reason read-only property returns a JavaScript value that indicates the abort reason.

MDN Reference

reason
)
})
})
}
export type
type Payload = {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}
Payload
= typeof
const payload: {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}
payload
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 ..., {
...;
}], typeof globalThis, {
...;
}, 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
(
const payload: {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}
payload
, {
transport: Transport & typeof globalThis
transport
:
module globalThis
globalThis
})
main.ts
import type {
type Payload = {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}
Payload
} from './worker'
import {
const expose: <T = unknown, const TModules extends readonly RevivableModule[] = 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"), ... 16 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>, TResult = Remote<...>>(value: TValue extends Contextual<infer U> ? Contextual<CapableCheck<...>> : CapableCheck<...>, options: Omit<StartConnectionsOptions<TModules>, "connection"> & {
transport: TTransport;
connection?: (connected: Connected<Remote<T>>) => TResult;
}) => Exposed<TResult>

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
} from 'osra'
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
('/worker.ts', {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
const {
const countTo: (n: number, onTick: (i: number) => void) => Promise<string>
countTo
,
const fibonacci: () => Promise<AsyncIterableIterator<number>>
fibonacci
,
const wait: (ms: number, signal: AbortSignal) => Promise<string>
wait
} = await
expose<{
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}, 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"), ... 19 more ..., {
...;
}], Transport, Capable<...>, {
...;
}>(value: Capable<...>, 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
<
type Payload = {
countTo: (n: number, onTick: (i: number) => void) => Promise<string>;
fibonacci: () => AsyncGenerator<number, never, unknown>;
wait: (ms: number, signal: AbortSignal) => Promise<string>;
}
Payload
>({}, {
transport: Transport
transport
:
const worker: Worker
worker
})
await
const countTo: (n: number, onTick: (i: number) => void) => Promise<string>
countTo
(3,
i: number
i
=>
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
i: number
i
)) // logs 1, 2, 3 and resolves with 'done'
for await (const
const n: number
n
of await
const fibonacci: () => Promise<AsyncIterableIterator<number>>
fibonacci
()) {
if (
const n: number
n
> 20) break // stops the generator in the worker too
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const n: number
n
) // 0, 1, 1, 2, 3, 5, 8, 13
}
const
const controller: AbortController
controller
= new
var AbortController: new () => AbortController

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

MDN Reference

AbortController
()
const
const pending: Promise<string>
pending
=
const wait: (ms: number, signal: AbortSignal) => Promise<string>
wait
(10_000,
const controller: AbortController
controller
.
AbortController.signal: AbortSignal

The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.

MDN Reference

signal
)
const controller: AbortController
controller
.
AbortController.abort(reason?: any): void

The abort() method of the AbortController interface aborts an asynchronous operation before it has completed. This is able to abort fetch requests, the consumption of any response bodies, or streams.

MDN Reference

abort
(new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('changed my mind'))
await
const pending: Promise<string>
pending
// rejects with Error('changed my mind')

Everything you pass along goes through the same treatment, whatever its depth.
The onTick callback arrives in the worker as an async function, the generator’s next() and return() are proxied so break cleans up on the worker side, and aborting the signal aborts its twin in the worker with the same reason.

One thing to note is that every call and every generator item is a round trip.
That is fine for the examples above, but for bulk data prefer a ReadableStream, which pipelines, see revivables.

In osra there is no client and no server: both sides expose a value, and both sides get the other’s back.
This worker exposes two functions that take a stream, and calls a log function the page exposed as soon as they are connected.

worker.ts
import {
const expose: <T = unknown, const TModules extends readonly RevivableModule[] = 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"), ... 16 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>, TResult = Remote<...>>(value: TValue extends Contextual<infer U> ? Contextual<CapableCheck<...>> : CapableCheck<...>, options: Omit<StartConnectionsOptions<TModules>, "connection"> & {
transport: TTransport;
connection?: (connected: Connected<Remote<T>>) => TResult;
}) => Exposed<TResult>

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
,
const transfer: <T>(value: T) => T

Opt into transfer (move) semantics for a transferable value. Idempotent; non-transferable inputs pass through unchanged. Silently degrades to a copy when the platform/transport can't transfer the given type. Lies at the type level - runtime value is a TransferWrapper typed as T.

transfer
} from 'osra'
type
type PageApi = {
log: (line: string) => void;
}
PageApi
= {
log: (line: string) => void
log
: (
line: string
line
: string) => void }
const
const payload: {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}
payload
= {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>
sha256
: async (
stream: ReadableStream<Uint8Array<ArrayBuffer>>
stream
:
interface ReadableStream<R = any>

The ReadableStream interface of the Streams API represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object.

MDN Reference

ReadableStream
<
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
<
interface ArrayBuffer

Represents a raw buffer of binary data, which is used to store data for the different typed arrays. ArrayBuffers cannot be read from or written to directly, but can be passed to a typed array or DataView Object to interpret the raw buffer as needed.

ArrayBuffer
>>) => {
const
const bytes: ArrayBuffer
bytes
= await new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
(
stream: ReadableStream<Uint8Array<ArrayBuffer>>
stream
).
Body.arrayBuffer(): Promise<ArrayBuffer>
arrayBuffer
()
const
const digest: ArrayBuffer
digest
= await
var crypto: Crypto
crypto
.
Crypto.subtle: SubtleCrypto

The Crypto.subtle read-only property returns a SubtleCrypto which can then be used to perform low-level cryptographic operations. Available only in secure contexts.

MDN Reference

subtle
.
SubtleCrypto.digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise<ArrayBuffer>

The digest() method of the SubtleCrypto interface generates a digest of the given data, using the specified hash function. A digest is a short fixed-length value derived from some variable-length input. Cryptographic digests should exhibit collision-resistance, meaning that it's hard to come up with two different inputs that have the same digest value.

MDN Reference

digest
('SHA-256',
const bytes: ArrayBuffer
bytes
)
return
var Array: ArrayConstructor
Array
.
ArrayConstructor.from<number, string>(iterable: Iterable<number> | ArrayLike<number>, mapfn: (v: number, k: number) => string, thisArg?: any): string[] (+3 overloads)

Creates an array from an iterable object.

@paramiterable An iterable object to convert to an array.

@parammapfn A mapping function to call on every element of the array.

@paramthisArg Value of 'this' used to invoke the mapfn.

from
(new
var Uint8Array: Uint8ArrayConstructor
new <ArrayBuffer>(buffer: ArrayBuffer, byteOffset?: number, length?: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(
const digest: ArrayBuffer
digest
),
byte: number
byte
=>
byte: number
byte
.
Number.toString(radix?: number): string

Returns a string representation of an object.

@paramradix Specifies a radix for converting numeric values to strings. This value is only used for numbers.

toString
(16).
String.padStart(maxLength: number, fillString?: string): string

Pads the current string with a given string (possibly repeated) so that the resulting string reaches a given length. The padding is applied from the start (left) of the current string.

@parammaxLength The length of the resulting string once the current string has been padded. If this parameter is smaller than the current string's length, the current string will be returned as it is.

@paramfillString The string to pad the current string with. If this string is too long, it will be truncated and the left-most part will be applied. The default value for this parameter is " " (U+0020).

padStart
(2, '0')).
Array<string>.join(separator?: string): string

Adds all the elements of an array into a string, separated by the specified separator string.

@paramseparator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.

join
('')
},
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>
gzip
: (
stream: ReadableStream<Uint8Array<ArrayBuffer>>
stream
:
interface ReadableStream<R = any>

The ReadableStream interface of the Streams API represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object.

MDN Reference

ReadableStream
<
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
<
interface ArrayBuffer

Represents a raw buffer of binary data, which is used to store data for the different typed arrays. ArrayBuffers cannot be read from or written to directly, but can be passed to a typed array or DataView Object to interpret the raw buffer as needed.

ArrayBuffer
>>) =>
transfer<ReadableStream<Uint8Array<ArrayBuffer>>>(value: ReadableStream<Uint8Array<ArrayBuffer>>): ReadableStream<Uint8Array<ArrayBuffer>>

Opt into transfer (move) semantics for a transferable value. Idempotent; non-transferable inputs pass through unchanged. Silently degrades to a copy when the platform/transport can't transfer the given type. Lies at the type level - runtime value is a TransferWrapper typed as T.

transfer
(
stream: ReadableStream<Uint8Array<ArrayBuffer>>
stream
.
ReadableStream<Uint8Array<ArrayBuffer>>.pipeThrough<Uint8Array<ArrayBuffer>>(transform: ReadableWritablePair<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>, options?: StreamPipeOptions): ReadableStream<Uint8Array<ArrayBuffer>>

The pipeThrough() method of the ReadableStream interface provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair.

MDN Reference

pipeThrough
(new
var CompressionStream: new (format: CompressionFormat) => CompressionStream

The CompressionStream interface of the Compression Streams API compresses a stream of data. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods.

MDN Reference

CompressionStream
('gzip')))
}
export type
type Payload = {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}
Payload
= typeof
const payload: {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}
payload
const
const page: {
log: (line: string) => Promise<void>;
}
page
= await
expose<PageApi, 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 ..., {
...;
}], Transport, Capable<...>, {
...;
}>(value: Capable<...>, 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
<
type PageApi = {
log: (line: string) => void;
}
PageApi
>(
const payload: {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}
payload
, {
transport: Transport
transport
:
module globalThis
globalThis
})
await
const page: {
log: (line: string) => Promise<void>;
}
page
.
log: (line: string) => Promise<void>
log
('ready')
main.ts
import type {
type Payload = {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}
Payload
} from './worker'
import {
const expose: <T = unknown, const TModules extends readonly RevivableModule[] = 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"), ... 16 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>, TResult = Remote<...>>(value: TValue extends Contextual<infer U> ? Contextual<CapableCheck<...>> : CapableCheck<...>, options: Omit<StartConnectionsOptions<TModules>, "connection"> & {
transport: TTransport;
connection?: (connected: Connected<Remote<T>>) => TResult;
}) => Exposed<TResult>

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
,
const transfer: <T>(value: T) => T

Opt into transfer (move) semantics for a transferable value. Idempotent; non-transferable inputs pass through unchanged. Silently degrades to a copy when the platform/transport can't transfer the given type. Lies at the type level - runtime value is a TransferWrapper typed as T.

transfer
} from 'osra'
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
('/worker.ts', {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
const
const controller: AbortController
controller
= new
var AbortController: new () => AbortController

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

MDN Reference

AbortController
()
const {
const sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>
sha256
,
const gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<ReadableStream<Uint8Array<ArrayBuffer>>>
gzip
} = await
expose<{
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}, 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: 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
<
type Payload = {
sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>;
gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => ReadableStream<Uint8Array<ArrayBuffer>>;
}
Payload
>(
{
log: (line: string) => void
log
: (
line: string
line
: string) =>
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(`worker says: ${
line: string
line
}`) }, // logs "worker says: ready"
{
transport: Transport
transport
:
const worker: Worker
worker
,
unregisterSignal?: AbortSignal | undefined
unregisterSignal
:
const controller: AbortController
controller
.
AbortController.signal: AbortSignal

The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.

MDN Reference

signal
}
)
const
const file: File
file
= new
var File: new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) => File

The File interface provides information about files and allows JavaScript in a web page to access their content.

MDN Reference

File
(['hello osra'], 'hello.txt')
// the file's chunks are moved to the worker instead of being copied
await
const sha256: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<string>
sha256
(
transfer<ReadableStream<Uint8Array<ArrayBuffer>>>(value: ReadableStream<Uint8Array<ArrayBuffer>>): ReadableStream<Uint8Array<ArrayBuffer>>

Opt into transfer (move) semantics for a transferable value. Idempotent; non-transferable inputs pass through unchanged. Silently degrades to a copy when the platform/transport can't transfer the given type. Lies at the type level - runtime value is a TransferWrapper typed as T.

transfer
(
const file: File
file
.
Blob.stream(): ReadableStream<Uint8Array<ArrayBuffer>>

The stream() method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the Blob.

MDN Reference

stream
())) // 'e3c59cf7…'
// the compressed chunks stream back the same way, as the worker produces them
const
const compressed: Blob
compressed
= await new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
(await
const gzip: (stream: ReadableStream<Uint8Array<ArrayBuffer>>) => Promise<ReadableStream<Uint8Array<ArrayBuffer>>>
gzip
(
transfer<ReadableStream<Uint8Array<ArrayBuffer>>>(value: ReadableStream<Uint8Array<ArrayBuffer>>): ReadableStream<Uint8Array<ArrayBuffer>>

Opt into transfer (move) semantics for a transferable value. Idempotent; non-transferable inputs pass through unchanged. Silently degrades to a copy when the platform/transport can't transfer the given type. Lies at the type level - runtime value is a TransferWrapper typed as T.

transfer
(
const file: File
file
.
Blob.stream(): ReadableStream<Uint8Array<ArrayBuffer>>

The stream() method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the Blob.

MDN Reference

stream
()))).
Body.blob(): Promise<Blob>
blob
()
const controller: AbortController
controller
.
AbortController.abort(reason?: any): void

The abort() method of the AbortController interface aborts an asynchronous operation before it has completed. This is able to abort fetch requests, the consumption of any response bodies, or streams.

MDN Reference

abort
() // done with the worker, close the connection

A ReadableStream is never copied as a whole, it’s proxied chunk by chunk with backpressure, so the page starts receiving compressed chunks while the worker is still reading the file.
Wrapping a stream in transfer() moves each chunk’s buffer instead of copying it, and leaving it out still works, just with a copy per chunk.

Aborting unregisterSignal closes the connection on both sides and rejects anything still in flight, see errors and lifecycle.

From here, transport modes explains which values can cross which channels, and transports shows the same expose() call on iframes, shared workers, WebSockets and web extensions.