Gamehoster Instance Functions
An instance is one live room. This namespace covers its whole life: join or create it,
watch its state, render its frames,
send inputs and commands, and leave. Every in-room call takes
the instance (or its id) as its first argument.
| Function | Returns | Yields |
|---|---|---|
Gamehoster.Instance.Join.Any(playerInfo) | operation handle | Instance |
Gamehoster.Instance.Join.Instance(playerInfo, instance) | operation handle | Instance |
Gamehoster.Instance.Join.Info | schema | the fields a player must supply |
Gamehoster.Create(instanceInfo?) | operation handle | Instance |
Gamehoster.Instance.State(instance) | subscription handle | instance state |
Gamehoster.Instance.Frame(instance) | subscription handle | frame view |
Gamehoster.Instance.Send(instance, command) | — | fire-and-forget |
Gamehoster.Instance.Leave(instance) | operation handle | — |
Handles follow the conventions: subscriptions carry
onChange/onError/cancel(), operations carry
onSuccess/onError/cancel(), and Send is the
fire-and-forget exception.
Joining
There are two ways in, plus creating a room. Each is an operation handle whose
onSuccess yields the instance you joined; each way in takes the
joining player's info first.
const info = { name: "Ada" }
const any = Gamehoster.Instance.Join.Any(info) // drop into any open room
const inRoom = Gamehoster.Instance.Join.Instance(info, "i-7f3a2c") // a specific live room
inRoom.onSuccess = (instance) => enter(instance)
inRoom.onError = (error) => showJoinError(error) // full, closed, invalid info…
inRoom.cancel() // give up if it hangs
Join.Any drops the player into any open instance, creating one if none has room.
Join.Instance joins a specific room chosen from Browse;
for a key room pass its password as a third argument:
Join.Instance(info, instance, { password }).
playerInfo & the join schema
A game can ask a joining player for details: a name, a colour, a class pick. It declares them as a
schema (backend file gamehoster-player-info.json), and
the front end reads that schema from Gamehoster.Instance.Join.Info to build its form.
The collected values are the playerInfo object you pass to every join call; the client
validates it against the schema and rejects a bad one through onError. A game that asks
for nothing has an empty schema, and playerInfo is {}.
// Gamehoster.Instance.Join.Info — the schema the game declares (pong: name only)
{
"name": { "type": "string", "required": true, "max": 16 }
}
// a matching playerInfo you pass to a join call
{ "name": "Ada" }
Creating a room
The top-level Gamehoster.Create opens a new instance and joins it. You pass
instanceInfo: the room settings the game lets you choose, validated the same way. Every
field is optional; a bare Create() opens a default public room:
const room = Gamehoster.Create({ password: "hunter2", capacity: 8 })
room.onSuccess = (instance) => enter(instance) // instance.key holds the invite key
room.onError = (error) => showError(error)
| instanceInfo | Type | Description |
|---|---|---|
password | string | Set the password joiners must supply; the room then lists as key. Omit for an open room. |
capacity | number | Max players, within the limits the game allows. |
A private room isn't listed in Browse at all; its result carries an invite key to share
instead. Which of these the game offers is set by gamehoster-game-create.
The Instance object
What onSuccess yields: the same shape Browse lists, plus you, your own player id in the room:
{
"id": "i-7f3a2c",
"phase": "waiting",
"players": 4,
"capacity": 8,
"access": "open",
"you": "p-92"
}
The instance state
An instance starts in its waiting phase (a lobby
is just that phase) and advances through playing to over;
Instance.State is one subscription that tracks the room's authoritative
logical state at the send rate: the
discrete snaps the server pushes, not a per-frame view. Your handlers stay bound to the instance the whole
time (the value simply changes) so you rebind nothing when the match starts:
const state = Gamehoster.Instance.State(instance)
state.onChange = (s) => {
if (s.phase === "waiting") showLobby(s) // roster, ready-up
else showScoreboard(s)
}
state.onError = (error) => showError(error)
state.cancel()
The instance state object
onChange fires on every logical change: a player joins, the phase advances, the score
moves. It is the discrete authoritative state as it snaps at the send
rate (for a smooth per-frame view use Frame instead). Its core fields, plus
whatever the instance's gamehoster-schema.json declares:
players / entities as the server sends enter and leave,
so a state may hold only part of the room. Then, within a relevant object, each field's
sync and audience: you get its sent and shared
fields (and your own owner-audience fields), never a server field or another
player's owner-audience fields.{
"id": "i-7f3a2c",
"phase": "playing",
"players": [
{ "id": "p-92", "info": { "name": "Ada" }, "score": 3, "y": 118.3 }
],
"score": 5 // ← the instance's own schema fields
}
| Field | Type | Description |
|---|---|---|
id | string | The instance id. |
phase | string | The instance's phase field. For pong, waiting, playing or over. |
players | array | The players currently relevant to you: each with an id, their fixed info, plus their visible per-instance state fields (for pong, score and paddle y). |
| … | — | Plus every field the instance's schema declares (score, and so on); which of these are present depends on each field's sync and audience. |
Rendering frames
Instance.Frame is the render path. Where State snaps at the
send rate, Frame runs on the client's own free frame
loop (requestAnimationFrame): the library ingests the backend's authoritative snaps
and, between them, runs the game's own *-update-frontend.js shared bodies at the tick rate (the
same code the backend runs) so the state keeps simulating and stays live. It then
interpolates and calls your handler once per animation frame (as fast as the display refreshes) with a
ready-to-draw view. You never call those bodies, touch raw state, or write netcode:
const frame = Gamehoster.Instance.Frame(instance)
frame.onChange = (view) => draw(view) // called ~each animation frame, already interpolated
frame.cancel()
The frame view object
The interpolated positions of everything on screen (the players and the entities) at this frame.
Like the instance state, a frame is the visible
view for your player: it holds only the objects currently relevant
to you (those that have entered and not yet leaved), and omits
server fields and other players' owner-audience fields:
{
"instance": { "phase": "playing", "score": 5 },
"players": [ { "id": "p-92", "x": 40.0, "y": 118.3 } ],
"entities": { "ball": [ { "id": "e-1", "x": 400.2, "y": 231.7 } ] }
}
Sending inputs & commands
Instance.Send is how a player acts: both high-frequency input and one-off commands go
through it. It is fire-and-forget: it returns nothing, and you observe the result through
State or Frame, never a per-call handle. That keeps the
input path allocation-free and matches the authoritative model (you propose, the server disposes):
Gamehoster.Instance.Send(instance, { type: "move", y }) // input
Gamehoster.Instance.Send(instance, { type: "ready" }) // lobby command
Gamehoster.Instance.Send(instance, { type: "start" }) // begin the match
Every command is subject to the instance's legality predicate: the server rejects one that isn't allowed right now (not your turn, wrong phase), and the same predicate on the client tells your UI whether to offer the control. A rejected command simply never shows up in the instance state; that's where you see it.
Leaving
Instance.Leave disconnects from the room and ends its subscriptions. It's an operation
handle, so you can confirm you're out:
const left = Gamehoster.Instance.Leave(instance)
left.onSuccess = () => returnToMenu()