docs: README, CHANGELOG and skill for @rudder/sdk 0.1.0 with generated module clients
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
# @rudder/js-sdk
|
||||
# @rudder/sdk
|
||||
|
||||
LiveOps Web SDK for browser games — device auth, observable player state,
|
||||
catalog currencies, storage, generic resources, and module function calls.
|
||||
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 a private registry, not npmjs. Point the
|
||||
`@rudder` scope at it in your project's `.npmrc`:
|
||||
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/
|
||||
@@ -15,50 +16,87 @@ The package is published to a private registry, not npmjs. Point the
|
||||
Then install as usual (anonymous read access, no token needed):
|
||||
|
||||
```bash
|
||||
npm install @rudder/js-sdk
|
||||
npm install @rudder/sdk
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ts
|
||||
import { RudderClient } from '@rudder/js-sdk';
|
||||
import { createClient } from '@rudder/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
|
||||
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(); // region 'global', language 'en'
|
||||
// or: await client.auth.loginWithDevice({ region: 'eu', language: 'de', nickname: 'Bob' });
|
||||
await client.auth.loginWithDevice();
|
||||
|
||||
const profile = client.player.data;
|
||||
const profile = client.player.value;
|
||||
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
|
||||
const wins = profile?.counters?.wins ?? 0;
|
||||
|
||||
const items = await client.resources('items').list();
|
||||
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' });
|
||||
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 / 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` |
|
||||
| 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 |
|
||||
|
||||
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)`.
|
||||
`counters` holds the values of the project's client-visible number counters
|
||||
by slug; currency balances are in `wallets`.
|
||||
|
||||
## Error handling
|
||||
|
||||
@@ -71,30 +109,31 @@ import {
|
||||
RudderNetworkError,
|
||||
RudderHttpError,
|
||||
RudderAuthError,
|
||||
} from '@rudder/js-sdk';
|
||||
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
|
||||
// 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
|
||||
// fetch failed or timed out; 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`.
|
||||
`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.
|
||||
|
||||
Invalid client options (missing `baseUrl` / `projectKey`) throw `RudderError`
|
||||
with `code: 'sdk/invalid-options'` from the constructor.
|
||||
A missing `projectKey` throws `RudderError` with
|
||||
`code: 'sdk/invalid-options'` from `createClient`.
|
||||
|
||||
Track the session lifecycle without polling the token store:
|
||||
|
||||
@@ -107,7 +146,7 @@ client.auth.onAuthStateChange((state) => {
|
||||
|
||||
## Data liveness model
|
||||
|
||||
Observable domains (`player`, `catalog`, `storage`, `projectStorage`) are
|
||||
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.
|
||||
@@ -124,10 +163,10 @@ straight onto `useSyncExternalStore`:
|
||||
```tsx
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
function useSynced<T>(state: { onChange(cb: () => void): () => void; data: T | undefined }) {
|
||||
function useSynced<T>(state: { onChange(cb: () => void): () => void; value: T | undefined }) {
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => state.onChange(() => onStoreChange()),
|
||||
() => state.data,
|
||||
() => state.value,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,3 +187,11 @@ 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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user