Skip to content

Operations

Keep a room online

Reopen your room whenever it closes, with backoff that respects the service's limits.

A room closes when its host loses the service connection, its key is revoked or expires, or you close it. This host runs forever: whenever its room ends, it opens a new one, backing off when the service asks it to.

host.mjs
import { createRoom, RoomAdmissionError } from 'ball2d/node';

const config = {
  apiKey: process.env.BALL2D_API_KEY,
  roomName: 'Always-on football',
  noPlayer: true,
  public: true,
};

const stopping = new AbortController();
for (const signal of ['SIGINT', 'SIGTERM'])
  process.once(signal, () => stopping.abort());

const sleep = (seconds) =>
  new Promise((resolve) => {
    const timer = setTimeout(resolve, seconds * 1000);
    stopping.signal.addEventListener(
      'abort',
      () => (clearTimeout(timer), resolve()),
      { once: true },
    );
  });

function setUp(room) {
  room.onError = (message) =>
    console.error(`[${room.roomId}]`, message);
  room.onPlayerJoin = (player) =>
    room.setPlayerTeam(player.id, 1);
  return room.setDefaultStadium('Classic');
}

let failures = 0;
while (!stopping.signal.aborted) {
  let room;
  try {
    room = await createRoom(config, {
      signal: stopping.signal,
    });
  } catch (error) {
    if (stopping.signal.aborted) break;
    // Bad configuration.
    if (error instanceof TypeError) throw error;
    if (
      error instanceof RoomAdmissionError &&
      [400, 401, 403].includes(error.status)
    )
      throw error;
    failures++;
    const wait =
      error.retryAfterSeconds ??
      Math.min(300, 2 ** failures) + Math.random() * 3;
    console.warn(
      `Room did not open: ${error.message}.` +
        ` Retrying in ${Math.round(wait)} s.`,
    );
    await sleep(wait);
    continue;
  }

  failures = 0;
  console.log('Room open:', room.roomLink);
  await setUp(room).catch((error) =>
    console.error('Setup failed:', error),
  );

  // Wait until the room ends or we are asked to stop.
  await new Promise((resolve) => {
    room.signal.addEventListener('abort', resolve, {
      once: true,
    });
    stopping.signal.addEventListener('abort', resolve, {
      once: true,
    });
  });
  room.close();
  await room.closed;
  if (!stopping.signal.aborted) await sleep(5);
}

How it works

  • Stop on permanent errors. Invalid settings and invalid, expired or revoked keys won't fix themselves, so the host exits and your process manager or alerting takes over.
  • Back off on the rest. Limits (429) wait the retryAfterSeconds the service sends. Other failures wait exponentially longer, up to five minutes, with jitter so many hosts don't retry together.
  • Wait on the room's own signal. room.signal aborts however the room ends, so one await covers a lost connection, a revocation and an expiry.
  • Pause before reopening. A short delay avoids spending your per-minute request budget in a loop if rooms keep closing.
  • Shut down cleanly. SIGTERM closes the room, waits for room.closed and ends the loop.

Players in a closed room are not moved to the new one; the new room has a new link. Publish a stable link on your own site that points at the current room.roomLink, or list the room publicly so players find it in the room list.