4.9 KiB
@rudder/js-sdk
LiveOps Web SDK for browser games — device auth, observable player state, catalog currencies, storage, generic resources, and module function calls.
Install
The package is published to a private registry, 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/js-sdk
Quickstart
import { RudderClient } from '@rudder/js-sdk';
const client = new RudderClient({
baseUrl: 'https://api.rudder.build',
projectKey: 'your-project-key',
// tokenStore is optional: defaults to localStorage with an in-memory
// fallback where localStorage is unavailable (SSR, private mode).
appVersion: '1.4.2', // optional; sent with platform 'web' on login and refresh
});
await client.auth.loginWithDevice(); // region 'global', language 'en'
// or: await client.auth.loginWithDevice({ region: 'eu', language: 'de', nickname: 'Bob' });
const profile = client.player.data;
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
const items = await client.resources('items').list();
const result = await client.modules.call('quests', 'list');
client.dispose();
Surface
| Area | Access |
|---|---|
| Auth | client.auth.loginWithDevice(), client.auth.loginWithCustom({ customData }), client.auth.logout(), client.auth.isAuthenticated, client.auth.onAuthStateChange(cb) |
| Player / wallets | client.player (observable) |
| Catalog currencies | client.catalog (observable) |
| Storage | client.storage (observable) + .save(items) / .delete(type) |
| Project storage | client.projectStorage (observable) + .save(items) |
| Modules | client.modules.call(module, fn, args?) |
| Resources | client.resources(slug) → list / get / create / update / delete |
Game-design features (quests, stores, inventory, leaderboards, battle pass,
remote config) live in installed modules. Call them through
client.modules.call and read their rows through client.resources(slug).
Error handling
All SDK errors extend RudderError and carry an optional machine-readable
code:
import {
RudderError,
RudderNetworkError,
RudderHttpError,
RudderAuthError,
} from '@rudder/js-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 / timeout — the underlying error is in error.cause
console.error('offline?', error.cause);
}
}
A module business error rejects with RudderHttpError (status 422) whose
code is the module's own open string code. The platform codes are
forbidden and row_referenced.
Invalid client options (missing baseUrl / projectKey) throw RudderError
with code: 'sdk/invalid-options' from the constructor.
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
Observable domains (player, catalog, storage, projectStorage) are
warmed at login (player and catalog) or on first use, then kept fresh by
a revision poll: the client asks the gateway 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; data: T | undefined }) {
return useSyncExternalStore(
(onStoreChange) => state.onChange(() => onStoreChange()),
() => state.data,
);
}
function Wallet() {
const profile = useSynced(client.player);
return <div>{profile?.wallets?.map((w) => `${w.currency}: ${w.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 gateway
Generated wire types (src/generated) come from the gateway's apigen; run
npm run generate to refresh them. Do not edit them by hand.