Docs
Actions send data between peers
An action gives a name to the data your app sends. Use a message action for events, or a request action when you need a reply.
Message actions
Create the same action name on each peer. send(data)
broadcasts to connected peers; pass {target: peerId}
to send to one peer, or an array of IDs to send to a group.
type CursorMove = {x: number; y: number}
const cursor = room.makeAction<CursorMove>('cursor')
cursor.onMessage = (pos, {peerId}) => {
movePeerCursor(peerId, pos)
}
document.onpointermove = e => {
cursor.send({x: e.clientX, y: e.clientY})
}Request actions
Return a value from onRequest to answer a request. The
sender awaits request(), which rejects if the peer
disconnects, the request fails, or the timeout expires.
The timeout covers approval, sending, and waiting for the response.
const isEven = room.makeAction<number, boolean>('is-even', {
kind: 'request',
onRequest: n => n % 2 === 0
})
const result = await isEven.request(42, {
target: peerId,
timeoutMs: 1000
})Request many peers
requestMany() takes an explicit list of peer IDs and resolves
with a result for each: fulfilled, timeout,
rejected, or disconnected. Use
onResult to update your UI as individual answers arrive.
const availability = room.makeAction('availability', {
kind: 'request',
onRequest: ({date}) => calendar.isFree(date)
})
const results = await availability.requestMany(
{date: '2026-05-04'},
{
targets: teammateIds,
timeoutMs: 1000,
onResult: result => updateBadge(result)
}
)
for (const result of results) {
if (result.status === 'fulfilled') {
console.log(result.peerId, result.value)
} else {
console.log(result.peerId, result.status)
}
}Accept or decline incoming data
Since 0.26, both action kinds expose onReceive. Return a
boolean or a promise of one to accept or decline a payload. The context
includes its byte length, peer ID, metadata, kind, and an abort signal.
Request actions use the same callback for requests and responses;
responses have no metadata.
const files = room.makeAction('file', {
onReceive: ({byteLength}) => byteLength <= 100 * 1024 ** 2,
onMessage: (bytes, {metadata}) => {
const blob = new Blob([bytes], {type: metadata?.type})
saveFile(blob, metadata?.name ?? 'download')
}
})
// Approval can also be asynchronous
files.onReceive = ({byteLength, peerId, metadata, signal}) =>
askToAcceptFile({byteLength, peerId, metadata, signal})
Large payloads wait for approval before their contents are sent. Declined
small messages are discarded after arrival. The signal aborts when the
transfer ends, so your approval UI can close. Set onReceive
to null to restore automatic acceptance within the room’s
receive limit.
Set a receive budget
Rooms default to 256 MiB for the maximum incoming payload
and the combined budget for concurrent large transfers. Set
maxReceiveBytes to a positive integer in bytes to change it.
Transfers queue while the budget is occupied. Received files must fit in
memory; this is not a streaming-to-disk API.
const room = joinRoom(
{
appId: 'com.example.files',
maxReceiveBytes: 512 * 1024 ** 2
},
'drop-id'
) byteLength counts payload bytes, excluding metadata. Strings
and JSON are measured as UTF-8. Received binary data is a
Uint8Array, even when the sender passes a File
or Blob.
Progress, cancellation, and delivery
Pass onProgress when sending and set
onReceiveProgress for incoming transfers. Both receive a
fraction from 0 to 1 and a context with the peer ID and optional metadata.
Pass an AbortSignal as signal to cancel a send
or request.
send() resolves when local sending finishes. It does not
confirm that the recipient processed the message. A declined large
transfer rejects with error.kind === 'rejected'. Use a
request action when you need an answer, and handle rejected promises in
your UI.
TypeScript generics describe payloads for your editor; they do not validate incoming values at runtime. Validate data before using it in your app. The display and approval functions in these snippets belong to your UI.