Skip to content

HTTP Client ​

This page maps the namespaced SDK resource methods to HTTP routes. Most apps should import createSimulatorClient() and use the resource API directly.

ts
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 methodHTTP routeResponse
simulatorClient.system.health()GET /healthHealthResponse
simulatorClient.system.availability()GET /api/availabilitySimulatorAvailabilityResponse
simulatorClient.cards.get(ids)GET /api/cards?ids=...CardMetadata[]
simulatorClient.cards.resolveIds(references)POST /api/cards/resolveCardIdResolution[]
simulatorClient.cards.catalog()GET /api/cards/catalogCardMetadata[]
simulatorClient.matches.create(request)POST /api/matchesMatchAccessResponse
simulatorClient.matches.join(match, request)POST /api/matches/{matchId}/joinMatchAccessResponse
simulatorClient.matches.get(match)GET /api/matches/{matchId}GameState
simulatorClient.matches.applyFormatEvent(access, command)POST /api/matches/{matchId}/format-eventsGameState
simulatorClient.limitedEvents.create(request)POST /api/limited-eventsLimitedEventAccessResponse
simulatorClient.limitedEvents.join(event, request?)POST /api/limited-events/{eventId}/joinLimitedEventAccessResponse
simulatorClient.limitedEvents.get(event)GET /api/limited-events/{eventId}LimitedEventSnapshot
simulatorClient.limitedEvents.applyCommand(access, command)POST /api/limited-events/{eventId}/commandsLimitedEventSnapshot
simulatorClient.limitedEvents.leave(access)POST /api/limited-events/{eventId}/leaveNo content

Health checks ​

Two unauthenticated routes answer "can I use this server right now", and they answer different halves of it.

ts
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().

ts
// 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.

ts
// 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():

ts
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 timeoutMs always wins over the client default. Passing 0 (or any non-positive value) opts that one call out of a configured default.
  • A client without timeoutMs never arms a timer.
  • When a signal and 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.signal on the configured fetch. 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:

ts
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:

ts
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.

Released under the MIT License.