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
+22 -167
View File
@@ -1,173 +1,28 @@
# Changelog # Changelog
## Unreleased ## 0.1.0
- Removed `client.effects`, the `Effects` / `EffectUnsubscribe` / First release as `@rudder/sdk`. Not compatible with `@rudder/js-sdk` 2.x.
`NotificationEffect` / `WaitEffect` / `ScenarioCompletedEffect` /
`ScenarioFailedEffect` types, and the `onEffectError` client option. Nothing
emitted effects since scenarios were removed.
- `client.modules.call` resolves to `CallModuleFunctionClientResponse`, which
carries only `result`. Console output, the kernel call log and `error` are
no longer part of the player response; they stay on the platform dry run.
- Module business errors reject with `RudderHttpError` (`status` 422, body
`{ error, code, requestId }`). `code` is defined by the module and is an
open string.
- `RudderErrorCodes` and the `RudderErrorCode` type are no longer exported
from the package. The generated platform codes are now only `forbidden` and
`row_referenced`; the scenario and quest codes (`early_completion`,
`level_not_reached`, `node_not_active`, `objectives_incomplete`,
`run_expired`, `run_not_active`, `scenario_not_active`, `unknown_run`) are
removed with those domains. Compare `error.code` with string literals.
- Login and token refresh send `platform: 'web'` and, when the new optional
`appVersion` client option is set, `appVersion`. The backend stores them on
the player for segment rules (`player.platform`, `player.appVersion`).
## 2.0.0 - `createClient({ projectKey })` creates the client; `baseUrl` defaults to
`https://api.rudder.build`.
- Kernel surface only: `auth`, `player` (player, identities, wallets,
`counters`), `catalog`, `storage`, `modules`. Revision sync keys are
`profile`, `catalog` and `storage`.
- State objects expose `value` (was `data`), `status`, `error`, `load()`,
`reload()` and `onChange()`.
- `storage.save(type, data)` upserts the single item of a type;
`storage.delete(type)` removes it.
- `modules.call<T>(module, fn, args?, { moduleVersion? })` resolves to
`result` as `T`. It always sends `args` (`{}` when omitted), one
`Idempotency-Key` per call reused on retries, and
`X-Rudder-Module-Version: <module>@<version>` when a version is given.
Retries up to 2 times with jittered backoff on network errors, 5xx and 409
`conflict`. A 422 rejects with `RudderModuleError<C>` (`code`, `message`,
`requestId`).
- Typed module clients come from `rudder client generate --lang ts`.
- Removed: `client.resources(slug)`, `client.projectStorage` and their types.
Breaking change — kernel surface. Module-specific client methods are removed. ## 2.x (@rudder/js-sdk)
The SDK now exposes the kernel only: auth, player (including wallets), catalog
currencies, storage, project storage, revision sync,
`client.modules.call(module, fn, args)`, and `client.resources(slug)`.
### Removed Deprecated. Its history is in git before the 0.1.0 commit.
- `client.battlePass` / `BattlePassService`
- `client.quests` / `QuestsService` / `QuestMetrics`
- `client.leaderboards` / `LeaderboardsService` / `LeaderboardHandle`
- `client.stores` / `StoresDomain` / `ShopHandle` / `OfferHandle`
- `client.inventory` / `InventoryDomain`
- `client.remoteConfig` / `ConfigDomain` / `RemoteConfigState`
- Scenario runtime (`ScenarioService`, login-event trigger, pending poll)
- Module-specific effects: `onStoreOffer`, `onLeaderboard`, `onQuest`,
`onBattlePass`, `onBattlePassLevel`, `onConfigChanged`
- Generated wire types for battle pass, quests, stores, inventory,
leaderboards, remote config, and scenarios
Call module functions with `client.modules.call(module, fn, args)` and read or
write resource rows with `client.resources(slug)` (`list` / `get` / `create` /
`update` / `delete`).
Also required by the backend environments release: every project now has
exactly two environments, `staging` and `prod`, and the SDK key passed as
`projectKey` decides which one the player belongs to. The environment never
appears in the client API: it is resolved at login and carried inside the
access and refresh tokens. Tokens issued before this release have no
environment claim and are rejected with 401, so the first call after the
backend upgrade refreshes, fails, clears the token store and emits
`'signed-out'` to every `onAuthStateChange` listener. Log the player in again
with `client.auth.loginWithDevice()` / `loginWithCustom()`.
## 1.0.0
- Scenario execution moved server-side. The SDK no longer walks a local DAG
or persists plan state to IndexedDB.
- Removed `RudderClientOptions.runtime.planStateStore` and the
`PlanStateStore` type.
- `GET /sdk/v1/scenarios/pending` is polled on login, on a ~30s heartbeat,
and at each effect `waitDeadline`. Completions POST
`/sdk/v1/scenarios/callback`; the response may carry the next
`PendingEffect`.
- Scenario `remote_config_override` nodes no longer emit `onConfigChanged`
(the server applies the override). The `ConfigChangedEffect` type and
`onConfigChanged` subscription remain.
## 0.6.0
- `LeaderboardSession.claim()` / effect `claim()` completes the leaderboard
node with handle `onClaim`. The server matches live rank to an authored
place and continues from that place (Grant Reward and/or In-App Message).
- `rewardClaimed()` is a deprecated alias for `claim()` and will be removed
in the next SDK version.
- `rank_not_eligible` on Claim leaves the session open (retry Claim or End);
it no longer fails the run.
## 0.5.1
- `QuestMetrics` / `reportProgress`: custom free-text metrics no longer
progress quests. Report a released catalog counter slug instead.
`purchaseOffer` / `purchaseItem` helpers are unchanged (server-side shop
fan-out).
## 0.5.0
- `client.auth.loginWithCustom({ customData, region?, language?, nickname? })`
— custom webhook auth, same token + runtime start as `loginWithDevice`.
- `QuestMetrics.purchaseOffer` / `purchaseItem` helpers for the shop purchase
metric format.
- Typecheck against regenerated models: `listQuests` no longer takes a body,
battle-pass `track` is `'free' | 'premium'`, scenario run status has no
`unknown_run`.
## 0.4.0
- Fixed `BattlePassLevelSession.level` reading node data key `level` — the
admin editor writes `levelNumber`, so tiers always reported `0`. The session
now reads `levelNumber`, matching the editor and server validation.
- UGC support removed from the generated wire types (`src/generated/ugc.ts`
deleted by apigen); no UGC surface was ever exposed by this SDK.
- Generated API types regenerated from the gateway openapi spec, adding the
new platform operations.
## 0.3.0
Breaking changes — the package was aligned with the Rudder SDK glossary and
the regenerated wire types (`src/generated`, apigen). No compatibility shims
are provided.
### Auth
- `client.auth.loginViaDevice(region, language, nickname?)` replaced by
`client.auth.loginWithDevice(options?: { region?, language?, nickname? })`.
Default region is now `'global'` (was `'en'`); default language stays `'en'`.
- New `client.auth.onAuthStateChange(cb)` — fires immediately with the current
state, then on every `'signed-in' | 'signed-out'` transition (login, logout,
and unrecoverable session expiry in the transport).
- New `client.auth.isAuthenticated`.
### Client configuration
- `tokenStore` in `RudderClientOptions` is now optional. Default:
localStorage-backed store with a silent in-memory fallback where
localStorage is unavailable (SSR, private mode). New `createDefaultTokenStore()` export.
- Invalid client options (`baseUrl` / `projectKey` missing) now throw
`RudderError` with `code: 'sdk/invalid-options'` instead of a plain `Error`.
- New `onEffectError` option — called when an effect handler throws
(default: `console.error`); previously such errors were swallowed silently.
### Renames
- `client.config` → `client.remoteConfig` (canonical glossary name).
- `client.battlepass` → `client.battlePass`; `BattlepassService` → `BattlePassService`.
- Effects: `onBattlepass` / `BattlepassEffect` → `onBattlePass` / `BattlePassEffect`;
`onBattlepassLevel` / `BattlepassLevelEffect` → `onBattlePassLevel` / `BattlePassLevelEffect`.
- Generated types: `SdkQuest` → `Quest`; `*Battlepass*` request/response types → `*BattlePass*`.
- Internal transport `sendAsync` → `request` (not part of the public surface).
### Leaderboards
- Removed `LeaderboardsService.getRanking(slug, limit)` and
`LeaderboardsService.submitScore(slug, score)`. Single path:
`client.leaderboards.findBySlug(slug)` → `handle.list(limit?)` / `handle.submit(score)`.
### Errors
- `RudderNetworkError.inner` → standard `Error.cause`.
- `RudderHttpError.code` is typed as `RudderErrorCode | (string & {})`
(generated union of server error codes, plus room for unknown strings).
- New exports: `RudderErrorCode` (type), `RudderErrorCodes` (constants),
`SDK_ERROR_INVALID_OPTIONS`, `RudderErrorCodeLike` (type).
### Public surface
- The barrel no longer exports `getOrCreateDeviceId`, domain classes
(`PlayerDomain`, …) or service constructors (`AuthService`, …) as values —
they remain available as types. Instances live on the client.
- `SubmittedBy` is no longer re-exported (UGC is unsupported on web).
### Bundle
- The scenario engine and the IndexedDB plan store are loaded lazily (dynamic
import) on first login / first runtime access — a thin client never pulls
the scenario machinery into the initial chunk.
- The package now ships both ESM (`dist/index.js`) and CJS (`dist/index.cjs`)
builds with matching `exports` entries.
+86 -39
View File
@@ -1,12 +1,13 @@
# @rudder/js-sdk # @rudder/sdk
LiveOps Web SDK for browser games — device auth, observable player state, Rudder client SDK for web games: device and social auth, player state
catalog currencies, storage, generic resources, and module function calls. (profile, wallets, counters), catalog currencies, player storage, and calls to
installed modules through generated typed clients.
## Install ## Install
The package is published to a private registry, not npmjs. Point the The package is published to the Gitea registry at hub.rudder.build, not
`@rudder` scope at it in your project's `.npmrc`: npmjs. Point the `@rudder` scope at it in your project's `.npmrc`:
``` ```
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/ @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): Then install as usual (anonymous read access, no token needed):
```bash ```bash
npm install @rudder/js-sdk npm install @rudder/sdk
``` ```
## Quickstart ## Quickstart
```ts ```ts
import { RudderClient } from '@rudder/js-sdk'; import { createClient } from '@rudder/sdk';
const client = new RudderClient({ const client = createClient({
baseUrl: 'https://api.rudder.build', projectKey: 'your-project-key', // the key selects the environment
projectKey: 'your-project-key', // baseUrl: 'https://api.rudder.build' (default)
// tokenStore is optional: defaults to localStorage with an in-memory // tokenStore: localStorage with an in-memory fallback (default)
// fallback where localStorage is unavailable (SSR, private mode). // appVersion: '1.4.2', sent with platform 'web' on login and refresh
appVersion: '1.4.2', // optional; sent with platform 'web' on login and refresh
}); });
await client.auth.loginWithDevice(); // region 'global', language 'en' await client.auth.loginWithDevice();
// or: await client.auth.loginWithDevice({ region: 'eu', language: 'de', nickname: 'Bob' });
const profile = client.player.data; const profile = client.player.value;
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance; const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
const wins = profile?.counters?.wins ?? 0;
const items = await client.resources('items').list(); await client.storage.save('progress', JSON.stringify({ level: 7 }));
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' });
client.dispose(); 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 ## Surface
| Area | Access | | Area | Access |
|---|---| |---|---|
| Auth | `client.auth.loginWithDevice()`, `loginWithGoogle({ idToken })`, `loginWithApple({ idToken })`, `loginWithCustom({ customData })`, `linkWithGoogle(idToken)`, `linkWithApple(idToken)`, `linkWithCustom({ customData })`, `unlinkIdentity(provider)`, `logout()`, `isAuthenticated`, `onAuthStateChange(cb)` | | 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) | | Player | `client.player` state: `player`, `identities`, `wallets`, `counters` |
| Catalog currencies | `client.catalog` (observable) | | Catalog currencies | `client.catalog` state (`Map<slug, CatalogCurrency>`) |
| Storage | `client.storage` (observable) + `.save(items)` / `.delete(type)` | | Storage | `client.storage` state + `.save(type, data)` / `.delete(type)` |
| Project storage | `client.projectStorage` (observable) + `.save(items)` | | Modules | `client.modules.call<T>(module, fn, args?, { moduleVersion? })`, usually through a generated client |
| Modules | `client.modules.call(module, fn, args?)` |
| Resources | `client.resources(slug)` → `list` / `get` / `create` / `update` / `delete` |
Game features are installed modules, not SDK services. The first-party `counters` holds the values of the project's client-visible number counters
modules are items, inventory, reward, remote_config, leaderboards, quests, by slug; currency balances are in `wallets`.
store and battlepass. Call a module through `client.modules.call` and read its
rows through `client.resources(slug)`.
## Error handling ## Error handling
@@ -71,30 +109,31 @@ import {
RudderNetworkError, RudderNetworkError,
RudderHttpError, RudderHttpError,
RudderAuthError, RudderAuthError,
} from '@rudder/js-sdk'; RudderModuleError,
} from '@rudder/sdk';
try { try {
await client.player.reload(); await client.player.reload();
} catch (error) { } catch (error) {
if (error instanceof RudderAuthError) { 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. // cleared tokens and emitted 'signed-out' via onAuthStateChange.
showLogin(); showLogin();
} else if (error instanceof RudderHttpError) { } else if (error instanceof RudderHttpError) {
console.error(error.status, error.code, error.body); console.error(error.status, error.code, error.body);
} else if (error instanceof RudderNetworkError) { } 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); console.error('offline?', error.cause);
} }
} }
``` ```
A module business error rejects with `RudderHttpError` (`status` 422) whose `RudderModuleError` extends `RudderHttpError` (`status` 422). Its type
`code` is the module's own open string code. The platform codes are parameter narrows `code` to a generated union such as
`forbidden` and `row_referenced`. `LeaderboardsSubmitError`. A module trap is a 500 and a timeout a 504.
Invalid client options (missing `baseUrl` / `projectKey`) throw `RudderError` A missing `projectKey` throws `RudderError` with
with `code: 'sdk/invalid-options'` from the constructor. `code: 'sdk/invalid-options'` from `createClient`.
Track the session lifecycle without polling the token store: Track the session lifecycle without polling the token store:
@@ -107,7 +146,7 @@ client.auth.onAuthStateChange((state) => {
## Data liveness model ## 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 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 a revision poll: the client asks the backend for entity revisions every
**30 s (±20% jitter)** and reloads only the entities whose revision grew. **30 s (±20% jitter)** and reloads only the entities whose revision grew.
@@ -124,10 +163,10 @@ straight onto `useSyncExternalStore`:
```tsx ```tsx
import { useSyncExternalStore } from 'react'; 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( return useSyncExternalStore(
(onStoreChange) => state.onChange(() => onStoreChange()), (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 Generated wire types (`src/generated`) come from `liveops-backend/apigen`; run
`npm run generate` to refresh them. Do not edit them by hand. `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
```
+72 -71
View File
@@ -1,30 +1,31 @@
--- ---
name: rudder-web-sdk 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: Client SDK for web games on the Rudder LiveOps platform. Source of truth:
`liveops-web-sdk/src` (generated wire types in `src/generated` come from `liveops-web-sdk/src` (wire types in `src/generated` come from
`liveops-backend/apigen` — never edit them by hand). `liveops-backend/apigen`; never edit them by hand).
2.0 kernel surface: auth, player (wallets), catalog currencies, storage, 0.1.0 covers only the kernel: auth, player, catalog, storage, module calls and
project storage, revision sync, `client.modules.call(module, fn, args)`, internal revision sync. Game features are installed modules, reached through a
and `client.resources(slug)`. Module-specific methods (battle pass, quests, typed client generated from their manifests. There is no `resources(slug)` and
leaderboards, stores, inventory, remote config, scenarios) are gone. no `projectStorage`; game-facing configuration comes from modules such as
`remote_config`.
## Install ## Install
Published to a private registry, not npmjs. Point the `@rudder` scope at it in Published to the Gitea registry at hub.rudder.build, not npmjs. Point the
`.npmrc` (anonymous read, no token): `@rudder` scope at it in `.npmrc` (anonymous read, no token):
``` ```
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/ @rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/
``` ```
```bash ```bash
npm install @rudder/js-sdk npm install @rudder/sdk
``` ```
ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies. ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
@@ -32,22 +33,21 @@ ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
## Init ## Init
```ts ```ts
import { RudderClient } from '@rudder/js-sdk'; import { createClient } from '@rudder/sdk';
const client = new RudderClient({ const client = createClient({
baseUrl: 'https://api.rudder.build', // required projectKey: 'your-project-key', // required; selects staging or prod
projectKey: 'your-project-key', // required // baseUrl?: string — default 'https://api.rudder.build'
// tokenStore?: TokenStore — default: localStorage, in-memory fallback // tokenStore?: TokenStore — default: localStorage, in-memory fallback
// appVersion?: string — sent with platform 'web' on login/refresh
// requestTimeoutMs?: number — default 10000 // requestTimeoutMs?: number — default 10000
// syncIntervalMs?: number — revision poll, default 30000 (±20% jitter) // syncIntervalMs?: number — revision poll, default 30000 (±20% jitter)
}); });
``` ```
Missing `baseUrl`/`projectKey` throws `RudderError` with A missing `projectKey` throws `RudderError` with
`code: 'sdk/invalid-options'` from the constructor. `code: 'sdk/invalid-options'`. Call `client.dispose()` on unmount/HMR to stop
the sync poll and drop cached state.
Call `client.dispose()` on unmount/HMR to stop the sync poll and drop cached
state.
## Auth essentials ## Auth essentials
@@ -65,12 +65,9 @@ client.auth.isAuthenticated;
client.auth.onAuthStateChange(cb); 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 Tokens are saved to the `TokenStore` automatically. The transport injects
`Authorization: Bearer <token>`, retries GETs, and single-flight refreshes on `Authorization: Bearer <token>` and single-flight refreshes on 401; a failed
401; a failed refresh clears tokens and emits `signed-out`. Details: refresh clears tokens and emits `signed-out`. Details:
[reference/auth.md](reference/auth.md). [reference/auth.md](reference/auth.md).
## Client surface ## Client surface
@@ -78,64 +75,68 @@ Tokens are saved to the `TokenStore` automatically. The transport injects
| Area | Access | Kind | | Area | Access | Kind |
|---|---|---| |---|---|---|
| Auth | `client.auth` | service | | Auth | `client.auth` | service |
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` | | Player | `client.player` (`player`, `identities`, `wallets`, `counters`) | `SyncedState<PlayerProfile>` |
| Catalog currencies | `client.catalog` | observable `SyncedState<Map<string, CatalogCurrency>>` | | Catalog currencies | `client.catalog` | `SyncedState<Map<string, CatalogCurrency>>` |
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations | | Player storage | `client.storage` + `.save(type, data)` / `.delete(type)` | `SyncedState<GetStorageResponse>` + mutations |
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation | | Modules | `client.modules.call<T>(module, fn, args?, { moduleVersion? })` | resolves to `result` as `T` |
| Modules | `client.modules.call(module, fn, args?)` | `CallModuleFunctionClientResponse` (`{ result }`) |
| Resources | `client.resources(slug)` | `list` / `get` / `create` / `update` / `delete` |
Wallets live on `client.player.data.wallets`. Game features are installed ## Module calls through generated clients
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).
## 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 ```ts
`SyncedState<T>`: 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` `.error?: Error`
- `onChange(cb: (snapshot: SyncedSnapshot<T>) => void): () => void` — fires - `onChange(cb: (snapshot: SyncedSnapshot<T>) => void): () => void`: fires
immediately with the current snapshot; maps onto React `useSyncExternalStore` immediately with `{ status, value, error }`; maps onto React
- `load(): Promise<T>` — deduplicated, no-op when ready `useSyncExternalStore`
- `reload(): Promise<T>` — forced refetch - `load(): Promise<T>`: deduplicated, no-op when ready
- `reload(): Promise<T>`: forced refetch
`player` and `catalog` are warmed once at login; `storage` and `player` and `catalog` are warmed at login; `storage` loads on first use. A
`projectStorage` load on first use. All observable domains are then kept fresh revision poll (`GET /sdk/v1/sync`, keys `profile`, `catalog`, `storage`) runs
by a revision poll every 30 s (±20% jitter); polling pauses while the tab is every 30 s (±20% jitter), pauses while the tab is hidden, and reloads the
hidden. Storage mutations invalidate their domain immediately. Call `reload()` states whose revision grew. Storage mutations invalidate immediately. Call
when you need a freshness guarantee. `player.reload()` after a module call that changes wallets or counters when
you need fresh values right away.
## Errors ## Errors
All SDK errors extend `RudderError` (optional machine-readable `code`): All SDK errors extend `RudderError` (optional machine-readable `code`):
- `RudderNetworkError` — fetch failed / timeout (`error.cause` holds the original) - `RudderNetworkError`: fetch failed or timed out (`error.cause` holds the original)
- `RudderHttpError` — non-2xx, carries `status`, `statusText`, `body`, `code` - `RudderHttpError`: non-2xx, carries `status`, `statusText`, `body`, `code`
- `RudderAuthError extends RudderHttpError` — 401 after failed refresh; tokens - `RudderAuthError extends RudderHttpError`: 401 after a failed refresh;
already cleared, `signed-out` already emitted 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`) - `RudderError` with `code: 'sdk/invalid-options'` (`SDK_ERROR_INVALID_OPTIONS`)
— constructor validation
The platform error codes are `forbidden` and `row_referenced` Compare `error.code` with string literals. A module trap is a 500 and a
(`src/generated/errors.ts`, not exported from the package entry); compare timeout a 504, both without details.
`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.
## Reference files ## Reference files
- [reference/auth.md](reference/auth.md) — login flows, options, token store, session lifecycle - [reference/auth.md](reference/auth.md): login flows, options, token store, session lifecycle
- [reference/player.md](reference/player.md) — profile + wallets (`PlayerProfile`, `Wallet`) - [reference/player.md](reference/player.md): profile, wallets, counters
- [reference/storage.md](reference/storage.md) — player + project storage, limits - [reference/storage.md](reference/storage.md): player storage, limits
- [reference/modules.md](reference/modules.md) — `modules.call`, response, module errors - [reference/modules.md](reference/modules.md): generated clients, `modules.call`, idempotency, retries, errors
- [reference/resources.md](reference/resources.md) — `resources(slug)` row operations
+2 -6
View File
@@ -101,7 +101,7 @@ await client.auth.unlinkIdentity('custom:my-backend');
`google`, `apple`, `custom`, or `custom:<name>`. `google`, `apple`, `custom`, or `custom:<name>`.
Linked identities appear on the player profile: 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 }`. `{ provider: string; subject: string; createdAt: string }`.
## Environments ## 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 environment is invisible in the other. Content released only to `staging` is
empty for a `prod` key and vice versa. 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 ## Auth state
```ts ```ts
@@ -165,7 +161,7 @@ backends (sessionStorage, cookies).
## Errors ## Errors
- Constructor: missing `baseUrl`/`projectKey` → `RudderError`, - `createClient`: missing `projectKey` → `RudderError`,
`code: 'sdk/invalid-options'`. `code: 'sdk/invalid-options'`.
- Login failure → `RudderHttpError` (e.g. unknown project key) or - Login failure → `RudderHttpError` (e.g. unknown project key) or
`RudderNetworkError`. `RudderNetworkError`.
+62 -16
View File
@@ -3,29 +3,75 @@
Source: `src/domains/ModulesDomain.ts`. Calls an installed module function as Source: `src/domains/ModulesDomain.ts`. Calls an installed module function as
the signed-in player through `POST /sdk/v1/modules/{module}/{fn}`. the signed-in player through `POST /sdk/v1/modules/{module}/{fn}`.
```ts ## Generated clients
client.modules.call(module: string, fn: string, args?: unknown): Promise<CallModuleFunctionClientResponse>
// CallModuleFunctionClientResponse = { result?: unknown } 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 The file reads the manifests of the installed modules, generates only
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' }); `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 - `modules(client)`: `{ <module>: { <fn>(args): Promise<Result> } }`
output and the kernel call log stay on the platform dry run. - `<Module><Fn>Args` / `<Module><Fn>Result` types from the manifest schemas
- `<Module><Fn>Error`: the function's declared codes plus `RudderKernelError`
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.
```ts ```ts
import { createClient, RudderModuleError } from '@rudder/sdk';
import { modules, type StoreBuyError } from './rudder.modules';
const m = modules(createClient({ projectKey }));
try { try {
await client.modules.call('my_module', 'hello'); const result = await m.store.buy({ offer: 'starter_pack' });
} catch (error) { } catch (error) {
if (error instanceof RudderHttpError && error.status === 422) { if (error instanceof RudderModuleError) {
console.warn(error.code); // module-defined 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 `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 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 the profile and are credited/debited server-side (admin wallet adjust, module
@@ -12,7 +13,7 @@ functions).
`client.player` is a `SyncedState<PlayerProfile>`: `client.player` is a `SyncedState<PlayerProfile>`:
```ts ```ts
client.player.data // PlayerProfile | undefined client.player.value // PlayerProfile | undefined
client.player.status // 'idle' | 'loading' | 'ready' | 'error' client.player.status // 'idle' | 'loading' | 'ready' | 'error'
client.player.onChange((snapshot) => { /* fires immediately */ }); client.player.onChange((snapshot) => { /* fires immediately */ });
await client.player.load(); await client.player.load();
@@ -23,6 +24,7 @@ await client.player.reload(); // force refetch
```ts ```ts
interface PlayerProfile { interface PlayerProfile {
counters?: { [slug: string]: number }; // client-visible number counters
identities?: PlayerProfileIdentity[]; identities?: PlayerProfileIdentity[];
player?: Player; player?: Player;
wallets?: Wallet[]; wallets?: Wallet[];
@@ -51,9 +53,12 @@ interface Wallet {
## Behavior notes ## 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). - Warmed automatically at login (with catalog).
- Kept fresh by the revision poll; call `reload()` after a module function - 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. - All fields are optional on the wire (`?`); code defensively.
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK - React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
README "React recipe"). 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 Per-player storage, one item per `type`. `StorageDomain`, revision key
project-wide shared (`ProjectStorageDomain`, revision key `projectStorage`). `storage`. Source: `src/domains/StorageDomain.ts`.
Sources: `src/domains/StorageDomain.ts`, `src/domains/ProjectStorageDomain.ts`.
## Player storage
```ts ```ts
client.storage.data // GetStorageResponse | undefined client.storage.value // GetStorageResponse | undefined
await client.storage.save(items); await client.storage.save(type, data);
await client.storage.delete(type); await client.storage.delete(type);
``` ```
@@ -18,52 +15,21 @@ interface GetStorageResponse {
nextCursor?: string; nextCursor?: string;
} }
interface StorageItem { interface StorageItem {
data?: string; // opaque payload, JSON-stringify yourself if needed data?: string; // opaque payload, JSON-stringify yourself
id?: string; id?: string; // server-assigned
type?: string; // the storage "collection" key type?: string; // the storage key
} }
``` ```
- `save(items: StorageItem[]): Promise<void>` — upserts items, then - `save(type: string, data: string): Promise<void>`: upserts the item of
invalidates the domain so subscribers refetch. `type` (the server keeps one item per player and type), then invalidates.
- `delete(type: string): Promise<void>` — deletes **all** player storage items - `delete(type: string): Promise<void>`: deletes the item of `type`, then
of that type, then invalidates. 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.
## Limits and notes ## Limits and notes
- Both domains load with `{ limit: 100 }` — the observable snapshot holds at - The state loads with `{ limit: 100 }`; `nextCursor` pagination is not
most 100 items and `nextCursor` pagination is not surfaced by the domain. surfaced.
- `data` is a raw string on the wire; serialize/deserialize JSON yourself. - `data` is a raw string on the wire; serialize and parse JSON yourself.
- Both are observable (`SyncedState`) and warmed by the revision poll only when - Project storage is not reachable from the client SDK. It stays available to
in use; mutations self-invalidate. modules, the dashboard and `/game/v1`.