FOUR SDK / V1.0.0

Requests, retries and observability

Every method accepts a final RequestOptions argument:

ts
import { ConflictError, isFourBTDError } from '@fourbtd/sdk';

const controller = new AbortController();

try {
  await fourBtd.characters.update(
    mira.id,
    {
      description: 'Explorer',
    },
    {
      expectedVersion: mira.version,
      idempotencyKey: 'mira-profile-edit-1',
      signal: controller.signal,
      requestId: 'profile-request-1',
    },
  );
} catch (error) {
  if (error instanceof ConflictError) {
    const latest = await fourBtd.characters.get(mira.id);

    // Reconcile your edit against the latest version
    // before submitting a new operation.
  } else if (isFourBTDError(error)) {
    console.error(error.code);
  } else {
    throw error;
  }
}

// Cancels an active request or its retry wait.
controller.abort();

Retries and timeouts

  • Timeout is per attempt and defaults to 30 seconds.

  • Retries default to 2, for a maximum of 3 total attempts.

  • Retry delay is calculated as:

text
min(maxBackoffMs, backoffMs * 2^attempt)
  * (1 + jitter * (2 * random - 1))

The actual wait is always at least the server-provided Retry-After, expressed as seconds or an HTTP date.

Only reads or mutations with an idempotency key are retried. Retries apply to network errors, 408, 429, and 5xx responses.

Every SDK mutation receives one stable generated idempotency key across retries unless one is supplied explicitly. Persist your own key across process restarts or manually repeated calls when you need the same operation to remain idempotent.

Idempotency

Local idempotency is scoped by network, operation, and key. Reusing the same key with changed payload, context, or version produces a conflict.

Event-level idempotency keys are additionally scoped by character.

Batches support up to 100 inputs and return ordered successes or typed error data. A batch-level key replays the entire result, while event-level keys deduplicate individual events.

Do not reuse a context idempotency key for different mutations of the same operation.

Optimistic concurrency

expectedVersion checks the entity being mutated:

  • Character

  • Memory

  • Relationship

  • Inventory source stack

  • Identity

  • Session

For new relationship or inventory entries, use 0.

Creation of new immutable entities has no previous version to compare against.

sync checks the character version. Session-end checkpointing checks the session version, not a coincidentally matching identity version.

Data handling

Client calls detach inputs and outputs. Unknown external responses fail Zod validation.

Public dates use UTC ISO strings. Metadata must contain valid JSON values, and undefined optional properties are omitted.

Cancellation and timeout abort underlying work. Custom transports and signers must honor abort signals.

Cancellation cannot undo a transaction that has already been submitted to an external system.

Context

ts
const scoped = fourBtd.withContext({
  worldId: 'forest',
  sessionId: session.id,
  playerId: 'player-one',
  trace: {
    feature: 'quest',
  },
  idempotencyKey: 'quest-operation-1',
});

withContext() returns a client that shares the same transport and dependencies.

Explicit method fields override context defaults. Setting a context override to undefined clears that default.

Context defaults apply as follows:

  • worldId → characters, events, and sessions

  • playerId → sessions

  • sessionId → captured events, including batches

  • Context metadata → every transport request

Logging and hooks

Inject a logger with:

text
debug
info
warn
error

Lifecycle hooks include:

text
onRequestStart
onRequestSuccess
onRequestRetry
onRequestFailure

Hooks receive the operation, request ID, zero-based attempt, and optional error code or retry delay.

Logger output contains the operation, attempt, lifecycle phase, and redacted trace metadata. Request payloads are never logged.

Logging defaults to a no-op.

Use sensitiveFields to configure additional trace keys for redaction. Known API key values are also scrubbed.

For custom instrumentation:

ts
redact(value, sensitiveFields);

redact() sanitizes nested credentials and configured custom keys.

Instrumentation exceptions never alter requests.

Error details are automatically redacted. Arbitrary external causes and HTTP error bodies are discarded, while typed SDK causes may be retained through the standard cause property.