Skip to content

Getting started

osra is a zero-runtime-dependency TypeScript RPC library that connects two JavaScript contexts over any message channel. This page installs the package and wires a page to a Web Worker; the same expose() call works over every other transport.

Terminal window
npm install osra

One ESM module, zero runtime dependencies. The published declarations require TypeScript >= 5.9 with strict mode.

Expose an API inside the worker:

worker.ts
import type {
type Transport = PlatformTransport | CustomTransport
Transport
} from 'osra'
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"), ... 21 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>>(value: CapableCheck<...>, options: StartConnectionsOptions<TModules> & {
transport: TTransport;
}) => Promise<Remote<T>>
expose
} from 'osra'
const
const api: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, unknown>;
}
api
= {
add: (a: number, b: number) => Promise<number>
add
: async (
a: number
a
: number,
b: number
b
: number) =>
a: number
a
+
b: number
b
,
makeCounter: () => Promise<() => Promise<number>>
makeCounter
: async () => {
let
let count: number
count
= 0
return async () => ++
let count: number
count
},
streamData: () => AsyncGenerator<number, void, unknown>
streamData
: async function* () {
for (let
let i: number
i
= 0;
let i: number
i
< 3;
let i: number
i
++) yield
let i: number
i
},
}
export type
type Api = {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, unknown>;
}
Api
= typeof
const api: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, unknown>;
}
api
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 ..., {
...;
}], Transport, {
...;
}>(value: {
...;
}, options: StartConnectionsOptions<...> & {
...;
}): Promise<...>
expose
(
const api: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, unknown>;
}
api
, {
transport: Transport
transport
:
module globalThis
globalThis
as unknown as
type Transport = PlatformTransport | CustomTransport
Transport
})

Consume it from the page:

// main.ts
import type {
type Api = {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, unknown>;
}
Api
} 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"), ... 21 more ..., {
...;
}], const TTransport extends Transport = Transport, const TValue = Capable<...>>(value: CapableCheck<...>, options: StartConnectionsOptions<TModules> & {
transport: TTransport;
}) => Promise<Remote<T>>
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
(new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URL.

MDN Reference

URL
('./worker.ts', import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.url: string

The absolute file: URL of the module.

This is defined exactly the same as it is in browsers providing the URL of the current module file.

This enables useful patterns such as relative file loading:

import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));

url
), {
WorkerOptions.type?: WorkerType | undefined
type
: 'module' })
const
const remote: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => Promise<AsyncIterableIterator<number>>;
}
remote
= await
expose<{
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, 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"), ... 22 more ..., {
...;
}], Transport, Capable<...>>(value: Capable<...>, options: StartConnectionsOptions<...> & {
...;
}): Promise<...>
expose
<
type Api = {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => AsyncGenerator<number, void, unknown>;
}
Api
>({}, {
transport: Transport
transport
:
const worker: Worker
worker
})
await
const remote: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => Promise<AsyncIterableIterator<number>>;
}
remote
.
add: (a: number, b: number) => Promise<number>
add
(40, 2) // 42
const
const counter: () => Promise<number>
counter
= await
const remote: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => Promise<AsyncIterableIterator<number>>;
}
remote
.
makeCounter: () => Promise<() => Promise<number>>
makeCounter
()
await
const counter: () => Promise<number>
counter
() // 1
await
const counter: () => Promise<number>
counter
() // 2
for await (const
const n: number
n
of await
const remote: {
add: (a: number, b: number) => Promise<number>;
makeCounter: () => Promise<() => Promise<number>>;
streamData: () => Promise<AsyncIterableIterator<number>>;
}
remote
.
streamData: () => Promise<AsyncIterableIterator<number>>
streamData
()) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
const n: number
n
) // 0, 1, 2
}

Both sides call expose(); the returned promise resolves with the remote side’s value once the handshake completes. A side that only serves (like the worker above) can ignore the returned promise, and a side that only consumes passes {}. Functions returned across the boundary stay callable (makeCounter hands back a live counter), and async generators stream with for await.

Option Default Description
transport required The channel to communicate over (see Transports)
key '__OSRA_DEFAULT_KEY__' Namespacing tag that lets multiple independent osra connections share one channel. Not authentication.
origin '*' On window transports: sets the outbound postMessage target origin and filters inbound messages by event.origin
name / remoteName - Label your endpoint / only accept envelopes from a matching peer name
unregisterSignal - AbortSignal that tears the connection down (see Lifecycle)
uuid / remoteUuid random / - Pin instance uuids (remoteUuid is otherwise learned from the peer’s announce); when both sides preset each other’s remoteUuid, the announce handshake is skipped
revivableModules - defaults => modules function to add, drop, reorder, or override revivable modules (see Custom revivables)

If multiple peers connect over the same transport, the returned promise resolves with the first peer’s value; later peers still connect and can call your exposed value. See multi-peer.

See transports for every channel osra runs over — windows and iframes, SharedWorkers, WebSockets, service workers, web extensions, and custom { emit, receive } pairs. Supported types lists everything that crosses the boundary, on both structured-clone and JSON transports. For the full expose() signature, handshake sequence, and error behavior, read the expose() reference.