docs: README, CHANGELOG and skill for @rudder/sdk 0.1.0 with generated module clients
CI / check (push) Successful in 15s
CI / publish (push) Has been skipped

This commit is contained in:
edmand46
2026-09-17 15:28:08 +03:00
parent 1e4e0d928d
commit aa9d9800c9
8 changed files with 270 additions and 383 deletions
+72 -71
View File
@@ -1,30 +1,31 @@
---
name: rudder-web-sdk
description: Use when working with the Rudder TypeScript/JavaScript SDK (@rudder/js-sdk) — the browser/player-facing SDK for the Rudder LiveOps platform. Covers client setup, device/google/apple/custom auth, identity linking, observable kernel domains (player, catalog, storage), modules.call, resources(slug), and error handling. Load this whenever code imports from @rudder/js-sdk or RudderClient appears.
description: Use when working with the Rudder TypeScript/JavaScript client SDK (@rudder/sdk) for web games. Covers createClient, device/google/apple/custom auth, identity linking, state objects (player with wallets and counters, catalog, storage), module calls through clients generated by `rudder client generate`, idempotent retries, and error handling. Load this whenever code imports from @rudder/sdk or a generated rudder.modules.ts.
---
# Rudder Web SDK (@rudder/js-sdk)
# Rudder Web SDK (@rudder/sdk)
Browser-facing player SDK for the Rudder LiveOps platform. Source of truth:
`liveops-web-sdk/src` (generated wire types in `src/generated` come from
`liveops-backend/apigen` — never edit them by hand).
Client SDK for web games on the Rudder LiveOps platform. Source of truth:
`liveops-web-sdk/src` (wire types in `src/generated` come from
`liveops-backend/apigen`; never edit them by hand).
2.0 kernel surface: auth, player (wallets), catalog currencies, storage,
project storage, revision sync, `client.modules.call(module, fn, args)`,
and `client.resources(slug)`. Module-specific methods (battle pass, quests,
leaderboards, stores, inventory, remote config, scenarios) are gone.
0.1.0 covers only the kernel: auth, player, catalog, storage, module calls and
internal revision sync. Game features are installed modules, reached through a
typed client generated from their manifests. There is no `resources(slug)` and
no `projectStorage`; game-facing configuration comes from modules such as
`remote_config`.
## Install
Published to a private registry, not npmjs. Point the `@rudder` scope at it in
`.npmrc` (anonymous read, no token):
Published to the Gitea registry at hub.rudder.build, not npmjs. Point the
`@rudder` scope at it in `.npmrc` (anonymous read, no token):
```
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/
```
```bash
npm install @rudder/js-sdk
npm install @rudder/sdk
```
ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
@@ -32,22 +33,21 @@ ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
## Init
```ts
import { RudderClient } from '@rudder/js-sdk';
import { createClient } from '@rudder/sdk';
const client = new RudderClient({
baseUrl: 'https://api.rudder.build', // required
projectKey: 'your-project-key', // required
const client = createClient({
projectKey: 'your-project-key', // required; selects staging or prod
// baseUrl?: string — default 'https://api.rudder.build'
// tokenStore?: TokenStore — default: localStorage, in-memory fallback
// appVersion?: string — sent with platform 'web' on login/refresh
// requestTimeoutMs?: number — default 10000
// syncIntervalMs?: number — revision poll, default 30000 (±20% jitter)
});
```
Missing `baseUrl`/`projectKey` throws `RudderError` with
`code: 'sdk/invalid-options'` from the constructor.
Call `client.dispose()` on unmount/HMR to stop the sync poll and drop cached
state.
A missing `projectKey` throws `RudderError` with
`code: 'sdk/invalid-options'`. Call `client.dispose()` on unmount/HMR to stop
the sync poll and drop cached state.
## Auth essentials
@@ -65,12 +65,9 @@ client.auth.isAuthenticated;
client.auth.onAuthStateChange(cb);
```
Linked identities are listed on the profile: `client.player.data.identities`
(`PlayerProfileIdentity { provider, subject, createdAt }`).
Tokens are saved to the `TokenStore` automatically. The transport injects
`Authorization: Bearer <token>`, retries GETs, and single-flight refreshes on
401; a failed refresh clears tokens and emits `signed-out`. Details:
`Authorization: Bearer <token>` and single-flight refreshes on 401; a failed
refresh clears tokens and emits `signed-out`. Details:
[reference/auth.md](reference/auth.md).
## Client surface
@@ -78,64 +75,68 @@ Tokens are saved to the `TokenStore` automatically. The transport injects
| Area | Access | Kind |
|---|---|---|
| Auth | `client.auth` | service |
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
| Catalog currencies | `client.catalog` | observable `SyncedState<Map<string, CatalogCurrency>>` |
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations |
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation |
| Modules | `client.modules.call(module, fn, args?)` | `CallModuleFunctionClientResponse` (`{ result }`) |
| Resources | `client.resources(slug)` | `list` / `get` / `create` / `update` / `delete` |
| Player | `client.player` (`player`, `identities`, `wallets`, `counters`) | `SyncedState<PlayerProfile>` |
| Catalog currencies | `client.catalog` | `SyncedState<Map<string, CatalogCurrency>>` |
| Player storage | `client.storage` + `.save(type, data)` / `.delete(type)` | `SyncedState<GetStorageResponse>` + mutations |
| Modules | `client.modules.call<T>(module, fn, args?, { moduleVersion? })` | resolves to `result` as `T` |
Wallets live on `client.player.data.wallets`. Game features are installed
modules, not SDK services (first-party modules: items, inventory, reward,
remote_config, leaderboards, quests, store, battlepass). Call them with
`client.modules.call('my_module', 'hello', { ... })` and read their rows with
`client.resources(slug)`. Details: [reference/modules.md](reference/modules.md),
[reference/resources.md](reference/resources.md).
## Module calls through generated clients
## Observable domains — the shared pattern
```bash
RUDDER_URL=... RUDDER_TOKEN=... RUDDER_PROJECT=... \
rudder client generate --lang ts --out src/rudder.modules.ts [--environment staging|prod]
```
`player`, `catalog`, `storage`, and `projectStorage` all extend
`SyncedState<T>`:
```ts
import { modules } from './rudder.modules';
- `.data: T | undefined`, `.status: 'idle' | 'loading' | 'ready' | 'error'`,
const m = modules(client);
await m.leaderboards.submit({ slug: 'weekly', score: 120 });
const top = await m.leaderboards.top({ slug: 'weekly', limit: 10 });
```
The generated file is a factory over `client.modules.call`, with argument and
result types, and a `<Module><Fn>Error` code union per function. Regenerate
and commit it when installed modules change. Details:
[reference/modules.md](reference/modules.md).
## State objects: the shared pattern
`player`, `catalog` and `storage` extend `SyncedState<T>`:
- `.value: T | undefined`, `.status: 'idle' | 'loading' | 'ready' | 'error'`,
`.error?: Error`
- `onChange(cb: (snapshot: SyncedSnapshot<T>) => void): () => void` — fires
immediately with the current snapshot; maps onto React `useSyncExternalStore`
- `load(): Promise<T>` — deduplicated, no-op when ready
- `reload(): Promise<T>` — forced refetch
- `onChange(cb: (snapshot: SyncedSnapshot<T>) => void): () => void`: fires
immediately with `{ status, value, error }`; maps onto React
`useSyncExternalStore`
- `load(): Promise<T>`: deduplicated, no-op when ready
- `reload(): Promise<T>`: forced refetch
`player` and `catalog` are warmed once at login; `storage` and
`projectStorage` load on first use. All observable domains are then kept fresh
by a revision poll every 30 s (±20% jitter); polling pauses while the tab is
hidden. Storage mutations invalidate their domain immediately. Call `reload()`
when you need a freshness guarantee.
`player` and `catalog` are warmed at login; `storage` loads on first use. A
revision poll (`GET /sdk/v1/sync`, keys `profile`, `catalog`, `storage`) runs
every 30 s (±20% jitter), pauses while the tab is hidden, and reloads the
states whose revision grew. Storage mutations invalidate immediately. Call
`player.reload()` after a module call that changes wallets or counters when
you need fresh values right away.
## Errors
All SDK errors extend `RudderError` (optional machine-readable `code`):
- `RudderNetworkError` — fetch failed / timeout (`error.cause` holds the original)
- `RudderHttpError` — non-2xx, carries `status`, `statusText`, `body`, `code`
- `RudderAuthError extends RudderHttpError` — 401 after failed refresh; tokens
already cleared, `signed-out` already emitted
- `RudderNetworkError`: fetch failed or timed out (`error.cause` holds the original)
- `RudderHttpError`: non-2xx, carries `status`, `statusText`, `body`, `code`
- `RudderAuthError extends RudderHttpError`: 401 after a failed refresh;
tokens already cleared, `signed-out` already emitted
- `RudderModuleError<C> extends RudderHttpError`: a module call returned 422
`{ error, code, requestId }`; exposes `code: C`, `message`, `requestId`
- `RudderError` with `code: 'sdk/invalid-options'` (`SDK_ERROR_INVALID_OPTIONS`)
— constructor validation
The platform error codes are `forbidden` and `row_referenced`
(`src/generated/errors.ts`, not exported from the package entry); compare
`error.code === 'forbidden'`.
`client.modules.call` resolves to `{ result }` only; console output and the
kernel call log never reach players. A module business error rejects with
`RudderHttpError` whose `status` is 422 and whose `code` is the module's own
code (the body is `{ error, code, requestId }`). Module codes are open strings
with no generated constants, so compare against the codes the module
documents. A module trap is a 500 and a timeout a 504, both without details.
Compare `error.code` with string literals. A module trap is a 500 and a
timeout a 504, both without details.
## Reference files
- [reference/auth.md](reference/auth.md) — login flows, options, token store, session lifecycle
- [reference/player.md](reference/player.md) — profile + wallets (`PlayerProfile`, `Wallet`)
- [reference/storage.md](reference/storage.md) — player + project storage, limits
- [reference/modules.md](reference/modules.md) — `modules.call`, response, module errors
- [reference/resources.md](reference/resources.md) — `resources(slug)` row operations
- [reference/auth.md](reference/auth.md): login flows, options, token store, session lifecycle
- [reference/player.md](reference/player.md): profile, wallets, counters
- [reference/storage.md](reference/storage.md): player storage, limits
- [reference/modules.md](reference/modules.md): generated clients, `modules.call`, idempotency, retries, errors
+2 -6
View File
@@ -101,7 +101,7 @@ await client.auth.unlinkIdentity('custom:my-backend');
`google`, `apple`, `custom`, or `custom:<name>`.
Linked identities appear on the player profile:
`client.player.data.identities: PlayerProfileIdentity[]` with
`client.player.value.identities: PlayerProfileIdentity[]` with
`{ provider: string; subject: string; createdAt: string }`.
## Environments
@@ -113,10 +113,6 @@ the client API takes an environment argument, and a player created in one
environment is invisible in the other. Content released only to `staging` is
empty for a `prod` key and vice versa.
Tokens issued before SDK 2.0.0 carry no environment claim and are rejected with
401. The transport's refresh then fails, clears the token store and emits
`'signed-out'` — log the player in again.
## Auth state
```ts
@@ -165,7 +161,7 @@ backends (sessionStorage, cookies).
## Errors
- Constructor: missing `baseUrl`/`projectKey` → `RudderError`,
- `createClient`: missing `projectKey` → `RudderError`,
`code: 'sdk/invalid-options'`.
- Login failure → `RudderHttpError` (e.g. unknown project key) or
`RudderNetworkError`.
+62 -16
View File
@@ -3,29 +3,75 @@
Source: `src/domains/ModulesDomain.ts`. Calls an installed module function as
the signed-in player through `POST /sdk/v1/modules/{module}/{fn}`.
```ts
client.modules.call(module: string, fn: string, args?: unknown): Promise<CallModuleFunctionClientResponse>
// CallModuleFunctionClientResponse = { result?: unknown }
## Generated clients
Use `rudder client generate` from `liveops-module-sdk/cli` instead of calling
modules by name:
```bash
export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...
rudder client generate --lang ts --out src/rudder.modules.ts # staging by default
rudder client generate --lang ts --out src/rudder.modules.ts --environment prod
rudder client generate --lang ts --manifests ./modules --out src/rudder.modules.ts # local manifests
```
```ts
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' });
```
The file reads the manifests of the installed modules, generates only
`trigger: client` functions, and lists every module with its version in the
header. It exports:
`args` is sent as `{ args }`. The response carries only `result`; console
output and the kernel call log stay on the platform dry run.
A business error raised by the module rejects with `RudderHttpError`
(`status` 422, body `{ error, code, requestId }`) whose `code` is the
module's own open string code; there are no generated constants for it. A
trap is a 500 and a timeout a 504, both without details.
- `modules(client)`: `{ <module>: { <fn>(args): Promise<Result> } }`
- `<Module><Fn>Args` / `<Module><Fn>Result` types from the manifest schemas
- `<Module><Fn>Error`: the function's declared codes plus `RudderKernelError`
```ts
import { createClient, RudderModuleError } from '@rudder/sdk';
import { modules, type StoreBuyError } from './rudder.modules';
const m = modules(createClient({ projectKey }));
try {
await client.modules.call('my_module', 'hello');
const result = await m.store.buy({ offer: 'starter_pack' });
} catch (error) {
if (error instanceof RudderHttpError && error.status === 422) {
console.warn(error.code); // module-defined
if (error instanceof RudderModuleError) {
switch (error.code as StoreBuyError) {
case 'insufficient_funds': /* ... */ break;
case 'offer_not_found': /* ... */ break;
}
}
}
```
Commit the generated file and regenerate it when installed modules change.
Do not edit it by hand.
## `modules.call`
```ts
client.modules.call<T = unknown>(
module: string,
fn: string,
args?: unknown,
options?: { moduleVersion?: string },
): Promise<T>
```
- Sends `{ args }`; `args` is `{}` when omitted. Resolves to the response
`result`. Console output and the kernel call log never reach players.
- Idempotency: each call generates one `Idempotency-Key` (UUID v4) and sends
the same key on every retry, so the server runs the function at most once
and returns the stored response for a repeated key. A new `call()` gets a
new key.
- Retries: up to 2, with jittered exponential backoff, on network errors, 5xx
and 409 with code `conflict`. Other 4xx responses (including other 409
codes) are never retried.
- `moduleVersion` (set by generated clients) is sent as
`X-Rudder-Module-Version: <module>@<version>`. A major mismatch with the
installed module is logged on the backend; the call still runs.
## Errors
- 422 `{ error, code, requestId }` rejects with `RudderModuleError<C>`
(`status` 422, `code`, `message` from `error`, `requestId`). `C` defaults
to `string`; use the generated union to narrow it.
- A trap is a 500 and a timeout a 504, both `RudderHttpError` without details
(after the retries above).
- 401 follows the normal refresh flow (`RudderAuthError` when it fails).
+9 -4
View File
@@ -1,7 +1,8 @@
# Player profile + wallets — `client.player`
# Player — `client.player`
`PlayerDomain` (source: `src/domains/PlayerDomain.ts`) — the observable player
profile: identity plus currency wallets. Synced under revision key `profile`.
profile: player, linked identities, currency wallets and
number counters. Synced under revision key `profile`.
There is no separate wallet service in this SDK. Wallet balances are read from
the profile and are credited/debited server-side (admin wallet adjust, module
@@ -12,7 +13,7 @@ functions).
`client.player` is a `SyncedState<PlayerProfile>`:
```ts
client.player.data // PlayerProfile | undefined
client.player.value // PlayerProfile | undefined
client.player.status // 'idle' | 'loading' | 'ready' | 'error'
client.player.onChange((snapshot) => { /* fires immediately */ });
await client.player.load();
@@ -23,6 +24,7 @@ await client.player.reload(); // force refetch
```ts
interface PlayerProfile {
counters?: { [slug: string]: number }; // client-visible number counters
identities?: PlayerProfileIdentity[];
player?: Player;
wallets?: Wallet[];
@@ -51,9 +53,12 @@ interface Wallet {
## Behavior notes
- `counters` holds the values of `number` counters whose definition is
`client_visible`; hidden counters are left out. Currency counters are in
`wallets`, not in `counters`. Counter changes bump the `profile` revision.
- Warmed automatically at login (with catalog).
- Kept fresh by the revision poll; call `reload()` after a module function
that mutates wallets.
that changes wallets or counters.
- All fields are optional on the wire (`?`); code defensively.
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
README "React recipe").
@@ -1,29 +0,0 @@
# Resources
Source: `src/domains/ResourcesDomain.ts`. Row operations on a resource as the
signed-in player through `/sdk/v1/resources/{slug}`. `client.resources(slug)`
returns a `ResourceHandle`; it performs no request by itself.
```ts
const items = client.resources('items');
items.list(): Promise<ListResourceRowsResponse> // GET /{slug} → { items?: ResourceRow[], total?: number }
items.get(id): Promise<ResourceRow> // GET /{slug}/{id}
items.create({ id?, data? }): Promise<ResourceRow> // POST /{slug}
items.update(id, { data?, version? }): Promise<ResourceRow> // PATCH /{slug}/{id}
items.delete(id): Promise<void> // DELETE /{slug}/{id}
```
`ResourceRow` is `{ id, playerId, data, version, rank, createdAt, updatedAt }`
(all optional in the generated type). Pass `version` on `update` for
optimistic concurrency.
```ts
const row = await client.resources('items').create({ data: { name: 'Sword' } });
await client.resources('items').update(row.id!, { data: { name: 'Axe' }, version: row.version });
```
What a player may read or write is decided by the resource's access rules on
the server; a denied operation rejects with `RudderHttpError` `code`
`forbidden`. Deleting a row that other rows still reference rejects with
status 409 and `code` `row_referenced`. Resources are not observable domains:
call `list()` again when you need fresh rows.
+17 -51
View File
@@ -1,14 +1,11 @@
# Storage — `client.storage` + `client.projectStorage`
# Storage — `client.storage`
Two key/value stores: per-player (`StorageDomain`, revision key `storage`) and
project-wide shared (`ProjectStorageDomain`, revision key `projectStorage`).
Sources: `src/domains/StorageDomain.ts`, `src/domains/ProjectStorageDomain.ts`.
## Player storage
Per-player storage, one item per `type`. `StorageDomain`, revision key
`storage`. Source: `src/domains/StorageDomain.ts`.
```ts
client.storage.data // GetStorageResponse | undefined
await client.storage.save(items);
client.storage.value // GetStorageResponse | undefined
await client.storage.save(type, data);
await client.storage.delete(type);
```
@@ -18,52 +15,21 @@ interface GetStorageResponse {
nextCursor?: string;
}
interface StorageItem {
data?: string; // opaque payload, JSON-stringify yourself if needed
id?: string;
type?: string; // the storage "collection" key
data?: string; // opaque payload, JSON-stringify yourself
id?: string; // server-assigned
type?: string; // the storage key
}
```
- `save(items: StorageItem[]): Promise<void>` — upserts items, then
invalidates the domain so subscribers refetch.
- `delete(type: string): Promise<void>` — deletes **all** player storage items
of that type, then invalidates.
## Project storage
```ts
client.projectStorage.data // GetProjectStorageResponse | undefined
await client.projectStorage.save(items);
```
```ts
interface GetProjectStorageResponse {
items?: ProjectStorageItem[];
nextCursor?: string;
}
interface ProjectStorageItem {
data?: string;
expiresAt?: string;
id?: string;
readPermission?: 'public' | 'serverOnly';
size?: number;
type?: string;
updatedAt?: string;
version?: number;
writePermission?: 'public' | 'serverOnly';
}
interface ProjectStorageUpdateItem { data?: string; type?: string }
```
- `save(items: ProjectStorageUpdateItem[]): Promise<void>` — upserts, then
invalidates. Writing is only possible for items whose `writePermission` is
`public`; `serverOnly` items are read-only for clients.
- The SDK exposes no client-side project-storage delete.
- `save(type: string, data: string): Promise<void>`: upserts the item of
`type` (the server keeps one item per player and type), then invalidates.
- `delete(type: string): Promise<void>`: deletes the item of `type`, then
invalidates.
## Limits and notes
- Both domains load with `{ limit: 100 }` — the observable snapshot holds at
most 100 items and `nextCursor` pagination is not surfaced by the domain.
- `data` is a raw string on the wire; serialize/deserialize JSON yourself.
- Both are observable (`SyncedState`) and warmed by the revision poll only when
in use; mutations self-invalidate.
- The state loads with `{ limit: 100 }`; `nextCursor` pagination is not
surfaced.
- `data` is a raw string on the wire; serialize and parse JSON yourself.
- Project storage is not reachable from the client SDK. It stays available to
modules, the dashboard and `/game/v1`.