Files
rudder-js-sdk/README.md
T
edmand46 aa9d9800c9
CI / check (push) Successful in 15s
CI / publish (push) Has been skipped
docs: README, CHANGELOG and skill for @rudder/sdk 0.1.0 with generated module clients
2026-09-17 15:28:08 +03:00

198 lines
6.7 KiB
Markdown

# @rudder/sdk
Rudder client SDK for web games: device and social auth, player state
(profile, wallets, counters), 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):
```bash
npm install @rudder/sdk
```
## Quickstart
```ts
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?.wallets?.find((w) => w.currency === 'gold')?.balance;
const wins = profile?.counters?.wins ?? 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:
```bash
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
```
```ts
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`, `wallets`, `counters` |
| 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 |
`counters` holds the values of the project's client-visible number counters
by slug; currency balances are in `wallets`.
## Error handling
All SDK errors extend `RudderError` and carry an optional machine-readable
`code`:
```ts
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:
```ts
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`:
```tsx
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?.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 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:
```bash
rudder client generate --lang ts --manifests <dir with leaderboards.manifest.json> --out test/fixtures/rudder.modules.ts
```