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).
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.
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.
// when the remote sends back identity(saved): saved === settings
},
}, {
transport: Transport & Worker
transport:
constworker: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 {
consttransfer: <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.
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(
constpixels: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.