@rudder/sdk

Rudder client SDK for web games: device and social auth, player state (profile, balances, items), catalog currencies, player storage, and calls to installed modules through generated typed clients.

Install

The package is published to the Gitea registry at hub.rudder.build, not npmjs. Point the @rudder scope at it in your project's .npmrc:

@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/

Then install as usual (anonymous read access, no token needed):

npm install @rudder/sdk

Quickstart

import { createClient } from '@rudder/sdk';

const client = createClient({
  projectKey: 'your-project-key', // the key selects the environment
  // baseUrl: 'https://api.rudder.build' (default)
  // tokenStore: localStorage with an in-memory fallback (default)
  // appVersion: '1.4.2', sent with platform 'web' on login and refresh
});

await client.auth.loginWithDevice();

const profile = client.player.value;
const gold = profile?.balances?.find((b) => b.currency === 'gold')?.balance;
const swords = profile?.items?.find((i) => i.item === 'sword')?.quantity ?? 0;

await client.storage.save('progress', JSON.stringify({ level: 7 }));

client.dispose();

Module clients

Game features are installed modules. Generate a typed client for the modules installed in an environment with the rudder CLI and commit the file:

export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...
rudder client generate --lang ts --out src/rudder.modules.ts   # --environment staging (default) | prod
import { createClient, RudderModuleError } from '@rudder/sdk';
import { modules, type LeaderboardsSubmitError } from './rudder.modules';

const client = createClient({ projectKey });
const m = modules(client);

await m.leaderboards.submit({ slug: 'weekly', score: 120 });
const top = await m.leaderboards.top({ slug: 'weekly', limit: 10 });

try {
  await m.leaderboards.submit({ slug: 'weekly', score: 120 });
} catch (error) {
  if (error instanceof RudderModuleError) {
    const code = error.code as LeaderboardsSubmitError;
    console.warn(code, error.message, error.requestId);
  }
}

Only trigger: client functions are generated. Each generated function calls client.modules.call<Result>(module, fn, args, { moduleVersion }), which you can also call directly:

  • args is always sent ({} when omitted); the promise resolves to result.
  • Every call gets one Idempotency-Key, reused on its retries, so a retried call runs at most once on the server.
  • Retries: up to 2, with jittered backoff, on network errors, 5xx and 409 conflict. Other 4xx responses are never retried.
  • moduleVersion is sent as X-Rudder-Module-Version: <module>@<version>. The backend logs a warning on a major mismatch and still runs the call.
  • A 422 rejects with RudderModuleError (code, message, requestId).

Surface

Area Access
Auth client.auth.loginWithDevice(), loginWithGoogle({ idToken }), loginWithApple({ idToken }), loginWithCustom({ customData }), linkWithGoogle(idToken), linkWithApple(idToken), linkWithCustom({ customData }), unlinkIdentity(provider), logout(), isAuthenticated, onAuthStateChange(cb)
Player client.player state: player, identities, balances, items
Catalog currencies client.catalog state (Map<slug, CatalogCurrency>)
Storage client.storage state + .save(type, data) / .delete(type)
Modules client.modules.call<T>(module, fn, args?, { moduleVersion? }), usually through a generated client

balances carries one entry per released currency and items one per inventory stack.

Error handling

All SDK errors extend RudderError and carry an optional machine-readable code:

import {
  RudderError,
  RudderNetworkError,
  RudderHttpError,
  RudderAuthError,
  RudderModuleError,
} from '@rudder/sdk';

try {
  await client.player.reload();
} catch (error) {
  if (error instanceof RudderAuthError) {
    // 401: session expired and the refresh failed; the client already
    // cleared tokens and emitted 'signed-out' via onAuthStateChange.
    showLogin();
  } else if (error instanceof RudderHttpError) {
    console.error(error.status, error.code, error.body);
  } else if (error instanceof RudderNetworkError) {
    // fetch failed or timed out; the underlying error is in error.cause
    console.error('offline?', error.cause);
  }
}

RudderModuleError extends RudderHttpError (status 422). Its type parameter narrows code to a generated union such as LeaderboardsSubmitError. A module trap is a 500 and a timeout a 504.

A missing projectKey throws RudderError with code: 'sdk/invalid-options' from createClient.

Track the session lifecycle without polling the token store:

client.auth.onAuthStateChange((state) => {
  // fires immediately with the current state, then on every change
  setSignedIn(state === 'signed-in');
});

Data liveness model

State objects (player, catalog, storage) are warmed at login (player and catalog) or on first use, then kept fresh by a revision poll: the client asks the backend for entity revisions every 30 s (±20% jitter) and reloads only the entities whose revision grew. Polling pauses while the tab is hidden and resumes on visibilitychange. Storage writes invalidate the affected domain immediately. Expect data to be eventually consistent within one poll interval; call reload() when you need a guarantee.

React recipe

SyncedState.onChange fires immediately with the current snapshot, which maps straight onto useSyncExternalStore:

import { useSyncExternalStore } from 'react';

function useSynced<T>(state: { onChange(cb: () => void): () => void; value: T | undefined }) {
  return useSyncExternalStore(
    (onStoreChange) => state.onChange(() => onStoreChange()),
    () => state.value,
  );
}

function Wallet() {
  const profile = useSynced(client.player);
  return <div>{profile?.balances?.map((b) => `${b.currency}: ${b.balance}`).join(', ')}</div>;
}

Development

npm run typecheck   # tsc over src and tests
npm test            # unit + local e2e (jsdom)
npm run build       # tsup → dist (ESM + CJS + d.ts)
npm run test:e2e-prod   # drives a seeded project on the live backend

Generated wire types (src/generated) come from liveops-backend/apigen; run npm run generate to refresh them. Do not edit them by hand.

test/fixtures/rudder.modules.ts is CLI output for test/fixtures/leaderboards.manifest.json; npm run typecheck checks that it accepts RudderClient. Refresh it after generator changes:

rudder client generate --lang ts --manifests <dir with leaderboards.manifest.json> --out test/fixtures/rudder.modules.ts
S
Description
No description provided
Readme 746 KiB
Languages
TypeScript 100%