HTTP Client
This page maps the namespaced SDK resource methods to HTTP routes. Most apps should import createSimulatorClient() and use the resource API directly.
import { createSimulatorClient } from '@my-swu/simulator-client'
const simulatorClient = createSimulatorClient({
// Base URL is the HTTP origin; resource methods append their route paths.
baseUrl: 'http://127.0.0.1:4000',
})Routes
| SDK method | HTTP route | Response |
|---|---|---|
simulatorClient.system.health() | GET /health | HealthResponse |
simulatorClient.system.availability() | GET /api/availability | SimulatorAvailabilityResponse |
simulatorClient.cards.get(ids) | GET /api/cards?ids=... | CardMetadata[] |
simulatorClient.cards.resolveIds(references) | POST /api/cards/resolve | CardIdResolution[] |
simulatorClient.cards.catalog() | GET /api/cards/catalog | CardMetadata[] |
simulatorClient.matches.create(request) | POST /api/matches | MatchAccessResponse |
simulatorClient.matches.join(match, request) | POST /api/matches/{matchId}/join | MatchAccessResponse |
simulatorClient.matches.get(match) | GET /api/matches/{matchId} | GameState |
simulatorClient.matches.applyFormatEvent(access, command) | POST /api/matches/{matchId}/format-events | GameState |
simulatorClient.limitedEvents.create(request) | POST /api/limited-events | LimitedEventAccessResponse |
simulatorClient.limitedEvents.join(event, request?) | POST /api/limited-events/{eventId}/join | LimitedEventAccessResponse |
simulatorClient.limitedEvents.get(event) | GET /api/limited-events/{eventId} | LimitedEventSnapshot |
simulatorClient.limitedEvents.applyCommand(access, command) | POST /api/limited-events/{eventId}/commands | LimitedEventSnapshot |
simulatorClient.limitedEvents.leave(access) | POST /api/limited-events/{eventId}/leave | No content |
Health checks
Two unauthenticated routes answer "can I use this server right now", and they answer different halves of it.
const serverHealth = await simulatorClient.system.health()
// `'ok'` whenever the process is up and serving.
console.log(serverHealth.status)
// Exact Git revision, or `development` for local builds.
console.log(serverHealth.buildSha)
const simulatorAvailability = await simulatorClient.system.availability()
console.log(simulatorAvailability.acceptingNewMatches)health() is the liveness probe. It touches no state and returns status: 'ok' plus the compiled buildSha from any process that can serve at all. It answers whether the server is reachable and identifies its artifact — never whether it will accept work.
availability() is the one to check before offering a "new match" button. acceptingNewMatches goes false while the simulator is draining, and maintenance carries that state with an optional operator message worth surfacing to players. checkedAtMs stamps when the answer was computed, so a cached copy can be aged out.
Starting a match anyway during a drain is not silently ignored — it fails with simulator_maintenance; see Error Handling.
Request validation
Request bodies are validated before being sent only when the client was given a validator. Validation is off by default; see Runtime validation for why and how to turn it on with validate: createSchemaValidator().
// With `validate` set, this request body is checked before network I/O.
const premierHostDeck = {
leader: 46102, // Leia Organa - Someone Who Loves You
leaders: [], // Twin Suns only; Premier uses the single `leader` above.
base: 308, // Echo Base
cards: Array.from({ length: 50 }, () => 45), // Alliance X-Wing
sideboard: [],
}
await simulatorClient.matches.create({
format: 'premier',
playerCount: 2,
hostDeck: premierHostDeck,
hostName: 'Host',
hostAuthenticated: false,
historyIdentities: [],
aiOpponents: [],
})With a validator installed, a malformed body throws SdkValidationError before network I/O. Without one, the body is sent as-is and the server answers — usually a 400 surfaced as EngineHttpError.
Response validation
Successful responses are validated against the generated JSON schemas only when the client has a validator. With one installed, schema drift or malformed response JSON throws SdkValidationError; without one, the payload is returned typed as if it matched, and any mismatch surfaces later as a missing field. A non-2xx response still throws EngineHttpError either way. See Runtime validation.
Cancellation and timeouts
Every HTTP resource method takes an optional trailing options argument holding a caller-owned signal and a per-call timeoutMs. Both are optional: a call that passes neither behaves exactly as it always has.
// One controller cancels every request a screen started, e.g. on unmount.
const unmountController = new AbortController()
const publicMatchSnapshot = await simulatorClient.matches.get('match_123', {
signal: unmountController.signal,
timeoutMs: 5_000,
})
console.log(publicMatchSnapshot.phase)Set a default for every call with timeoutMs on createSimulatorClient():
import { createSimulatorClient } from '@my-swu/simulator-client'
const boundedClient = createSimulatorClient({
baseUrl: 'http://127.0.0.1:4000',
// Applies to every HTTP resource call that does not pass its own timeout.
timeoutMs: 10_000,
})
// A per-call value replaces the default; `0` opts one slow call out of it.
const catalogCards = await boundedClient.cards.catalog({ timeoutMs: 60_000 })
console.log(catalogCards.length)How the two compose:
- A per-call
timeoutMsalways wins over the client default. Passing0(or any non-positive value) opts that one call out of a configured default. - A client without
timeoutMsnever arms a timer. - When a
signaland a timeout are both in play, whichever fires first wins. - The timeout covers the response body read too, not just the connection.
- Cancellation reaches the wire through
init.signalon the configuredfetch. An adapter that ignores it (the in-process local engine, for example) cannot stop the work, but the SDK still rejects the call once the deadline passed.
Cancelled calls reject with EngineRequestAbortedError, whose kind is 'aborted' for a caller-initiated abort and 'timeout' for an elapsed timeout:
import { EngineRequestAbortedError } from '@my-swu/simulator-client'
try {
await simulatorClient.system.health({ timeoutMs: 2_000 })
}
catch (error) {
if (error instanceof EngineRequestAbortedError) {
// `kind` separates "the user navigated away" from "the server is slow".
console.warn(error.kind, error.timeoutMs, error.method, error.url)
}
}Socket connections are not covered by these options; a match session bounds its own handshake with handshakeTimeoutMs.
HTTP errors
Non-2xx responses throw EngineHttpError:
import { EngineHttpError } from '@my-swu/simulator-client'
try {
// Missing resources reject with EngineHttpError instead of returning null.
await simulatorClient.matches.get('missing-match')
}
catch (error) {
if (error instanceof EngineHttpError) {
// Log stable fields that help route UI handling and diagnostics.
console.warn(error.status, error.code, error.method, error.url)
}
}When the server returns a valid engine error payload, code and message come from that payload. If the body is empty, not JSON, or not shaped like api-error-response, the error still includes status, method, url, and the raw responseText when present.
