Skip to content

identity() and transfer()

Osra comes with two small helpers that change how a value crosses a connection:

  • identity(value) keeps the value’s reference stable, so the other side sees one object for it no matter how many times you send it.
  • transfer(value) moves the value instead of copying it, which is a lot cheaper for large buffers.

Both of them give you back the exact value you passed in, with the same type, so you can drop them into an existing call without changing any signature.
Both are also no-ops on values they don’t apply to, wrapping a string in either of them does nothing.

By default, every send is a copy.
If you send the same object twice, the other side ends up with two unrelated copies of it.

Wrapping the value in identity() once ties it to its reference instead.
From then on, the other side sees a single object for it, on every send:

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 identity: <T>(value: T) => T

Mark a value so osra preserves its reference identity across the boundary. The peer's revived value stands for this one, and handing it back - to you, or onward to a further context and back again - resolves to this very reference. The mark sticks to the value, so only the side that owns it has to opt in. Idempotent, and primitives pass through unchanged.

identity
} from 'osra'
const
const plain: {
foo: string;
}
plain
= {
foo: string
foo
: 'bar' }
const
const shared: {
foo: string;
}
shared
= {
foo: string
foo
: 'bar' }
const
const payload: {
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: string;
};
}
payload
= {
plain1: {
foo: string;
}
plain1
:
const plain: {
foo: string;
}
plain
,
plain2: {
foo: string;
}
plain2
:
const plain: {
foo: string;
}
plain
,
ref1: {
foo: string;
}
ref1
:
identity<{
foo: string;
}>(value: {
foo: string;
}): {
foo: string;
}

Mark a value so osra preserves its reference identity across the boundary. The peer's revived value stands for this one, and handing it back - to you, or onward to a further context and back again - resolves to this very reference. The mark sticks to the value, so only the side that owns it has to opt in. Idempotent, and primitives pass through unchanged.

identity
(
const shared: {
foo: string;
}
shared
),
ref2: {
foo: string;
}
ref2
:
const shared: {
foo: string;
}
shared
}
export type
type Payload = {
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: string;
};
}
Payload
= typeof
const payload: {
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: 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: {
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: string;
};
}
payload
, {
transport: Transport & typeof globalThis
transport
:
module globalThis
globalThis
})
main.ts
import type {
type Payload = {
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: 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 plain1: {
foo: string;
}
plain1
,
const plain2: {
foo: string;
}
plain2
,
const ref1: {
foo: string;
}
ref1
,
const ref2: {
foo: string;
}
ref2
} = await
expose<{
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: 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"), typeof import("osra/build/revivables/function"), typeof import("osra/build/revivables/message-port"), typeof import("osra/build/revivables/readable-stream"), ... 16 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 = {
plain1: {
foo: string;
};
plain2: {
foo: string;
};
ref1: {
foo: string;
};
ref2: {
foo: string;
};
}
Payload
>({}, {
transport: Transport
transport
:
const worker: Worker
worker
})
const plain1: {
foo: string;
}
plain1
===
const plain2: {
foo: string;
}
plain2
// false, the same object in two places arrives as two copies
const ref1: {
foo: string;
}
ref1
===
const ref2: {
foo: string;
}
ref2
// true, one object, and marking it once was enough

One thing to note is that ref2 was sent without any wrapper.
The mark lives on the value, not on that one send, so every later send of it resolves to the same object on the other side.

Since the mark travels with the value, the peer doesn’t have to do anything to hand it back.
Whatever you gave out, you get back as your actual original object:

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 identity: <T>(value: T) => T

Mark a value so osra preserves its reference identity across the boundary. The peer's revived value stands for this one, and handing it back - to you, or onward to a further context and back again - resolves to this very reference. The mark sticks to the value, so only the side that owns it has to opt in. Idempotent, and primitives pass through unchanged.

identity
} from 'osra'
const
const settings: {
theme: string;
}
settings
= {
theme: string
theme
: 'dark' }
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
({
getSettings: () => {
theme: string;
}
getSettings
: () =>
identity<{
theme: string;
}>(value: {
theme: string;
}): {
theme: string;
}

Mark a value so osra preserves its reference identity across the boundary. The peer's revived value stands for this one, and handing it back - to you, or onward to a further context and back again - resolves to this very reference. The mark sticks to the value, so only the side that owns it has to opt in. Idempotent, and primitives pass through unchanged.

identity
(
const settings: {
theme: string;
}
settings
),
saveSettings: (saved: typeof settings) => void
saveSettings
: (
saved: {
theme: string;
}
saved
: typeof
const settings: {
theme: string;
}
settings
) => {
saved: {
theme: string;
}
saved
===
const settings: {
theme: string;
}
settings
// true, the peer just sent back what it was given
}
}, {
transport: Transport & typeof globalThis
transport
:
module globalThis
globalThis
})

