# 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` 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 } ``` ### `StoreOfferEffect` — a store node became active ```ts { readonly store: ShopHandle; readonly offers: readonly OfferHandle[]; readonly message?: string; buy(offer: OfferHandle, options?: BuyOptions): Promise; dismiss(): Promise; // declines the offer and advances the run } ``` ### `LeaderboardEffect` — a leaderboard node became active ```ts { end(): Promise; rewardClaimed(): Promise } ``` ### `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>; reportProgress(objectiveId: string, amount?: number): Promise; } ``` 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; addXp(source: string, amount: number): Promise; claimReward(level: number, track?: 'free' | 'premium'): Promise; purchasePremium(): Promise; levelUp(): Promise; end(): Promise; } ``` 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 } ``` ### 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`).