- engine/ (DagWalker, sessions, IndexedDbPlanStore) deleted; server owns the graph - trigger/callback/counter carry PendingEffect; GET /sdk/v1/scenarios/pending polled (30s jittered heartbeat paused on hidden tab + wait-deadline timers) - client.effects public API unchanged (done/buy/dismiss/end/claim/battlepass/quest) - planStateStore removed; loginEvent kept; reconcile() makes server the source of truth - generated models regenerated; skills/README/CHANGELOG updated
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 awaitDeadlinealso 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 scenarioId/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 scenarioId: string }
interface ScenarioFailedEffect {
readonly runId: string; readonly scenarioId: 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/callbackwith{scenarioId, runId, nodeId, handle}using the handlesoutput,onPurchase,onDecline,onEnd,onClaim,onComplete,onLevelUp,onPremiumPurchase. - Transient callback failures (network error, 5xx) leave the effect active
for retry;
unknown_run/run_expireddrop that run's effects and fireonScenarioFailed. - 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).