Files
rudder-js-sdk/skills/rudder-web-sdk/reference/scenarios.md
T
edmand46 8753239fbd
CI / check (push) Successful in 55s
CI / publish (push) Has been skipped
Add agent skill (SKILL.md + per-domain reference)
2026-08-29 11:46:20 +03:00

162 lines
5.1 KiB
Markdown

# Scenarios + effects — `client.effects`
Scenarios are server-authored node graphs (configured in the dashboard) that
run per player. The SDK's scenario runtime is deliberately **not** part of the
public client surface: scenario nodes surface as typed effects through
`client.effects` (source: `src/effects/EffectsCenter.ts`), and the engine
itself loads lazily on first login.
## Lifecycle
- The runtime starts after login: scenario state is restored from persistence,
then the login event (`'player_login'` by default) is fired to trigger
event-driven scenarios.
- Runs persist to IndexedDB by default, so waits and pending nodes survive
page reloads. On restore, each run is reconciled with the server
(`unknown_run` / `expired` runs are dropped).
- `logout()` / `dispose()` clear all runs and persisted state.
### Runtime options (`RudderClientOptions.runtime`)
```ts
runtime?: {
// Scenario plan persistence. undefined = default IndexedDB store (created
// lazily); null = disable persistence entirely; or pass a PlanStateStore.
planStateStore?: PlanStateStore | null;
// 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;
}
```
### `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>; rewardClaimed(): Promise<void> }
```
### `ConfigChangedEffect` — a `remote_config_override` node patched config
```ts
{ readonly key: string } // client.remoteConfig.get(key) already returns the override
```
### `WaitEffect` — a wait node became active
```ts
{ readonly deadlineUtc: Date } // run resumes automatically at the deadline
```
### `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 SDK
crosses the `onComplete` boundary itself when the server reports completion.
### `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 `scenarioId`/`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 scenarioId: string }
interface ScenarioFailedEffect {
readonly runId: string; readonly scenarioId: string; readonly nodeId: string;
readonly error: Error; // transport gave up, or unsupported node type
}
```
Always subscribe to `onScenarioFailed` — otherwise run failures surface only
as console warnings.
## Supported node types
`wait`, `remote_config_override`, `notification`, `store`, `leaderboard`,
`quest`, `battlepass`, `battlepass_level`. Any other node type fails the run
(surfaced via `onScenarioFailed`).
## Reliability notes
- Node completion is idempotent client-side (completed handles are tracked).
- Transient boundary failures (network error, 5xx) leave the node active for
retry on reconnect; terminal server errors fail the run.
- 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`).