feat: modules and resources domains, drop removed domains, regenerated models

This commit is contained in:
edmand46
2026-09-17 12:08:03 +03:00
parent f45261439f
commit 6f37b00fb4
71 changed files with 516 additions and 4184 deletions
+30 -45
View File
@@ -1,6 +1,6 @@
---
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 domains (player, inventory, catalog, stores, remote config, storage), leaderboards, quests, battle pass, scenario effects, and error handling. Load this whenever code imports from @rudder/js-sdk or RudderClient appears.
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.
---
# Rudder Web SDK (@rudder/js-sdk)
@@ -9,6 +9,11 @@ Browser-facing player SDK for the Rudder LiveOps platform. Source of truth:
`liveops-web-sdk/src` (generated wire types in `src/generated` come from the
gateway's 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.
## Install
Published to a private registry, not npmjs. Point the `@rudder` scope at it in
@@ -36,16 +41,14 @@ const client = new RudderClient({
// requestTimeoutMs?: number — default 10000
// syncIntervalMs?: number — revision poll, default 30000 (±20% jitter)
// onEffectError?: (error) => void — default console.error
// runtime?: { loginEvent? } — advanced, see reference/scenarios.md
});
```
`client` is generic: `RudderClient<TConfig extends Record<string, unknown>>`
types `client.remoteConfig.get()`. Missing `baseUrl`/`projectKey` throws
`RudderError` with `code: 'sdk/invalid-options'` from the constructor.
Missing `baseUrl`/`projectKey` throws `RudderError` with
`code: 'sdk/invalid-options'` from the constructor.
Call `client.dispose()` on unmount/HMR to stop the sync poll, wait timers, and
drop cached state.
Call `client.dispose()` on unmount/HMR to stop the sync poll and drop cached
state.
## Auth essentials
@@ -54,14 +57,13 @@ await client.auth.loginWithDevice({ region, language, nickname }); // device id
await client.auth.loginWithGoogle({ idToken, region, language, nickname });
await client.auth.loginWithApple({ idToken, region, language, nickname });
await client.auth.loginWithCustom({ customData, provider?, region, language, nickname });
// identity linking (requires an active session):
await client.auth.linkWithGoogle(idToken);
await client.auth.linkWithApple(idToken);
await client.auth.linkWithCustom({ customData, provider? });
await client.auth.unlinkIdentity(provider); // 'device' | 'google' | 'apple' | 'custom' | 'custom:<name>'
client.auth.logout();
client.auth.isAuthenticated; // boolean
client.auth.onAuthStateChange(cb); // fires immediately, returns unsubscribe
client.auth.isAuthenticated;
client.auth.onAuthStateChange(cb);
```
Linked identities are listed on the profile: `client.player.data.identities`
@@ -72,31 +74,28 @@ Tokens are saved to the `TokenStore` automatically. The transport injects
401; a failed refresh clears tokens and emits `signed-out`. Details:
[reference/auth.md](reference/auth.md).
## Client surface (all domains that exist)
Every member below is a property on `RudderClient`. There is no UGC, wallet
service, or standalone economy service in this SDK — wallets live on
`client.player.data.wallets`, and purchases go through `client.stores`.
## Client surface
| Area | Access | Kind |
|---|---|---|
| Auth | `client.auth` | service |
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
| Inventory (catalog-merged) | `client.inventory` | observable `SyncedState<InventoryItem[]>` |
| Catalog | `client.catalog` | observable `SyncedState<Map<string, CatalogItem>>` |
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerSlug, opts?)` | observable `SyncedState<ShopHandle[]>` |
| Remote config | `client.remoteConfig` + `.get(key, default?)` | observable, typed |
| 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 |
| Leaderboards | `client.leaderboards.findBySlug(slug)` | service → cached handle |
| Battle pass | `client.battlePass` | plain service (no live sync) |
| Quests | `client.quests` | plain service (no live sync) |
| Scenario effects | `client.effects.on*` | event subscriptions |
| Modules | `client.modules.call(module, fn, args?)` | `CallModuleFunctionResponse` |
| Resources | `client.resources(slug)` | `list` / `get` / `create` / `update` / `delete` |
There is no UGC, wallet service, or standalone economy service — wallets live
on `client.player.data.wallets`. Quests, stores, inventory, leaderboards,
battle pass, and remote config are installed modules: call
`client.modules.call('quests', 'list')` (etc.) and read rows with
`client.resources('items')`.
## Observable domains — the shared pattern
`player`, `catalog`, `inventory`, `remoteConfig`, `storage`,
`projectStorage`, and `stores` all extend `SyncedState<T>`:
`player`, `catalog`, `storage`, and `projectStorage` all extend
`SyncedState<T>`:
- `.data: T | undefined`, `.status: 'idle' | 'loading' | 'ready' | 'error'`,
`.error?: Error`
@@ -105,16 +104,12 @@ service, or standalone economy service in this SDK — wallets live on
- `load(): Promise<T>` — deduplicated, no-op when ready
- `reload(): Promise<T>` — forced refetch
`remoteConfig`, `player`, `stores`, and `catalog` are warmed once at login;
`inventory`, `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. Mutations (purchases, storage writes,
scenario callbacks) invalidate affected domains immediately. Call `reload()`
`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.
`leaderboards`, `battlePass`, and `quests` are NOT observable — refetch
explicitly after mutations.
## Errors
All SDK errors extend `RudderError` (optional machine-readable `code`):
@@ -126,21 +121,11 @@ All SDK errors extend `RudderError` (optional machine-readable `code`):
- `RudderError` with `code: 'sdk/invalid-options'` (`SDK_ERROR_INVALID_OPTIONS`)
— constructor validation
Server error codes (`RudderErrorCodes`, from `src/generated/errors.ts`):
`early_completion`, `forbidden`, `level_not_reached`, `node_not_active`,
`objectives_incomplete`, `run_expired`, `run_not_active`,
`scenario_not_active`, `unknown_run`. Compare with
`error.code === RudderErrorCodes.runExpired`.
Server error codes (`RudderErrorCodes`, from `src/generated/errors.ts`) are
compared with `error.code === RudderErrorCodes.forbidden`.
## 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/inventory.md](reference/inventory.md) — inventory domain, merged `InventoryItem`, catalog
- [reference/stores.md](reference/stores.md) — `ShopHandle`/`OfferHandle`, purchases, idempotency
- [reference/remote-config.md](reference/remote-config.md) — typed `get()`, value types, overrides
- [reference/storage.md](reference/storage.md) — player + project storage, limits
- [reference/leaderboards.md](reference/leaderboards.md) — handles, submit, list
- [reference/quests.md](reference/quests.md) — list/claim/reportProgress, `QuestMetrics`
- [reference/battlepass.md](reference/battlepass.md) — progress, XP, claims, premium
- [reference/scenarios.md](reference/scenarios.md) — effects, scenario runtime, wait/quest/battlepass nodes
+5 -5
View File
@@ -70,9 +70,9 @@ interface LoginViaAppleResponse { accessToken?: string; refreshToken?: string }
interface LoginViaCustomResponse { accessToken?: string; refreshToken?: string }
```
On success both tokens are saved to the client's `TokenStore`, the runtime
starts (domains warmed, scenario engine restored, `player_login` event fired),
and the state flips to `'signed-in'`.
On success both tokens are saved to the client's `TokenStore`, player and
catalog are warmed, the revision poll starts, and the state flips to
`'signed-in'`.
## Identity linking
@@ -127,8 +127,8 @@ type AuthStateListener = (state: AuthState) => void;
- `isAuthenticated` is `true` while an access token is present in the token store.
- `onAuthStateChange` fires the listener **immediately** with the current state
and returns an unsubscribe function.
- `logout()` clears tokens, stops the runtime (sync poll, scenario runs,
cached domain data), and emits `'signed-out'`.
- `logout()` clears tokens, stops the revision poll, drops cached domain
data, and emits `'signed-out'`.
## Token refresh (automatic, transport level)
@@ -1,84 +0,0 @@
# Battle pass — `client.battlePass`
`BattlePassService` (source: `src/battlepass/BattlePassService.ts`).
Call-and-response access to battle pass endpoints. Battle pass state is tied to
a **scenario battle pass node**, so calls carry `scenarioSlug` + `nodeId` (plus
`runId` for mutating calls). NOT observable — re-fetch progress explicitly
after a mutation.
In most games you do not call this service directly: a scenario battle pass
node surfaces through `client.effects.onBattlePass` with a session object that
wraps these calls (see `reference/scenarios.md`). Use the service directly when
you already know the scenario/node/run identifiers.
## Methods
```ts
getProgress(scenarioSlug: string, nodeId: string): Promise<GetBattlePassProgressResponse>
addXp(request: AddBattlePassXpRequest): Promise<AddBattlePassXpResponse>
claimReward(request: ClaimBattlePassRewardRequest): Promise<ClaimBattlePassRewardResponse>
purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise<PurchaseBattlePassPremiumResponse>
```
## Request / response types
```ts
interface AddBattlePassXpRequest {
amount?: number;
nodeId?: string;
runId?: string;
scenarioSlug?: string;
source?: string; // configured XP source
}
interface AddBattlePassXpResponse {
level?: number;
leveledUp?: boolean;
maxLevel?: boolean;
xp?: number;
}
interface ClaimBattlePassRewardRequest {
level?: number;
nodeId?: string;
runId?: string;
scenarioSlug?: string;
track?: 'free' | 'premium';
}
interface ClaimBattlePassRewardResponse {
alreadyClaimed?: boolean;
error?: string;
granted?: Reward[]; // { amount?, currency?, itemId? }
success?: boolean;
}
interface GetBattlePassProgressResponse {
claimedTiers?: ClaimedTier[]; // { level?, track? }
level?: number;
premiumOwned?: boolean;
xp?: number;
}
interface PurchaseBattlePassPremiumRequest {
idempotencyKey?: string;
nodeId?: string;
runId?: string;
scenarioSlug?: string;
}
interface PurchaseBattlePassPremiumResponse {
error?: string;
success?: boolean;
}
```
## Semantics
- `addXp` credits XP from a configured source; returns the new `xp`/`level`
plus `leveledUp` / `maxLevel` flags.
- `claimReward` claims a tier reward at a reached level; idempotent
server-side (`alreadyClaimed`). Claiming a tier above the current level (or
a premium tier without premium) fails server-side — check `success`/`error`
in the response and the typed error `code` (`RudderErrorCodes`).
- `purchasePremium` charges the player's wallet; idempotent; pass your own
`idempotencyKey` for safe retries.
- Prefer the `onBattlePass` effect session, which binds `scenarioSlug` /
`nodeId` / `runId` and posts scenario callbacks for you.
@@ -1,56 +0,0 @@
# Inventory + catalog — `client.inventory`, `client.catalog`
## Inventory
`InventoryDomain` (source: `src/domains/InventoryDomain.ts`) — owned items
merged with their catalog entries. Synced under revision key `inventory`;
also refreshed whenever the catalog changes.
```ts
client.inventory.data // InventoryItem[] | undefined
client.inventory.onChange(cb);
await client.inventory.load() / .reload();
```
### `InventoryItem` (merged view, source: `src/state/inventory.ts`)
```ts
interface InventoryItem {
slug: string; // '' when the wire item has no slug
amount: number; // 0 when absent on the wire
name: string; // nameOverride || catalog name || slug
properties: Record<string, unknown>; // catalog properties + propertiesOverride (override wins)
tags: string[]; // from the catalog entry
}
```
The raw wire shape is `PlayerInventoryItem`
(`slug?, amount?, nameOverride?, propertiesOverride?, updatedAt?`) — you rarely
need it; the domain hands out the merged `InventoryItem`.
## Catalog
`CatalogDomain` (source: `src/domains/CatalogDomain.ts`) — the item catalog
keyed by slug. Synced under revision key `catalog`.
```ts
client.catalog.data // Map<string, CatalogItem> | undefined
interface CatalogItem {
name?: string;
properties?: { [key: string]: unknown };
slug?: string;
tags?: string[];
}
```
Items without a `slug` are skipped when the map is built.
## Behavior notes
- The catalog is warmed at login; inventory loads on first use, waits on the
catalog load, and merges, so `client.inventory.data` always has catalog
fields filled in.
- Invalidated after store purchases and scenario callbacks.
- Inventory has no client-side mutations — items change via purchases, quest /
battle pass rewards, and scenario nodes (all server-side).
@@ -1,50 +0,0 @@
# Leaderboards — `client.leaderboards`
`LeaderboardsService` (source: `src/leaderboards/LeaderboardsService.ts`).
Plain call-and-response service, NOT observable — refetch explicitly.
## Surface
```ts
const board = client.leaderboards.findBySlug('weekly-kills'); // cached handle
await board.submit(score);
const entries = await board.list(limit?);
board.getEntries();
```
## `LeaderboardHandle`
```ts
class LeaderboardHandle {
readonly slug: string;
getEntries(): readonly RankEntry[]; // last fetched list, [] initially
submit(score: number): Promise<void>;
list(limit = 100): Promise<readonly RankEntry[]>; // fetches and caches
}
```
- `findBySlug(slug)` caches handles per slug — repeated calls return the same
instance (and its cached entries).
- `list(limit)`: `limit <= 0` sends no limit to the server; default is 100.
- `submit` does not update the cached entries; call `list()` afterwards to see
the effect.
## Types
```ts
interface RankEntry {
playerId?: string;
playerName?: string;
rank?: number;
score?: number;
}
```
## Notes
- No player-around-me or metadata endpoints are exposed by this SDK — submit
and top-N list only.
- Scenario `leaderboard` nodes surface through
`client.effects.onLeaderboard` (`end()`, `rewardClaimed()`) — see
`reference/scenarios.md`.
+5 -6
View File
@@ -4,8 +4,8 @@
profile: identity plus currency wallets. 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 (purchases, quest/battle pass
rewards, scenario nodes).
the profile and are credited/debited server-side (admin wallet adjust, module
functions).
## Surface
@@ -51,10 +51,9 @@ interface Wallet {
## Behavior notes
- Warmed automatically at login (one of the four parallel warm loads).
- Invalidated (refetched) after every successful store purchase and after
scenario server callbacks — subscribers see fresh balances without waiting
for the revision poll.
- Warmed automatically at login (with catalog).
- Kept fresh by the revision poll; call `reload()` after a module function
that mutates wallets.
- All fields are optional on the wire (`?`); code defensively.
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
README "React recipe").
-75
View File
@@ -1,75 +0,0 @@
# Quests — `client.quests`
`QuestsService` (source: `src/quests/QuestsService.ts`). The player's **global**
quests — list, claim, report metric progress. Plain call-and-response service,
NOT observable (no sync revision key): re-list after a claim or report.
Distinct from scenario quest nodes, which advance through
`client.effects.onQuest` (see `reference/scenarios.md`).
## Methods
```ts
list(): Promise<Quest[]>
claim(questSlug: string): Promise<ClaimQuestResponse>
reportProgress(metric: string, amount: number): Promise<string[]> // slugs of quests completed by this report
```
## Types
```ts
interface Quest {
slug?: string; // stable across environments — what claim() takes
name?: string;
objectives?: QuestObjectiveProgress[];
rewards?: Reward[];
status?: 'active' | 'claimed' | 'completed';
}
interface QuestObjectiveProgress {
completed?: boolean;
current?: number;
metric?: string;
objectiveId?: string;
target?: number;
}
interface ClaimQuestResponse {
alreadyClaimed?: boolean;
error?: string;
granted?: Reward[]; // { amount?, currency?, itemId? }
success?: boolean;
}
```
## Usage notes
```ts
const quests = await client.quests.list();
for (const quest of quests) {
if (quest.status === 'completed' && quest.slug) {
const res = await client.quests.claim(quest.slug);
// res.success / res.alreadyClaimed / res.granted
}
}
const completedSlugs = await client.quests.reportProgress('kills', 1);
```
- `claim` is idempotent server-side; check `success` / `alreadyClaimed` /
`error` in the response rather than relying on exceptions.
- Objective completion is judged server-side from metric reports.
## `QuestMetrics` helpers
Exported as a namespace: `import { QuestMetrics } from '@rudder/js-sdk'`
(source: `src/quests/QuestMetrics.ts`).
```ts
QuestMetrics.purchaseOffer('starter-pack'); // "purchase.offer:starter-pack"
QuestMetrics.purchaseItem('moonberry'); // "purchase.item:moonberry"
```
Purchase metrics are reported automatically server-side by the store purchase
fan-out — these helpers exist so quest configs and client code name the format
consistently. Catalog counter slugs are reported via `reportProgress`. Custom
free-text metrics no longer progress quests — they no-op at runtime.
@@ -1,63 +0,0 @@
# Remote config — `client.remoteConfig`
`ConfigDomain<TConfig>` (source: `src/domains/ConfigDomain.ts`, base class
`src/state/RemoteConfigState.ts`) — typed remote configuration as an
observable entity. Synced under revision key `config`.
## Typed access
```ts
interface GameConfig extends Record<string, unknown> {
player_speed: number;
feature_x: boolean;
}
const client = new RudderClient<GameConfig>({ baseUrl, projectKey });
client.remoteConfig.get('player_speed', 200); // number
client.remoteConfig.get('feature_x'); // boolean | undefined
```
`get()` overloads:
```ts
get(key): TConfig[key] | undefined;
get(key, defaultValue: TConfig[key]): TConfig[key];
```
- Values are parsed synchronously from the loaded snapshot according to the
config's server-declared `valueType`.
- Returns the default value (or `undefined`) while not loaded, for unknown
keys, and when parsing fails.
- Parsing rules: `int`/`integer` → `parseInt`; `float`/`double`/`number` →
`parseFloat`; `bool`/`boolean` → `value === 'true'`; `json`/`object` →
`JSON.parse`; anything else → raw string.
## Observable
```ts
client.remoteConfig.data // Map<string, RemoteConfig> | undefined
client.remoteConfig.onChange(cb); // fires immediately
await client.remoteConfig.reload();
```
```ts
interface RemoteConfig {
active?: boolean; createdAt?: string; description?: string;
environment?: string; id?: string; key?: string; projectId?: string;
updatedAt?: string; value?: string;
valueType?: 'bool' | 'float' | 'int' | 'json' | 'string';
}
```
Only configs that are not explicitly `active: false` enter the map (the server
already filters inactive configs out and may omit the flag).
## Behavior notes
- Warmed at login.
- Scenario `remote_config_override` nodes are applied server-side and do not
reach the client. Reload or wait for the config sync poll to observe the
patched value via `get()`.
- Without a `TConfig` type argument, `RemoteConfigShape` defaults to
`Record<string, unknown>` and `get()` returns `unknown`.
@@ -1,166 +0,0 @@
# Scenarios + effects — `client.effects`
Scenarios are server-authored node graphs (configured in the dashboard) that
run per player. The server executes the graph. The SDK is a thin effects
client: it sends trigger events, turns `PendingEffect` payloads into typed
`client.effects` handlers, posts callbacks when the game completes an
effect, and polls for pending effects. The runtime is deliberately **not**
part of the public client surface (source: `src/effects/EffectsCenter.ts`)
and loads lazily on first login.
## Lifecycle
- After login the SDK fetches `GET /sdk/v1/scenarios/pending` (replacing any
local restore), then fires the login event (`'player_login'` by default).
- A heartbeat polls pending every ~30s (±20% jitter), paused while
`document.hidden`. Each effect with a `waitDeadline` also schedules a
local timer so waits fire on time.
- An effect with an already-active `(runId, nodeId)` is not re-emitted.
- `logout()` / `dispose()` stop the poll, cancel wait timers, and drop
active effects. There is no local plan persistence.
### Runtime options (`RudderClientOptions.runtime`)
```ts
runtime?: {
// Scenario event fired automatically after login.
// undefined = 'player_login'; null = fire nothing.
loginEvent?: string | null;
}
```
## Subscribing
Every `on*` method takes a handler `(effect) => void | Promise<void>` and
returns an unsubscribe function. Errors thrown inside handlers go to the
`onEffectError` client option (default `console.error`).
```ts
const off = client.effects.onNotification(async (n) => {
showToast(n.title, n.message);
await n.done(); // always resolve the session or the run stalls
});
```
## Effect types
```ts
interface Effects {
onNotification(handler): EffectUnsubscribe;
onStoreOffer(handler): EffectUnsubscribe;
onLeaderboard(handler): EffectUnsubscribe;
onConfigChanged(handler): EffectUnsubscribe;
onWait(handler): EffectUnsubscribe;
onScenarioCompleted(handler): EffectUnsubscribe;
onScenarioFailed(handler): EffectUnsubscribe;
onQuest(handler): EffectUnsubscribe;
onBattlePass(handler): EffectUnsubscribe;
onBattlePassLevel(handler): EffectUnsubscribe;
}
```
`onConfigChanged` remains on the public surface but scenario
`remote_config_override` nodes no longer reach the client — the server
applies them. Patched values show up through `client.remoteConfig` after
sync/reload.
### `NotificationEffect` — a notification node became active
```ts
{ readonly title: string; readonly message: string; done(): Promise<void> }
```
### `StoreOfferEffect` — a store node became active
```ts
{
readonly store: ShopHandle;
readonly offers: readonly OfferHandle[];
readonly message?: string;
buy(offer: OfferHandle, options?: BuyOptions): Promise<PurchaseOfferResponse>;
dismiss(): Promise<void>; // declines the offer and advances the run
}
```
### `LeaderboardEffect` — a leaderboard node became active
```ts
{ end(): Promise<void>; claim(): Promise<void>; rewardClaimed(): Promise<void> }
```
`rewardClaimed()` is a deprecated alias for `claim()`.
### `WaitEffect` — a wait node became active
```ts
{ readonly deadlineUtc: Date } // SDK polls pending at the deadline; the server advances the run
```
### `QuestEffect` — a scenario quest node became active
```ts
{
readonly name: string;
readonly objectives: ReadonlyArray<Record<string, unknown>>;
reportProgress(objectiveId: string, amount?: number): Promise<void>;
}
```
The node auto-completes server-side once every objective is satisfied; the
counter response may carry the next `PendingEffect`.
### `BattlePassEffect` — a battle pass node became active
```ts
{
getProgress(): Promise<GetBattlePassProgressResponse>;
addXp(source: string, amount: number): Promise<AddBattlePassXpResponse>;
claimReward(level: number, track?: 'free' | 'premium'): Promise<ClaimBattlePassRewardResponse>;
purchasePremium(): Promise<PurchaseBattlePassPremiumResponse>;
levelUp(): Promise<void>;
end(): Promise<void>;
}
```
These wrap `client.battlePass` with the run's `scenarioSlug`/`nodeId`/`runId`
already bound — prefer them over calling the service manually.
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
```ts
{ readonly level: number; claim(): Promise<void> }
```
### Run lifecycle effects
```ts
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string }
interface ScenarioFailedEffect {
readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string;
readonly error: Error;
}
```
Always subscribe to `onScenarioFailed` — otherwise run failures surface only
as console warnings.
## Supported node types
`wait`, `notification`, `store`, `leaderboard`, `quest`, `battlepass`,
`battlepass_level`. Any other node type fails the run (surfaced via
`onScenarioFailed`). `remote_config_override` is applied server-side and is
not delivered to the client.
## Reliability notes
- Completion methods POST `/sdk/v1/scenarios/callback` with
`{scenarioSlug, runId, nodeId, handle}` using the handles `output`,
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
`onPremiumPurchase`.
- Transient callback failures (network error, 5xx) leave the effect active
for retry; `unknown_run` / `run_expired` drop that run's effects and fire
`onScenarioFailed`.
- Server scenario errors use the typed codes `run_expired`, `run_not_active`,
`node_not_active`, `scenario_not_active`, `unknown_run`,
`early_completion`, `objectives_incomplete`, `level_not_reached`,
`forbidden` (`RudderErrorCodes`).
-82
View File
@@ -1,82 +0,0 @@
# Stores / purchases — `client.stores`
`StoresDomain` (source: `src/domains/StoresDomain.ts`, handles in
`src/state/shops.ts`). The observable store list plus the purchase executor.
Synced under revision key `stores`. This is the SDK's economy surface — there
is no separate economy service.
## Surface
```ts
client.stores.data // ShopHandle[] | undefined
client.stores.onChange(cb);
await client.stores.load() / .reload();
// Direct purchase (bypassing handles):
await client.stores.purchase(storeSlug, offerSlug, options?);
```
## Handles
```ts
class ShopHandle {
readonly slug: string;
readonly name?: string;
readonly description?: string;
readonly data?: { [key: string]: unknown };
readonly offers: OfferHandle[];
}
class OfferHandle {
readonly id: string; // authoring id, differs between staging and prod
readonly slug: string; // stable across environments — what purchases use
readonly name?: string;
readonly price?: OfferPrice; // { amount?: number; currency?: string }
readonly contents: OfferContent[]; // { amount?: number; itemId?: string }[]
readonly maxPurchases?: number;
buy(options?: BuyOptions): Promise<PurchaseOfferResponse>;
}
interface BuyOptions { idempotencyKey?: string }
interface PurchaseOfferResponse {
error?: string;
purchaseId?: string;
success?: boolean;
}
```
## Purchase semantics
- `stores.purchase(storeSlug, offerSlug, options?)` and `offer.buy(options?)` are
the same executor; handles just bind the store slug and the offer slug.
- When `options.idempotencyKey` is omitted the SDK generates
`crypto.randomUUID()` per call — safe retries require passing your own key.
- On success the executor invalidates `player`, `inventory`, and `stores`, so
subscribers observe fresh wallet/inventory/store data immediately.
- Check `response.success` / `response.error` — a failed purchase is a resolved
response, not necessarily a thrown error.
- Purchase metrics (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) are
reported to quests automatically server-side — do not report them manually
(see `reference/quests.md`).
## Wire types
```ts
interface Store {
createdAt?: string; data?: { [key: string]: unknown }; description?: string;
environment?: string; id?: string; name?: string; offers?: Offer[];
projectId?: string; scenarioId?: string; slug?: string; status?: string;
updatedAt?: string;
}
interface Offer {
contents?: OfferContent[]; createdAt?: string; id?: string;
maxPurchases?: number; name?: string; price?: OfferPrice; slug?: string;
updatedAt?: string;
}
```
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
without `slug` / an offer without `id` or `slug` — in practice the domain only
builds handles from server data that has all of them.