Skip to content

identity() and transfer()

By default, every send produces an independent copy of the value on the peer. identity() and transfer() are the two opt-outs: identity() preserves reference identity across the connection, and transfer() moves ownership of a Transferable instead of copying it.

Without a wrapper, each send is a fresh copy, including the return trip. If the peer receives a revived value and passes it back bare, it arrives as yet another copy, so the returning side must re-wrap it. Two fields pointing at the same object also arrive as two copies (see Limitations).

identity(value) preserves reference identity across the connection:

  • Sending the same wrapped value twice revives as the same object on the peer.
  • When the peer wraps the revived object in identity() and sends it back, you receive your original reference (===).
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
,
const identity: <T>(value: T) => T

Wrap a value so osra preserves reference identity across the RPC boundary. Idempotent; primitives pass through unchanged. Lies at the type level - runtime value is an IdentityWrapper typed as T.

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"), ... 20 more ..., {
...;
}], Worker, {
...;
}>(value: {
...;
}, options: StartConnectionsOptions<...> & {
...;
}): Promise<...>
expose
({
getSettings: () => Promise<{
theme: string;
}>
getSettings
: async () =>
identity<{
theme: string;
}>(value: {
theme: string;
}): {
theme: string;
}

Wrap a value so osra preserves reference identity across the RPC boundary. Idempotent; primitives pass through unchanged. Lies at the type level - runtime value is an IdentityWrapper typed as T.

identity
(
const settings: {
theme: string;
}
settings
),
saveSettings: (saved: typeof settings) => Promise<void>
saveSettings
: async (
saved: {
theme: string;
}
saved
: typeof
const settings: {
theme: string;
}
settings
) => {
// when the remote sends back identity(saved): saved === settings
},
}, {
transport: Transport & Worker
transport
:
const worker: Worker
worker
})

The per-connection caches behind this are GC-aware: when the sender’s original gets garbage-collected, the peer is notified and drops its cached revived value. identity() is idempotent, and primitives pass through unchanged; see the identity() reference for the exact signature and the identity-dispose wire behavior.

Because later sends of an already-tracked reference ship only an id instead of the full payload, identity() also dedupes repeat sends of large objects; see Performance.

transfer(value) opts a Transferable (ArrayBuffer, MessagePort, streams, ImageBitmap, OffscreenCanvas, …) into move semantics: ownership transfers to the peer instead of copying, and the value is detached locally.

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

On JSON transports there is nothing to transfer, so transfer() silently degrades to a copy.

Some types are always moved, with or without transfer(), because structured clone cannot copy them: MessagePort, TransformStream, OffscreenCanvas, MediaSourceHandle, MediaStreamTrack, MIDIAccess, RTCDataChannel, and WebTransport streams. A bare send still detaches them locally; transfer() adds nothing there. It’s the clonable Transferables (ArrayBuffer, typed-array views, ImageBitmap, VideoFrame, AudioData) where transfer() makes the difference between a copy and a move.

Like identity(), transfer() is idempotent, and non-transferable inputs pass through; see the transfer() reference for the exact signature.