Using irtio from Node
Use @irtio/client in Node for bots and tests against a running room. For room logic tests with a
controllable clock, use testRoom.
What you need
| Requirement | Why |
|---|---|
| Node 22 or later | The client uses the global WebSocket. Without one it throws irtio: no global WebSocket and names the transport option |
tsx, or built JavaScript | Your schema and room files are TypeScript. Run them with npx tsx script.ts, or compile first |
| A running endpoint | irtio dev on ws://localhost:7070, or your deployed room |
The published packages ship prebuilt: every @irtio/* package publishes dist/ with .js and .d.ts beside it, so Node resolves them with no build step of your own. Only your own
TypeScript needs tsx.
A headless client
// bot.ts
import { joinRoom } from '@irtio/client';
import { schema } from './irtio/schema.js';
const room = await joinRoom(schema, {
url: 'ws://localhost:7070',
role: 'player',
name: 'bot-1',
});
room.on('status', (status) => console.log('status', status));
console.log('joined', room.id, 'as', room.me);
room.leave(); npx tsx bot.ts joinRelay is the same shape for a room with no schema. Nothing on either path touches window, document, or requestAnimationFrame. The interpolated room.render view still works, but it
advances on the frames it receives rather than on a paint loop.
This script creates a new room each time it runs.
Getting several clients into the same room
In Node, joinRoom creates a room when you omit room. To connect several clients, join once and
pass the returned room.id to the other clients:
// bots.ts
import { joinRoom } from '@irtio/client';
import { schema } from './irtio/schema.js';
const url = 'ws://localhost:7070';
// The first bot creates the room, because it passes no `room`.
const host = await joinRoom(schema, { url, role: 'player', name: 'bot-1' });
console.log('room', host.id, host.link);
// Every other bot joins that code.
const others = await Promise.all(
[2, 3].map((n) => joinRoom(schema, { url, role: 'player', name: `bot-${n}`, room: host.id })),
);
const bots = [host, ...others];
console.log(bots.map((b) => b.me)); // three distinct client ids, one room
for (const bot of bots) bot.leave(); room.id is the room’s code, set from the server’s welcome message, so read it after the join
promise resolves rather than before. room.room does not exist: the option going in is room, the
field coming back is id.
The room option also accepts a full share link, not just a bare code, so room: host.link works
and so does pasting a link out of a browser to point a bot at a room you are already sitting in.
To join a room that already exists, name the code directly and skip the host step:
const room = await joinRoom(schema, { url, role: 'player', room: 'ABCD' }); matchRoom in Node needs both URLs
Pass controlUrl for the matchmaking API and url for the room connection. Use the wss:// endpoint printed by irtio deploy; without it or IRT_URL, Node defaults to ws://localhost:7070, which can fail with E_CONNECT_FAILED after matchmaking succeeds.
// bot.ts
import { matchRoom } from '@irtio/client';
import { schema } from './irtio/schema.js';
const room = await matchRoom(schema, {
queue: '1v1',
controlUrl: 'https://irt.io',
url: 'wss://eu.irt.io',
role: 'player',
name: 'bot-1',
}); Setting IRT_URL in the environment does the same thing for every join in the process, since the
client reads it before falling back to localhost.
For many clients at once, spawnBots and defineScenario from @irtio/bots already do this threading for you, along with the process
management, the trace recording, and the invariants. Reach for them before hand-rolling a fleet.
Two things that differ from a browser
Identity has nowhere to live. identity: true keeps its credential in localStorage, which a
Node process does not have. The client falls back to keeping it for the life of the process, so
every run is a new player. Pass your own storage (any object with getItem, setItem and removeItem) to make a bot’s identity durable across runs. Identity also needs a control plane and
a deployed project. See Player identity.
Close each room with room.leave() when your script finishes so its socket does not keep the
process open.
Installing from a monorepo or a local path
A file: or workspace: install of @irtio/* resolves the same way as a registry install, since
the packages are plain ESM with exports maps. Keep the versions of @irtio/client and @irtio/schema in step: the schema object your script imports has to be the one the client was
built against, and two copies in one node_modules tree read as two different schemas.
Room code is the exception, and its rule is stricter. The bundler that builds your room accepts a fixed list of bare imports, and a workspace specifier is not on it. See what room code may import for the relative-path approach. Your Node script is not room code and has no such limit.
Related
- Simulated players for tests with no sockets.
- Built-in invariants for
spawnBotsand bot scripts. - Testing in CI for what to run on a push.