Files
rudder-js-sdk/README.md
T

150 lines
4.9 KiB
Markdown
Raw Normal View History

2026-08-12 14:02:25 +03:00
# @rudder/js-sdk
LiveOps Web SDK for browser games — device auth, observable player state,
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).
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;
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
2026-08-12 14:02:25 +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) |
| Catalog currencies | `client.catalog` (observable) |
2026-08-12 14:02:25 +03:00
| 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)`.
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);
}
}
```
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`.
2026-08-12 14:02:25 +03:00
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 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.