# @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): ```bash npm install @rudder/js-sdk ``` ## Quickstart ```ts 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('my_module', 'hello', { name: 'Bob' }); client.dispose(); ``` ## 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 / 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 features are installed modules, not SDK services. The first-party modules are items, inventory, reward, remote_config, leaderboards, quests, store and battlepass. Call a module through `client.modules.call` and read its rows through `client.resources(slug)`. ## Error handling All SDK errors extend `RudderError` and carry an optional machine-readable `code`: ```ts 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: ```ts 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 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`: ```tsx import { useSyncExternalStore } from 'react'; function useSynced(state: { onChange(cb: () => void): () => void; data: T | undefined }) { return useSyncExternalStore( (onStoreChange) => state.onChange(() => onStoreChange()), () => state.data, ); } function Wallet() { const profile = useSynced(client.player); return
{profile?.wallets?.map((w) => `${w.currency}: ${w.balance}`).join(', ')}
; } ``` ## Development ```bash 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.