Files
rudder-js-sdk/skills/rudder-web-sdk/reference/scenarios.md
T
edmand46 7271874cac
CI / check (push) Successful in 16s
CI / publish (push) Has been skipped
2.0.0: changelog and skill docs for slugs and environments
Claude-Session: https://claude.ai/code/session_01SMCvdwDmuxoaqGgvGBLk1V
2026-09-06 22:39:03 +03:00

5.5 KiB

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)

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).

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

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

{ readonly title: string; readonly message: string; done(): Promise<void> }

StoreOfferEffect — a store node became active

{
  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

{ end(): Promise<void>; claim(): Promise<void>; rewardClaimed(): Promise<void> }

rewardClaimed() is a deprecated alias for claim().

WaitEffect — a wait node became active

{ readonly deadlineUtc: Date }   // SDK polls pending at the deadline; the server advances the run

QuestEffect — a scenario quest node became active

{
  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

{
  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)

{ readonly level: number; claim(): Promise<void> }

Run lifecycle effects

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).