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.
publish | send | |
|---|---|---|
| Who gets it | every awake, subscribed room | one room you name |
| If the target is asleep | it misses the message | it wakes up and gets it |
| Delivery | best effort | at least once |
| Answers you | no | a 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:
| Error | What happened |
|---|---|
E_BUS_NO_SUCH_ROOM | no room by that id in this project, or it is a relay room and runs no code |
E_BUS_PAYLOAD_TOO_LARGE | over 4 KiB |
E_BUS_MAILBOX_FULL | the target has 64 undelivered messages and is not keeping up |
E_BUS_RATE_LIMITED | this 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
| Payload | 4 KiB |
| Channels one room may subscribe to | 32 |
| Undelivered messages per room, counting both its mailbox and messages it is waiting to deliver to another server | 64 |
| Delivery attempts into the target room before a message is dropped | 5 |
| How long a message waits for another server before it is dropped | 1 hour |
| Bus operations per room | 50 a second, bursting to 100 |
Both verbs draw on the same budget. Going over it fails the call rather than slowing the room down.