Messages between rooms

room.bus is the direct route from one of your rooms to another. It is scoped to your project, and there is no way to address a different one.

Use it when something happens in one room that another room needs to know about: a match fills and the notifications hub should tell the players’ friends, or a bracket advances and the lobby should update. The alternative is routing the message through a player’s browser, where the client can edit it, drop it, or close the tab halfway through.

// irtio/rooms/match.ts
room.bus.publish('match.ready', JSON.stringify({ match: room.id, players: 4 }));

Two verbs

They are not two spellings of the same thing. Picking the wrong one is the mistake this API invites.

publishsend
Who gets itevery awake, subscribed roomone room you name
If the target is asleepit misses the messageit wakes up and gets it
Deliverybest effortat least once
Answers younoa promise that resolves or rejects
Use it for“something changed”“this needs to happen”

publish is a broadcast nobody is obliged to hear.

// irtio/rooms/match.ts
room.bus.publish('match.ready', JSON.stringify({ match: room.id, players: 4 }));

Every room of your project that is awake and subscribed to match.ready runs its handler. Hibernated rooms miss it and pick their state up again when they wake. Nothing queues. Nothing wakes. There is no ordering promise between two different channels, and there is not going to be one.

Publish is for facts a room can recompute if it missed them.

send is a delivery.

// irtio/rooms/match.ts
await room.bus.send('hub:global', JSON.stringify({ kind: 'match-ready', match: room.id }));

This one is durable. If hub:global is hibernated, it wakes and runs its handler. That holds while the target has a durable alarm armed or a client connected. A room that has been idle long enough to be dropped entirely is not woken by a send today.

A message that cannot be delivered right now stays parked and is tried again. The promise resolves once the message has been accepted, and rejects with a named error when it cannot be:

ErrorWhat happened
E_BUS_NO_SUCH_ROOMno room by that id in this project, or it is a relay room and runs no code
E_BUS_PAYLOAD_TOO_LARGEover 4 KiB
E_BUS_MAILBOX_FULLthe target has 64 undelivered messages and is not keeping up
E_BUS_RATE_LIMITEDthis room is over its bus budget

Receiving from the bus

Handlers go in the room definition’s config, next to alarms, not in a call you make at runtime. A send wakes a sleeping room, and a handler you registered before the room went to sleep would not be there when it woke up. Config is code, so it is always there.

// irtio/rooms/hub.ts
export default defineRoom(hubSchema, {
  mode: 'event',
  class: 'small',
  bus: {
    channels: {
      'match.ready'(state, event) {
        // event.from is the roomId that published. irtio stamps it.
        state.notices.add(event.payload, { from: event.from, seen: 0 });
      },
    },
    onMessage(state, message) {
      const notice = JSON.parse(message.payload) as { kind: string; match: string };
      // Keyed on the match, so a repeat overwrites instead of doubling. See below.
      state.notices.add(notice.match, { from: message.from, seen: 0 });
    },
  },
});

Declaring a channel in channels also subscribes the room to it, when the room starts and again every time it wakes. Most rooms never need room.bus.subscribe. It is there for a room that wants to leave a channel and rejoin it during its life.

Write the bus onMessage so it can run twice

send is at least once, and your handler can see the same message twice for one send. A delivery whose acknowledgement was lost is retried, and so is one posted to a room that then crashed, restarted, or hibernated mid-message. A message can also arrive twice when a retry fires while the target is still loading.

So make the handler idempotent. In practice that means keying on something in the message rather than appending:

// irtio/rooms/hub.ts
// Good: a repeat lands on the same row.
state.notices.add(notice.match, { from: message.from, seen: 0 });

// Trouble: a repeat is a second notification for one match.
state.notices.add(`n${state.notices.size}`, { from: message.from, seen: 0 });

A message that keeps failing is retried a few times, logged, and dropped. There is no dead-letter queue to read it out of. A handler that throws counts toward the room’s crash threshold like any other handler, which is why the retries are bounded rather than endless.

Delivery scope

Both methods stay within your project and accept up to 4 KiB. Use messages to send an id or event; keep larger data in storage. Ordering across channels is not guaranteed.

publish reaches awake subscribers on the same server. Use directed send for a room on another server.

Sending across servers

send reaches any room in your project, whichever server it runs on, and the promise you get back resolves the same way either way. It succeeds, or it rejects with E_BUS_NO_SUCH_ROOM (a room that exists nowhere in the project, and nothing is created for it) or E_BUS_MAILBOX_FULL.

E_BUS_MAILBOX_FULL also covers the sender running out of room. A message waiting to reach another server counts toward the sending room’s own 64-message limit, the same limit its mailbox uses.

A message can be retried at two separate points, and they have separate limits.

Reaching the target’s server. A send to a room on another server retries after 1, 2, 4 seconds and on, doubling up to 60 seconds, for up to an hour. After that the message is dropped and logged at the sender. Retries do not run in your handler, so a sender that has gone to sleep is neither woken nor billed for them.

Delivery into the target room. Once the message is in the target’s mailbox, each attempt to run your onMessage counts, and the fifth failure drops it with an error in the target’s log. The count rises before the handler runs, so a message whose handler throws or kills the room still spends an attempt. That bound is what stops one poisonous message from restarting a room until the crash threshold closes it.

A project getting another server does not disturb the rooms already running, and nothing restarts. A room can move to another server when the one holding it stops answering. See servers and room placement.

Bus limits

Payload4 KiB
Channels one room may subscribe to32
Undelivered messages per room, counting both its mailbox and messages it is waiting to deliver to another server64
Delivery attempts into the target room before a message is dropped5
How long a message waits for another server before it is dropped1 hour
Bus operations per room50 a second, bursting to 100

Both verbs draw on the same budget. Going over it fails the call rather than slowing the room down.