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.