# Calls

> Ask the room to do something and get its answer, with names, args and replies typed from one declaration.

A **call** is a request from one client that the room answers: buy an
item, claim a seat, look something up. The client awaits the reply, or
the error the room threw. Use calls for actions that need an answer;
for changes everyone sees, the answer can be the state itself, and for
fire-and-forget events, send a message.

## Declaring calls

Calls are declared once, in the schema module the room and its clients
both import. `defineCalls` takes the calls' signatures as a type, each
with at most one argument and its reply:

```ts
// schema.ts
import { defineCalls, defineSchema, entity } from "@irt/room";

export const prices = { sword: 50, shield: 30 };
export type Item = keyof typeof prices;

export const Wallet = entity("wallet", { coins: "u32" });

export const schema = defineSchema({
  private: Wallet,
  calls: defineCalls<{
    buy(args: { item: Item }): { coins: number };
    balance(): number;
  }>({
    buy(raw) {
      const item = (raw as { item?: unknown } | null)?.item;
      if (typeof item !== "string" || !Object.hasOwn(prices, item)) throw new Error("unknown item");
      return { item: item as Item };
    },
  }),
});
```

The object passed to `defineCalls` is optional and holds a `parse`
function for any call that needs one. The types are only types: args
come from the client, and a modified client can send anything. A
`parse` function runs on the room before the handler, gets the decoded
JSON, or the bytes, and returns the args or throws. Without one, the
handler gets the decoded JSON unchecked, so a call without one must
not trust its args; `balance` above takes none.

## In the room

The room's `calls` has a handler for each declared call. TypeScript
checks that none is missing, that `args` has the declared type, and that
the handler returns, or resolves to, the declared reply.

```ts
// room.ts
import { CallRejected, defineRoom } from "@irt/room";
import { prices, schema } from "./schema.js";

export default defineRoom({
  schema,
  onJoin(room, conn) {
    const wallet = room.private(conn);
    if (wallet.coins === 0) wallet.coins = 100;
  },
  calls: {
    buy(room, conn, { item }) {
      const wallet = room.private(conn);
      if (wallet.coins < prices[item]) throw new CallRejected("not enough coins");
      wallet.coins -= prices[item];
      return { coins: wallet.coins };
    },
    balance: (room, conn) => room.private(conn).coins,
  },
});
```

- What a handler throws becomes the client's error, with its message,
  and the room keeps running.
- Throw `CallRejected` for a rejection that's part of the game, such as
  "not enough coins" or "the stairs are sealed": the client gets its
  message and the room logs nothing. Any other error is logged at error
  level with the room, player, tick and stack, because it's taken for a
  bug. A `parse` that throws rejects the call without logging too.
- A handler may be async; the reply goes out when its promise settles.
- Handlers run as calls arrive, between ticks. A call that changed room
  state (shared or private state, entities, ownership, bans) replies
  after the next tick has saved the change and sent its state update,
  up to a tick later: a reply means the change is saved outside the VM
  and a room host crash can't lose it, and the client already has it.
  A call that changed nothing replies at once, unless another change
  is waiting for the same tick. Errors go out at once. What isn't room
  state, such as `room.data` or a stepper's own state, isn't covered.
- A call that changes room state just as the room hibernates fails
  rather than replies, as any call in flight when the connection drops
  does.

## On the client

```ts
// client.ts
import { connect, IrtError } from "@irt/client";
import { schema } from "./schema.js";

const room = await connect({ router: "wss://eu.irt.io", project: "my-game", room: "shop-1", schema });

try {
  const { coins } = await room.call("buy", { item: "sword" });
  console.log("coins left", coins);
} catch (e) {
  if (e instanceof IrtError) console.log("couldn't buy:", e.message);
  else throw e;
}

const balance: number = await room.call("balance");
console.log(balance);
```

`call` takes only declared names, with their args, and resolves to the
declared reply. It rejects with an `IrtError` when the room throws,
after a timeout of 10 seconds by default
(`room.call("balance", undefined, { timeout: 2000 })`), and when the
connection drops or the room restarts before the reply. In those last
cases the call may or may not have run, and the SDK doesn't retry it.
The error's `code` is 422 when the room answered with an error, such as
a `CallRejected`, and 0 when no answer came; see
[Errors](/docs/reference/errors#call).

## Common mistakes

- **Rejecting with a plain `Error`.** It works, but every rejection is
  logged as an error with a stack, which buries real bugs in normal
  traffic. Use `CallRejected` for expected outcomes.
- **Trusting args without `parse`.** The declared type is a promise the
  client makes, not one the room can rely on. Check anything that
  indexes, loops or spends.
- **Retrying a call that changes state.** After a dropped connection the
  room may already have run it. Check with another call, such as
  `balance`, or make the call safe to repeat.
- **Waiting on a call for state everyone sees.** Change the entity in
  the handler and let every client get it from the state update; reply
  with what only the caller needs.
