2026-08-12 14:02:25 +03:00
|
|
|
# @rudder/js-sdk
|
|
|
|
|
|
|
|
|
|
LiveOps Web SDK for browser games — device auth, observable player state,
|
2026-09-17 12:08:03 +03:00
|
|
|
catalog currencies, storage, generic resources, and module function calls.
|
2026-08-12 14:02:25 +03:00
|
|
|
|
|
|
|
|
## 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).
|
2026-09-17 12:08:03 +03:00
|
|
|
appVersion: '1.4.2', // optional; sent with platform 'web' on login and refresh
|
2026-08-12 14:02:25 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await client.auth.loginWithDevice(); // region 'global', language 'en'
|
|
|
|
|
// or: await client.auth.loginWithDevice({ region: 'eu', language: 'de', nickname: 'Bob' });
|
|
|
|
|
|
|
|
|
|
const profile = client.player.data;
|
2026-09-17 12:08:03 +03:00
|
|
|
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
|
2026-08-12 14:02:25 +03:00
|
|
|
|
2026-09-17 12:08:03 +03:00
|
|
|
const items = await client.resources('items').list();
|
|
|
|
|
const result = await client.modules.call('quests', 'list');
|
2026-08-12 14:02:25 +03:00
|
|
|
|
|
|
|
|
client.dispose();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Surface
|
|
|
|
|
|
|
|
|
|
| Area | Access |
|
|
|
|
|
|---|---|
|
2026-08-28 15:18:31 +03:00
|
|
|
| Auth | `client.auth.loginWithDevice()`, `client.auth.loginWithCustom({ customData })`, `client.auth.logout()`, `client.auth.isAuthenticated`, `client.auth.onAuthStateChange(cb)` |
|
2026-08-12 14:02:25 +03:00
|
|
|
| Player / wallets | `client.player` (observable) |
|
2026-09-17 12:08:03 +03:00
|
|
|
| Catalog currencies | `client.catalog` (observable) |
|
2026-08-12 14:02:25 +03:00
|
|
|
| Storage | `client.storage` (observable) + `.save(items)` / `.delete(type)` |
|
2026-09-17 12:08:03 +03:00
|
|
|
| Project storage | `client.projectStorage` (observable) + `.save(items)` |
|
|
|
|
|
| Modules | `client.modules.call(module, fn, args?)` |
|
|
|
|
|
| Resources | `client.resources(slug)` → `list` / `get` / `create` / `update` / `delete` |
|
2026-08-25 16:25:32 +03:00
|
|
|
|
2026-09-17 12:08:03 +03:00
|
|
|
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)`.
|
2026-08-12 14:02:25 +03:00
|
|
|
|
|
|
|
|
## 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-09-17 12:08:03 +03:00
|
|
|
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.
|
2026-08-12 14:02:25 +03:00
|
|
|
|
|
|
|
|
## React recipe
|
|
|
|
|
|
|
|
|
|
`SyncedState.onChange` fires immediately with the current snapshot, which maps
|
|
|
|
|
straight onto `useSyncExternalStore`:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
```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 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.
|