feat: modules and resources domains, drop removed domains, regenerated models

This commit is contained in:
edmand46
2026-09-17 12:08:03 +03:00
parent f45261439f
commit 6f37b00fb4
71 changed files with 516 additions and 4184 deletions
+20 -112
View File
@@ -1,16 +1,12 @@
/**
* 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.
* Composes the kernel domains (auth, player, catalog, storage, modules,
* resources) and the HTTP transport every service uses to reach the API.
*
* 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.
*
* The scenario effects client is loaded lazily on first use — a client that
* never logs in never pays for the scenario machinery.
* domain, so they depend on that narrow surface rather than on the client
* (which keeps the module graph acyclic).
*/
import type {
@@ -22,45 +18,32 @@ 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';
import { ProjectStorageDomain } from '../domains/ProjectStorageDomain.js';
import { StoresDomain } from '../domains/StoresDomain.js';
import { ModulesDomain } from '../domains/ModulesDomain.js';
import { ResourcesDomain, type ResourceHandle } from '../domains/ResourcesDomain.js';
type ManagedDomain = { invalidate(): void; reset(): void };
export class RudderClient<TConfig extends RemoteConfigShape = RemoteConfigShape> {
export class RudderClient {
public readonly options: ResolvedRudderClientOptions;
public readonly auth: AuthService<TConfig>;
public readonly leaderboards: LeaderboardsService;
public readonly battlePass: BattlePassService;
public readonly quests: QuestsService;
public readonly auth: AuthService;
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;
public readonly projectStorage: ProjectStorageDomain;
public readonly stores: StoresDomain;
public readonly modules: ModulesDomain;
private readonly resourcesDomain: ResourcesDomain;
private readonly effectsCenter: EffectsCenter;
private scenarioRuntime: ScenarioService | null = null;
private scenarioRuntimePromise: Promise<ScenarioService> | null = null;
private readonly syncEngine: SyncEngine;
private readonly loginEvent: string | null;
private readonly ctx: RudderContext;
private readonly transportContext: RequestContext;
private runtimeStarted = false;
@@ -78,13 +61,12 @@ export class RudderClient<TConfig extends RemoteConfigShape = RemoteConfigShape>
}
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,
appVersion: this.options.appVersion,
refreshState: {},
onAuthFailure: () => {
this.auth.notifySessionExpired();
@@ -105,29 +87,18 @@ export class RudderClient<TConfig extends RemoteConfigShape = RemoteConfigShape>
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);
this.projectStorage = new ProjectStorageDomain(ctx);
this.stores = new StoresDomain(ctx, () => {
this.player.invalidate();
this.inventory.invalidate();
this.stores.invalidate();
});
this.modules = new ModulesDomain(ctx);
this.resourcesDomain = new ResourcesDomain(ctx);
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,
this.projectStorage,
this.stores,
]);
this.auth = new AuthService<TConfig>(
this.auth = new AuthService(
ctx,
() => this.startRuntimeAfterLogin(),
() => this.stopRuntimeAfterLogout(),
@@ -136,81 +107,33 @@ export class RudderClient<TConfig extends RemoteConfigShape = RemoteConfigShape>
dispose(): void {
this.syncEngine.stop();
this.scenarioRuntime?.clear();
this.resetAll();
this.runtimeStarted = false;
}
resources(slug: string): ResourceHandle {
return this.resourcesDomain.of(slug);
}
/** @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;
await runtime.start();
if (this.loginEvent) {
await this.sendRuntimeEvent(runtime, this.loginEvent);
}
await Promise.all([this.player.load(), this.catalog.load()]);
this.syncEngine.start();
}
private ensureScenarioRuntime(): Promise<ScenarioService> {
this.scenarioRuntimePromise ??= this.createScenarioRuntime();
return this.scenarioRuntimePromise;
}
private async createScenarioRuntime(): Promise<ScenarioService> {
const { ScenarioService } = await import('../scenario/ScenarioService.js');
this.scenarioRuntime = new ScenarioService(
this.ctx,
{
player: this.player,
inventory: this.inventory,
stores: this.stores,
},
this.battlePass,
);
return this.scenarioRuntime;
}
private async sendRuntimeEvent(
runtime: ScenarioService,
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,
this.projectStorage,
this.stores,
];
return [this.player, this.catalog, this.storage, this.projectStorage];
}
private invalidateAll(): void {
@@ -221,18 +144,3 @@ export class RudderClient<TConfig extends RemoteConfigShape = RemoteConfigShape>
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`).
* Lazily loads the scenario effects client on first call.
*
* @internal
*/
export function getScenarioRuntime<TConfig extends RemoteConfigShape = RemoteConfigShape>(
client: RudderClient<TConfig>,
): Promise<ScenarioService> {
return (
client as unknown as { ensureScenarioRuntime(): Promise<ScenarioService> }
).ensureScenarioRuntime();
}
+6 -7
View File
@@ -1,9 +1,5 @@
import type { TokenStore } from '../token/TokenStore.js';
export interface RudderRuntimeOptions {
loginEvent?: string | null;
}
export interface RudderClientOptions {
/** Base URL of the LiveOps API gateway (e.g., "https://api.rudder.build"). */
baseUrl: string;
@@ -18,6 +14,12 @@ export interface RudderClientOptions {
*/
tokenStore?: TokenStore;
/**
* Game build version sent as `appVersion` on every login and token refresh
* (used by segments). Omitted when not set.
*/
appVersion?: string;
/** Per-request timeout in milliseconds (default: 10000). */
requestTimeoutMs?: number;
@@ -29,9 +31,6 @@ export interface RudderClientOptions {
* Defaults to `console.error`.
*/
onEffectError?: (error: unknown) => void;
/** Advanced runtime knobs (login event). */
runtime?: RudderRuntimeOptions;
}
/** Client options with all defaults resolved — what `client.options` exposes. */
+4 -6
View File
@@ -3,15 +3,13 @@
*
* All errors extend `RudderError`. Network errors, HTTP errors, and
* authentication errors have dedicated subclasses for granular handling.
* The machine-readable `code` is the server-provided {@link RudderErrorCode}
* when known, or any other string for unknown servers / SDK-local codes
* The machine-readable `code` is the server-provided code (open-ended: platform
* codes and module business error codes) or an SDK-local code
* (e.g. `sdk/invalid-options` for client configuration errors).
*/
import type { RudderErrorCode } from '../generated/errors.js';
/** Code carried by SDK errors: a known server code, or any other string. */
export type RudderErrorCodeLike = RudderErrorCode | (string & {});
/** Code carried by SDK errors: a server code or an SDK-local code. */
export type RudderErrorCodeLike = string;
/** Code for client-side configuration validation failures. */
export const SDK_ERROR_INVALID_OPTIONS = 'sdk/invalid-options';