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.

Provisional: this tracks the frontend API as it settles.
FunctionReturnsYields
Gamehoster.Instance.Join.Any(playerInfo)operation handleInstance
Gamehoster.Instance.Join.Instance(playerInfo, instance)operation handleInstance
Gamehoster.Instance.Join.Infoschemathe fields a player must supply
Gamehoster.Create(instanceInfo?)operation handleInstance
Gamehoster.Instance.State(instance)subscription handleinstance state
Gamehoster.Instance.Frame(instance)subscription handleframe 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
Two ways to join: each returns an operation handle

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" }
The join schema, and a value that satisfies it

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)
Create a password-protected room and join it
instanceInfoTypeDescription
passwordstringSet the password joiners must supply; the room then lists as key. Omit for an open room.
capacitynumberMax 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"
}
an Instance, as returned by a join or create

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()
Subscribing to the instance's state

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:

What you receive is the visible view for your player, not the room's true state. Two gates apply. First relevance: you only see the objects currently relevant to you; they appear and vanish from 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
}
an instance state
FieldTypeDescription
idstringThe instance id.
phasestringThe instance's phase field. For pong, waiting, playing or over.
playersarrayThe 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()
Subscribing to interpolated render frames

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 } ] }
}
a frame view

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
One call for inputs and lobby verbs alike

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()
Leaving a room