This is what makes remote callbacks removable: removeEventListener needs the exact function that was registered, and osra’s own EventTarget proxy uses identity() for exactly that.

Keep in mind that what travels is the reference, not the contents.
The peer’s copy is still its own object, so a change made on one side is not synced to the other.

An identity keeps working however far the value travels.
A context that received one can pass it on to the next, and whatever comes back resolves at each hop to exactly the value that hop handed out, all the way to the origin:

page.ts
const
const session: {
user: string;
}
session
= {
user: string
user
: 'ada' }
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 ..., {
...;
}], Worker, {
...;
}, 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
({
getSession: () => Promise<{
user: string;
}>
getSession
: async () =>
identity<{
user: string;
}>(value: {
user: string;
}): {
user: string;
}

Mark a value so osra preserves its reference identity across the boundary. The peer's revived value stands for this one, and handing it back - to you, or onward to a further context and back again - resolves to this very reference. The mark sticks to the value, so only the side that owns it has to opt in. Idempotent, and primitives pass through unchanged.

identity
(
const session: {
user: string;
}
session
),
close: (returned: typeof session) => Promise<void>
close
: async (
returned: {
user: string;
}
returned
: typeof
const session: {
user: string;
}
session
) => {
returned: {
user: string;
}
returned
===
const session: {
user: string;
}
session
// true, however many contexts it went through
}
}, {
transport: Transport & Worker
transport
:
const worker: Worker
worker
})
worker.ts
// the middle context forwards both ways and marks nothing itself
const
const page: {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
page
= await
expose<Page, 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 Page = {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
Page
>({}, {
transport: Transport
transport
:
module globalThis
globalThis
})
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 ..., {
...;
}], Worker, {
...;
}, 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
({
getSession: () => Promise<{
user: string;
}>
getSession
: async () =>
const page: {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
page
.
getSession: () => Promise<{
user: string;
}>
getSession
(),
close: (session: {
user: string;
}) => Promise<void>
close
: async (
session: {
user: string;
}
session
: {
user: string
user
: string }) =>
const page: {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
page
.
close: (session: {
user: string;
}) => Promise<void>
close
(
session: {
user: string;
}
session
),
}, {
transport: Transport & Worker
transport
:
const child: Worker
child
})
child.ts
const
const middle: {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
middle
= await
expose<Middle, 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 Middle = {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
Middle
>({}, {
transport: Transport
transport
:
module globalThis
globalThis
})
const
const session: {
user: string;
}
session
= await
const middle: {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
middle
.
getSession: () => Promise<{
user: string;
}>
getSession
()
await
const middle: {
getSession: () => Promise<{
user: string;
}>;
close: (session: {
user: string;
}) => Promise<void>;
}
middle
.
close: (session: {
user: string;
}) => Promise<void>
close
(
const session: {
user: string;
}
session
)

Sending it costs the full payload the first time a given peer sees it, and only a small reference on every send after that.

One thing to note is that the value comes home the way it went out.
Each context resolves identities per peer, so if the same value reaches a context through two different routes, that context ends up with two different references, each tied to whoever sent it.

Each side only holds on to the peer’s identities for as long as the original value is alive.
When your value gets garbage collected, osra tells the peer to drop its copy, and a peer that had passed it further along tells its own peer in turn, so a chain unwinds from the origin outward.

A few small things worth knowing:

  • Primitives pass through identity() untouched, there is no reference to keep.
  • Marking the same value twice does nothing extra.
  • Unique symbols (Symbol()) ride this machinery automatically, which is why they keep their identity across a connection without you wrapping anything, see supported types.

Osra copies transferable values by default, just like postMessage() does when you don’t give it a transfer list.
Wrapping a value in transfer() moves it instead, which is the difference between duplicating 16 MB of pixels and handing over a pointer:

const
const pixels: ArrayBuffer
pixels
= new
var ArrayBuffer: ArrayBufferConstructor
new (byteLength: number, options?: {
maxByteLength?: number;
}) => ArrayBuffer (+2 overloads)
ArrayBuffer
(16_000_000)
await
const render: (pixels: ArrayBuffer) => Promise<void>
render
(
transfer<ArrayBuffer>(value: ArrayBuffer): 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 pixels: ArrayBuffer
pixels
))
const pixels: ArrayBuffer
pixels
.
ArrayBuffer.byteLength: number

Read-only. The length of the ArrayBuffer (in bytes).

byteLength
// 0, it now belongs to the peer

Transfer semantics are the platform’s: the moment the value ships, it is detached on your side, and reading it afterwards is an error.
There is only ever one owner.

Here is what wrapping each kind of value does on a structured transport:

Value Default Wrapped in transfer()
ArrayBuffer copied moved
Typed array spanning its whole buffer copied backing buffer moved
Typed array over part of its buffer its window copied its window sliced out and moved, buffer intact
DataView copied whole backing buffer moved
ImageBitmap, VideoFrame, AudioData copied moved
ReadableStream, WritableStream proxied, chunk contents copied proxied, chunk contents moved
Request, Response body proxied, chunk contents copied body proxied, chunk contents moved
MessagePort, TransformStream, OffscreenCanvas moved anyway moved anyway
SharedArrayBuffer shared shared, see below

Anything else passes through transfer() unchanged, so wrapping a plain object is harmless, and wrapping the same value twice does nothing extra.

Keep in mind that a custom transport only moves values when its emit forwards the transferables list.
Otherwise everything falls back to a copy.

A typed array that spans its whole buffer moves that buffer, detaching it on your side.
A view over part of a buffer only ever ships the bytes it can see, so that window is sliced out first and the slice is what moves, leaving your buffer intact:

transfer(new Uint8Array(buffer)) // moves, buffer is detached
transfer(new Uint8Array(buffer, 8, 4)) // ships those 4 bytes, buffer is fine

DataView is the exception: wrapping one always moves its entire backing buffer, even when the view only covers part of it.
The view arrives with its window intact over the moved buffer, and the whole buffer is detached on your side.

A ReadableStream or a WritableStream is always proxied chunk by chunk, the stream itself never moves.
Wrapping one in transfer() keeps that proxying exactly as it is, and moves every transferable found inside each chunk instead of copying it.
The same goes for the body of a wrapped Request or Response.

The details, including per-chunk wrapping and the detach caveats, are in revivables.

Some host objects cannot be copied by structured clone at all, so on a structured transport they move whether you wrapped them or not:
MessagePort, TransformStream, OffscreenCanvas, MediaStreamTrack, MediaSourceHandle, MIDIAccess, RTCDataChannel, WebTransportSendStream, WebTransportReceiveStream.

This means that sending one of these detaches it on your side, every time.

SharedArrayBuffer is the opposite case: it is neither copied nor moved, both contexts simply look at the same memory.

A JSON transport cannot move anything, there is no memory to hand over in a text protocol.
On those, transfer() quietly degrades to a copy: same code, no error.

Most of the values in the table above are not available on JSON transports at all, see the supported types table.
MessagePort still works there because osra proxies it instead of moving it.