2026-08-12 14:02:25 +03:00
|
|
|
/**
|
|
|
|
|
* RudderClient — the main entry point for the LiveOps Web SDK.
|
|
|
|
|
*
|
|
|
|
|
* Composes the observable domains, the scenario runtime, and the core HTTP
|
|
|
|
|
* transport that every service uses to reach the LiveOps API gateway.
|
|
|
|
|
*
|
|
|
|
|
* The client builds a {@link RudderContext} first and injects it into every
|
|
|
|
|
* domain and service, so they depend on that narrow surface rather than on the
|
|
|
|
|
* client (which keeps the module graph acyclic). Cross-domain wiring (the
|
|
|
|
|
* inventory↔catalog link, purchase invalidation) is done here at construction.
|
|
|
|
|
*
|
2026-09-04 14:08:48 +03:00
|
|
|
* The scenario effects client is loaded lazily on first use — a client that
|
|
|
|
|
* never logs in never pays for the scenario machinery.
|
2026-08-12 14:02:25 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
ResolvedRudderClientOptions,
|
|
|
|
|
RudderClientOptions,
|
|
|
|
|
} from './RudderClientOptions.js';
|
|
|
|
|
import { RudderError, SDK_ERROR_INVALID_OPTIONS } from './RudderError.js';
|
|
|
|
|
import type { RudderContext } from '../core/context.js';
|
|
|
|
|
import { request, type RequestContext } from '../transport/request.js';
|
|
|
|
|
import { createDefaultTokenStore } from '../token/TokenStore.js';
|
|
|
|
|
import { AuthService } from '../auth/AuthService.js';
|
|
|
|
|
import { LeaderboardsService } from '../leaderboards/LeaderboardsService.js';
|
|
|
|
|
import { BattlePassService } from '../battlepass/BattlePassService.js';
|
|
|
|
|
import { QuestsService } from '../quests/QuestsService.js';
|
|
|
|
|
import type { ScenarioService } from '../scenario/ScenarioService.js';
|
|
|
|
|
import { EffectsCenter, type Effects } from '../effects/EffectsCenter.js';
|
|
|
|
|
import { SyncEngine } from '../state/SyncEngine.js';
|
|
|
|
|
import type { RemoteConfigShape } from '../state/RemoteConfigState.js';
|
|
|
|
|
import { PlayerDomain } from '../domains/PlayerDomain.js';
|
|
|
|
|
import { CatalogDomain } from '../domains/CatalogDomain.js';
|
|
|
|
|
import { InventoryDomain } from '../domains/InventoryDomain.js';
|
|
|
|
|
import { ConfigDomain } from '../domains/ConfigDomain.js';
|
|
|
|
|
import { StorageDomain } from '../domains/StorageDomain.js';
|
2026-08-12 23:58:30 +03:00
|
|
|
import { ProjectStorageDomain } from '../domains/ProjectStorageDomain.js';
|
2026-08-12 14:02:25 +03:00
|
|
|
import { StoresDomain } from '../domains/StoresDomain.js';
|
|
|
|
|
|
|
|
|
|
type ManagedDomain = { invalidate(): void; reset(): void };
|
|
|
|
|
|
|
|
|
|
export class RudderClient<TConfig extends RemoteConfigShape = RemoteConfigShape> {
|
|
|
|
|
public readonly options: ResolvedRudderClientOptions;
|
|
|
|
|
|
|
|
|
|
public readonly auth: AuthService<TConfig>;
|
|
|
|
|
public readonly leaderboards: LeaderboardsService;
|
|
|
|
|
public readonly battlePass: BattlePassService;
|
|
|
|
|
public readonly quests: QuestsService;
|
|
|
|
|
public readonly effects: Effects;
|
|
|
|
|
|
|
|
|
|
public readonly player: PlayerDomain;
|
|
|
|
|
public readonly catalog: CatalogDomain;
|
|
|
|
|
public readonly inventory: InventoryDomain;
|
|
|
|
|
public readonly remoteConfig: ConfigDomain<TConfig>;
|
|
|
|
|
public readonly storage: StorageDomain;
|
2026-08-12 23:58:30 +03:00
|
|
|
public readonly projectStorage: ProjectStorageDomain;
|
2026-08-12 14:02:25 +03:00
|
|
|
public readonly stores: StoresDomain;
|
|
|
|
|
|
|
|
|
|
private readonly effectsCenter: EffectsCenter;
|
2026-09-04 14:08:48 +03:00
|
|
|
private scenarioRuntime: ScenarioService | null = null;
|
|
|
|
|
private scenarioRuntimePromise: Promise<ScenarioService> | null = null;
|
2026-08-12 14:02:25 +03:00
|
|
|
private readonly syncEngine: SyncEngine;
|
|
|
|
|
private readonly loginEvent: string | null;
|
|
|
|
|
private readonly ctx: RudderContext;
|
|
|
|
|
private readonly transportContext: RequestContext;
|
|
|
|
|
private runtimeStarted = false;
|
|
|
|
|
|
|
|
|
|
constructor(options: RudderClientOptions) {
|
|
|
|
|
if (!options.baseUrl) {
|
|
|
|
|
throw new RudderError('RudderClient: baseUrl is required', {
|
|
|
|
|
code: SDK_ERROR_INVALID_OPTIONS,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (!options.projectKey) {
|
|
|
|
|
throw new RudderError('RudderClient: projectKey is required', {
|
|
|
|
|
code: SDK_ERROR_INVALID_OPTIONS,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.options = { ...options, tokenStore: options.tokenStore ?? createDefaultTokenStore() };
|
|
|
|
|
this.loginEvent =
|
|
|
|
|
options.runtime?.loginEvent === undefined ? 'player_login' : options.runtime.loginEvent;
|
|
|
|
|
|
|
|
|
|
this.transportContext = {
|
|
|
|
|
baseUrl: this.options.baseUrl,
|
|
|
|
|
tokenStore: this.options.tokenStore,
|
|
|
|
|
requestTimeoutMs: this.options.requestTimeoutMs,
|
|
|
|
|
refreshState: {},
|
|
|
|
|
onAuthFailure: () => {
|
|
|
|
|
this.auth.notifySessionExpired();
|
|
|
|
|
this.stopRuntimeAfterLogout();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.effectsCenter = new EffectsCenter(this.options.onEffectError);
|
|
|
|
|
this.effects = this.effectsCenter;
|
|
|
|
|
|
|
|
|
|
this.ctx = {
|
|
|
|
|
options: this.options,
|
|
|
|
|
effects: this.effectsCenter,
|
|
|
|
|
request: <TResponse>(method: string, path: string, body?: unknown) =>
|
|
|
|
|
request<TResponse>(method, path, body, this.transportContext),
|
|
|
|
|
};
|
|
|
|
|
const ctx = this.ctx;
|
|
|
|
|
|
|
|
|
|
this.player = new PlayerDomain(ctx);
|
|
|
|
|
this.catalog = new CatalogDomain(ctx);
|
|
|
|
|
this.inventory = new InventoryDomain(ctx, this.catalog);
|
|
|
|
|
this.remoteConfig = new ConfigDomain<TConfig>(ctx);
|
|
|
|
|
this.storage = new StorageDomain(ctx);
|
2026-08-12 23:58:30 +03:00
|
|
|
this.projectStorage = new ProjectStorageDomain(ctx);
|
2026-08-12 14:02:25 +03:00
|
|
|
this.stores = new StoresDomain(ctx, () => {
|
|
|
|
|
this.player.invalidate();
|
|
|
|
|
this.inventory.invalidate();
|
|
|
|
|
this.stores.invalidate();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.leaderboards = new LeaderboardsService(ctx);
|
|
|
|
|
this.battlePass = new BattlePassService(ctx);
|
|
|
|
|
this.quests = new QuestsService(ctx);
|
|
|
|
|
this.syncEngine = new SyncEngine(ctx, [
|
|
|
|
|
this.player,
|
|
|
|
|
this.catalog,
|
|
|
|
|
this.inventory,
|
|
|
|
|
this.remoteConfig,
|
|
|
|
|
this.storage,
|
2026-08-12 23:58:30 +03:00
|
|
|
this.projectStorage,
|
2026-08-12 14:02:25 +03:00
|
|
|
this.stores,
|
|
|
|
|
]);
|
|
|
|
|
this.auth = new AuthService<TConfig>(
|
|
|
|
|
ctx,
|
|
|
|
|
() => this.startRuntimeAfterLogin(),
|
|
|
|
|
() => this.stopRuntimeAfterLogout(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
this.syncEngine.stop();
|
|
|
|
|
this.scenarioRuntime?.clear();
|
|
|
|
|
this.resetAll();
|
|
|
|
|
this.runtimeStarted = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @internal */
|
|
|
|
|
async startRuntimeAfterLogin(): Promise<void> {
|
|
|
|
|
const runtimePromise = this.ensureScenarioRuntime();
|
|
|
|
|
if (this.runtimeStarted) {
|
|
|
|
|
this.scenarioRuntime?.clear();
|
|
|
|
|
this.invalidateAll();
|
|
|
|
|
}
|
|
|
|
|
this.runtimeStarted = true;
|
|
|
|
|
await Promise.all([
|
|
|
|
|
this.remoteConfig.load(),
|
|
|
|
|
this.player.load(),
|
|
|
|
|
this.stores.load(),
|
|
|
|
|
this.catalog.load(),
|
|
|
|
|
]);
|
|
|
|
|
const runtime = await runtimePromise;
|
2026-09-04 14:08:48 +03:00
|
|
|
await runtime.start();
|
2026-08-12 14:02:25 +03:00
|
|
|
if (this.loginEvent) {
|
|
|
|
|
await this.sendRuntimeEvent(runtime, this.loginEvent);
|
|
|
|
|
}
|
|
|
|
|
this.syncEngine.start();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 14:08:48 +03:00
|
|
|
private ensureScenarioRuntime(): Promise<ScenarioService> {
|
2026-08-12 14:02:25 +03:00
|
|
|
this.scenarioRuntimePromise ??= this.createScenarioRuntime();
|
|
|
|
|
return this.scenarioRuntimePromise;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 14:08:48 +03:00
|
|
|
private async createScenarioRuntime(): Promise<ScenarioService> {
|
2026-08-12 14:02:25 +03:00
|
|
|
const { ScenarioService } = await import('../scenario/ScenarioService.js');
|
2026-09-04 14:08:48 +03:00
|
|
|
this.scenarioRuntime = new ScenarioService(
|
2026-08-12 14:02:25 +03:00
|
|
|
this.ctx,
|
|
|
|
|
{
|
|
|
|
|
player: this.player,
|
|
|
|
|
inventory: this.inventory,
|
|
|
|
|
stores: this.stores,
|
|
|
|
|
},
|
|
|
|
|
this.battlePass,
|
|
|
|
|
);
|
|
|
|
|
return this.scenarioRuntime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async sendRuntimeEvent(
|
2026-09-04 14:08:48 +03:00
|
|
|
runtime: ScenarioService,
|
2026-08-12 14:02:25 +03:00
|
|
|
event: string,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await runtime.send(event);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn(`RudderClient: scenario event "${event}" failed after login`, error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @internal */
|
|
|
|
|
stopRuntimeAfterLogout(): void {
|
|
|
|
|
this.runtimeStarted = false;
|
|
|
|
|
this.syncEngine.stop();
|
|
|
|
|
this.scenarioRuntime?.clear();
|
|
|
|
|
this.resetAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private get managedDomains(): ManagedDomain[] {
|
|
|
|
|
return [
|
|
|
|
|
this.player,
|
|
|
|
|
this.catalog,
|
|
|
|
|
this.inventory,
|
|
|
|
|
this.remoteConfig,
|
|
|
|
|
this.storage,
|
2026-08-12 23:58:30 +03:00
|
|
|
this.projectStorage,
|
2026-08-12 14:02:25 +03:00
|
|
|
this.stores,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private invalidateAll(): void {
|
|
|
|
|
for (const domain of this.managedDomains) domain.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private resetAll(): void {
|
|
|
|
|
for (const domain of this.managedDomains) domain.reset();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Test/internal access to the scenario runtime, which is deliberately not part
|
|
|
|
|
* of the public client surface (it is driven through `client.effects`).
|
2026-09-04 14:08:48 +03:00
|
|
|
* Lazily loads the scenario effects client on first call.
|
2026-08-12 14:02:25 +03:00
|
|
|
*
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
export function getScenarioRuntime<TConfig extends RemoteConfigShape = RemoteConfigShape>(
|
|
|
|
|
client: RudderClient<TConfig>,
|
2026-09-04 14:08:48 +03:00
|
|
|
): Promise<ScenarioService> {
|
2026-08-12 14:02:25 +03:00
|
|
|
return (
|
2026-09-04 14:08:48 +03:00
|
|
|
client as unknown as { ensureScenarioRuntime(): Promise<ScenarioService> }
|
2026-08-12 14:02:25 +03:00
|
|
|
).ensureScenarioRuntime();
|
|
|
|
|
}
|