From 34c53bbb6817c2858cafef7b90d88cafaf51ec70 Mon Sep 17 00:00:00 2001 From: edmand46 Date: Thu, 17 Sep 2026 14:15:37 +0300 Subject: [PATCH] chore: remove effects leftovers, dead SyncedState.set, add modules/resources skill refs --- CHANGELOG.md | 4 + README.md | 17 +-- package.json | 2 +- skills/rudder-web-sdk/SKILL.md | 18 ++-- skills/rudder-web-sdk/reference/modules.md | 31 ++++++ skills/rudder-web-sdk/reference/resources.md | 29 ++++++ src/client/RudderClient.ts | 7 -- src/client/RudderClientOptions.ts | 6 -- src/core/context.ts | 4 +- src/effects/EffectsCenter.ts | 103 ------------------- src/index.ts | 10 -- src/state/SyncedState.ts | 16 +-- test/PublicApi.test.ts | 7 +- test/e2e-prod/_setup/harness.ts | 43 +------- 14 files changed, 89 insertions(+), 208 deletions(-) create mode 100644 skills/rudder-web-sdk/reference/modules.md create mode 100644 skills/rudder-web-sdk/reference/resources.md delete mode 100644 src/effects/EffectsCenter.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e16262e..6956c83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Removed `client.effects`, the `Effects` / `EffectUnsubscribe` / + `NotificationEffect` / `WaitEffect` / `ScenarioCompletedEffect` / + `ScenarioFailedEffect` types, and the `onEffectError` client option. Nothing + emitted effects since scenarios were removed. - `client.modules.call` resolves to `CallModuleFunctionClientResponse`, which carries only `result`. Console output, the kernel call log and `error` are no longer part of the player response; they stay on the platform dry run. diff --git a/README.md b/README.md index 7840ee7..1ecaa8e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ const profile = client.player.data; const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance; const items = await client.resources('items').list(); -const result = await client.modules.call('quests', 'list'); +const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' }); client.dispose(); ``` @@ -47,7 +47,7 @@ client.dispose(); | Area | Access | |---|---| -| Auth | `client.auth.loginWithDevice()`, `client.auth.loginWithCustom({ customData })`, `client.auth.logout()`, `client.auth.isAuthenticated`, `client.auth.onAuthStateChange(cb)` | +| Auth | `client.auth.loginWithDevice()`, `loginWithGoogle({ idToken })`, `loginWithApple({ idToken })`, `loginWithCustom({ customData })`, `linkWithGoogle(idToken)`, `linkWithApple(idToken)`, `linkWithCustom({ customData })`, `unlinkIdentity(provider)`, `logout()`, `isAuthenticated`, `onAuthStateChange(cb)` | | Player / wallets | `client.player` (observable) | | Catalog currencies | `client.catalog` (observable) | | Storage | `client.storage` (observable) + `.save(items)` / `.delete(type)` | @@ -55,9 +55,10 @@ client.dispose(); | Modules | `client.modules.call(module, fn, args?)` | | Resources | `client.resources(slug)` → `list` / `get` / `create` / `update` / `delete` | -Game-design features (quests, stores, inventory, leaderboards, battle pass, -remote config) live in installed modules. Call them through -`client.modules.call` and read their rows through `client.resources(slug)`. +Game features are installed modules, not SDK services. The first-party +modules are items, inventory, reward, remote_config, leaderboards, quests, +store and battlepass. Call a module through `client.modules.call` and read its +rows through `client.resources(slug)`. ## Error handling @@ -108,7 +109,7 @@ client.auth.onAuthStateChange((state) => { Observable domains (`player`, `catalog`, `storage`, `projectStorage`) are warmed at login (`player` and `catalog`) or on first use, then kept fresh by -a revision poll: the client asks the gateway for entity revisions every +a revision poll: the client asks the backend for entity revisions every **30 s (±20% jitter)** and reloads only the entities whose revision grew. Polling **pauses while the tab is hidden** and resumes on `visibilitychange`. Storage writes invalidate the affected domain immediately. Expect data to be @@ -142,8 +143,8 @@ function Wallet() { npm run typecheck # tsc over src and tests npm test # unit + local e2e (jsdom) npm run build # tsup → dist (ESM + CJS + d.ts) -npm run test:e2e-prod # drives a seeded project on the live gateway +npm run test:e2e-prod # drives a seeded project on the live backend ``` -Generated wire types (`src/generated`) come from the gateway's `apigen`; run +Generated wire types (`src/generated`) come from `liveops-backend/apigen`; run `npm run generate` to refresh them. Do not edit them by hand. diff --git a/package.json b/package.json index f5cb4d1..0eb3900 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "e2e:prod": "vitest run --config vitest.e2e-prod.config.ts", "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "check:package": "npm run build && npm pack --dry-run --json", - "generate": "make -C ../liveops-gateway generate-openapi" + "generate": "make -C ../liveops-backend generate-openapi" }, "devDependencies": { "@types/node": "^26.1.1", diff --git a/skills/rudder-web-sdk/SKILL.md b/skills/rudder-web-sdk/SKILL.md index 1583cd2..e3e053d 100644 --- a/skills/rudder-web-sdk/SKILL.md +++ b/skills/rudder-web-sdk/SKILL.md @@ -6,8 +6,8 @@ description: Use when working with the Rudder TypeScript/JavaScript SDK (@rudder # Rudder Web SDK (@rudder/js-sdk) Browser-facing player SDK for the Rudder LiveOps platform. Source of truth: -`liveops-web-sdk/src` (generated wire types in `src/generated` come from the -gateway's apigen — never edit them by hand). +`liveops-web-sdk/src` (generated wire types in `src/generated` come from +`liveops-backend/apigen` — never edit them by hand). 2.0 kernel surface: auth, player (wallets), catalog currencies, storage, project storage, revision sync, `client.modules.call(module, fn, args)`, @@ -40,7 +40,6 @@ const client = new RudderClient({ // tokenStore?: TokenStore — default: localStorage, in-memory fallback // requestTimeoutMs?: number — default 10000 // syncIntervalMs?: number — revision poll, default 30000 (±20% jitter) - // onEffectError?: (error) => void — default console.error }); ``` @@ -86,11 +85,12 @@ Tokens are saved to the `TokenStore` automatically. The transport injects | Modules | `client.modules.call(module, fn, args?)` | `CallModuleFunctionClientResponse` (`{ result }`) | | Resources | `client.resources(slug)` | `list` / `get` / `create` / `update` / `delete` | -There is no UGC, wallet service, or standalone economy service — wallets live -on `client.player.data.wallets`. Quests, stores, inventory, leaderboards, -battle pass, and remote config are installed modules: call -`client.modules.call('quests', 'list')` (etc.) and read rows with -`client.resources('items')`. +Wallets live on `client.player.data.wallets`. Game features are installed +modules, not SDK services (first-party modules: items, inventory, reward, +remote_config, leaderboards, quests, store, battlepass). Call them with +`client.modules.call('my_module', 'hello', { ... })` and read their rows with +`client.resources(slug)`. Details: [reference/modules.md](reference/modules.md), +[reference/resources.md](reference/resources.md). ## Observable domains — the shared pattern @@ -137,3 +137,5 @@ documents. A module trap is a 500 and a timeout a 504, both without details. - [reference/auth.md](reference/auth.md) — login flows, options, token store, session lifecycle - [reference/player.md](reference/player.md) — profile + wallets (`PlayerProfile`, `Wallet`) - [reference/storage.md](reference/storage.md) — player + project storage, limits +- [reference/modules.md](reference/modules.md) — `modules.call`, response, module errors +- [reference/resources.md](reference/resources.md) — `resources(slug)` row operations diff --git a/skills/rudder-web-sdk/reference/modules.md b/skills/rudder-web-sdk/reference/modules.md new file mode 100644 index 0000000..1e9da57 --- /dev/null +++ b/skills/rudder-web-sdk/reference/modules.md @@ -0,0 +1,31 @@ +# Modules + +Source: `src/domains/ModulesDomain.ts`. Calls an installed module function as +the signed-in player through `POST /sdk/v1/modules/{module}/{fn}`. + +```ts +client.modules.call(module: string, fn: string, args?: unknown): Promise +// CallModuleFunctionClientResponse = { result?: unknown } +``` + +```ts +const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' }); +``` + +`args` is sent as `{ args }`. The response carries only `result`; console +output and the kernel call log stay on the platform dry run. + +A business error raised by the module rejects with `RudderHttpError` +(`status` 422, body `{ error, code, requestId }`) whose `code` is the +module's own open string code; there are no generated constants for it. A +trap is a 500 and a timeout a 504, both without details. + +```ts +try { + await client.modules.call('my_module', 'hello'); +} catch (error) { + if (error instanceof RudderHttpError && error.status === 422) { + console.warn(error.code); // module-defined + } +} +``` diff --git a/skills/rudder-web-sdk/reference/resources.md b/skills/rudder-web-sdk/reference/resources.md new file mode 100644 index 0000000..a76b5d2 --- /dev/null +++ b/skills/rudder-web-sdk/reference/resources.md @@ -0,0 +1,29 @@ +# Resources + +Source: `src/domains/ResourcesDomain.ts`. Row operations on a resource as the +signed-in player through `/sdk/v1/resources/{slug}`. `client.resources(slug)` +returns a `ResourceHandle`; it performs no request by itself. + +```ts +const items = client.resources('items'); +items.list(): Promise // GET /{slug} → { items?: ResourceRow[], total?: number } +items.get(id): Promise // GET /{slug}/{id} +items.create({ id?, data? }): Promise // POST /{slug} +items.update(id, { data?, version? }): Promise // PATCH /{slug}/{id} +items.delete(id): Promise // DELETE /{slug}/{id} +``` + +`ResourceRow` is `{ id, playerId, data, version, rank, createdAt, updatedAt }` +(all optional in the generated type). Pass `version` on `update` for +optimistic concurrency. + +```ts +const row = await client.resources('items').create({ data: { name: 'Sword' } }); +await client.resources('items').update(row.id!, { data: { name: 'Axe' }, version: row.version }); +``` + +What a player may read or write is decided by the resource's access rules on +the server; a denied operation rejects with `RudderHttpError` `code` +`forbidden`. Deleting a row that other rows still reference rejects with +status 409 and `code` `row_referenced`. Resources are not observable domains: +call `list()` again when you need fresh rows. diff --git a/src/client/RudderClient.ts b/src/client/RudderClient.ts index 2ff329e..a92d7a7 100644 --- a/src/client/RudderClient.ts +++ b/src/client/RudderClient.ts @@ -18,7 +18,6 @@ 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 { EffectsCenter, type Effects } from '../effects/EffectsCenter.js'; import { SyncEngine } from '../state/SyncEngine.js'; import { PlayerDomain } from '../domains/PlayerDomain.js'; import { CatalogDomain } from '../domains/CatalogDomain.js'; @@ -33,7 +32,6 @@ export class RudderClient { public readonly options: ResolvedRudderClientOptions; public readonly auth: AuthService; - public readonly effects: Effects; public readonly player: PlayerDomain; public readonly catalog: CatalogDomain; @@ -42,7 +40,6 @@ export class RudderClient { public readonly modules: ModulesDomain; private readonly resourcesDomain: ResourcesDomain; - private readonly effectsCenter: EffectsCenter; private readonly syncEngine: SyncEngine; private readonly ctx: RudderContext; private readonly transportContext: RequestContext; @@ -74,12 +71,8 @@ export class RudderClient { }, }; - this.effectsCenter = new EffectsCenter(this.options.onEffectError); - this.effects = this.effectsCenter; - this.ctx = { options: this.options, - effects: this.effectsCenter, request: (method: string, path: string, body?: unknown) => request(method, path, body, this.transportContext), }; diff --git a/src/client/RudderClientOptions.ts b/src/client/RudderClientOptions.ts index 835eae6..8ad7278 100644 --- a/src/client/RudderClientOptions.ts +++ b/src/client/RudderClientOptions.ts @@ -25,12 +25,6 @@ export interface RudderClientOptions { /** Revision sync poll interval in milliseconds (default: 30000, ±20% jitter). */ syncIntervalMs?: number; - - /** - * Called when an effect handler (or effect preparation) throws. - * Defaults to `console.error`. - */ - onEffectError?: (error: unknown) => void; } /** Client options with all defaults resolved — what `client.options` exposes. */ diff --git a/src/core/context.ts b/src/core/context.ts index 4eb014b..218f708 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -1,9 +1,8 @@ import type { ResolvedRudderClientOptions } from '../client/RudderClientOptions.js'; -import type { EffectsCenter } from '../effects/EffectsCenter.js'; /** * RudderContext — the low-level surface every feature service depends on: - * the HTTP transport, client options, and the effects bus. + * the HTTP transport and client options. * * Built once by {@link RudderClient} and injected into each service. Services * depend on this narrow interface instead of on the client itself, so the @@ -11,6 +10,5 @@ import type { EffectsCenter } from '../effects/EffectsCenter.js'; */ export interface RudderContext { readonly options: ResolvedRudderClientOptions; - readonly effects: EffectsCenter; request(method: string, path: string, body?: unknown): Promise; } diff --git a/src/effects/EffectsCenter.ts b/src/effects/EffectsCenter.ts deleted file mode 100644 index 29f4b8f..0000000 --- a/src/effects/EffectsCenter.ts +++ /dev/null @@ -1,103 +0,0 @@ -export type EffectUnsubscribe = () => void; - -export interface NotificationEffect { - readonly title: string; - readonly message: string; - done(): Promise; -} - -export interface WaitEffect { - readonly deadlineUtc: Date; -} - -export interface ScenarioCompletedEffect { - readonly runId: string; - readonly scenarioSlug: string; -} - -export interface ScenarioFailedEffect { - readonly runId: string; - readonly scenarioSlug: string; - readonly nodeId: string; - readonly error: Error; -} - -type EffectHandler = (effect: TEffect) => void | Promise; - -export interface Effects { - onNotification(handler: EffectHandler): EffectUnsubscribe; - onWait(handler: EffectHandler): EffectUnsubscribe; - onScenarioCompleted(handler: EffectHandler): EffectUnsubscribe; - onScenarioFailed(handler: EffectHandler): EffectUnsubscribe; -} - -export class EffectsCenter implements Effects { - private readonly notificationHandlers = new Set>(); - private readonly waitHandlers = new Set>(); - private readonly scenarioCompletedHandlers = new Set>(); - private readonly scenarioFailedHandlers = new Set>(); - - constructor( - private readonly onError: (error: unknown) => void = (error) => - console.error('[Rudder] Effect handler failed', error), - ) {} - - onNotification(handler: EffectHandler): EffectUnsubscribe { - return this.addHandler(this.notificationHandlers, handler); - } - - onWait(handler: EffectHandler): EffectUnsubscribe { - return this.addHandler(this.waitHandlers, handler); - } - - onScenarioCompleted(handler: EffectHandler): EffectUnsubscribe { - return this.addHandler(this.scenarioCompletedHandlers, handler); - } - - onScenarioFailed(handler: EffectHandler): EffectUnsubscribe { - return this.addHandler(this.scenarioFailedHandlers, handler); - } - - /** @internal */ - emitNotification(effect: NotificationEffect): void { - this.emit(this.notificationHandlers, effect); - } - - /** @internal */ - emitWait(effect: WaitEffect): void { - this.emit(this.waitHandlers, effect); - } - - /** @internal */ - emitScenarioCompleted(effect: ScenarioCompletedEffect): void { - this.emit(this.scenarioCompletedHandlers, effect); - } - - /** @internal */ - emitScenarioFailed(effect: ScenarioFailedEffect): void { - this.emit(this.scenarioFailedHandlers, effect); - } - - private addHandler( - handlers: Set>, - handler: EffectHandler, - ): EffectUnsubscribe { - handlers.add(handler); - return () => { - handlers.delete(handler); - }; - } - - private emit( - handlers: Set>, - effect: TEffect, - ): void { - for (const handler of handlers) { - try { - Promise.resolve(handler(effect)).catch((error) => this.onError(error)); - } catch (error) { - this.onError(error); - } - } - } -} diff --git a/src/index.ts b/src/index.ts index daeff0d..e546fd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,16 +45,6 @@ export type { ResourcesDomain, ResourceHandle } from './domains/ResourcesDomain. export type { StorageDomain } from './domains/StorageDomain.js'; export type { ProjectStorageDomain } from './domains/ProjectStorageDomain.js'; -// Effects -export type { - Effects, - EffectUnsubscribe, - NotificationEffect, - WaitEffect, - ScenarioCompletedEffect, - ScenarioFailedEffect, -} from './effects/EffectsCenter.js'; - // Generated wire types — the shapes returned by the observable state and // handles, so consumers can name them instead of re-declaring mirrors. export type { Player, PlayerProfile, PlayerProfileIdentity, Wallet } from './generated/player.js'; diff --git a/src/state/SyncedState.ts b/src/state/SyncedState.ts index 54e39fc..dabadac 100644 --- a/src/state/SyncedState.ts +++ b/src/state/SyncedState.ts @@ -76,8 +76,8 @@ export class SyncedState { if (this.listeners.size === 0 && this.snapshot.status === 'idle') return; this.invalidateScheduled = true; queueMicrotask(() => { - // A reset() or set() between scheduling and flushing cancels the reload - // (fresh data arrived, or the entity was dropped on logout). + // A reset() between scheduling and flushing cancels the reload + // (the entity was dropped on logout). if (!this.invalidateScheduled) return; this.invalidateScheduled = false; if (this.listeners.size === 0 && this.snapshot.status === 'idle') return; @@ -85,18 +85,6 @@ export class SyncedState { }); } - /** - * Applies externally-provided data (e.g. scenario config patches). - * - * @internal - */ - set(data: T): void { - this.generation++; - this.pending = undefined; - this.invalidateScheduled = false; - this.setSnapshot({ status: 'ready', data }); - } - /** * Drops all state back to idle (logout). * diff --git a/test/PublicApi.test.ts b/test/PublicApi.test.ts index 7d20d8f..05617e7 100644 --- a/test/PublicApi.test.ts +++ b/test/PublicApi.test.ts @@ -71,7 +71,6 @@ describe('public API surface', () => { expect(client.modules).toBeDefined(); expect(typeof client.modules.call).toBe('function'); expect(typeof client.resources).toBe('function'); - expect(client.effects).toBeDefined(); expect('battlePass' in client).toBe(false); expect('quests' in client).toBe(false); @@ -80,10 +79,6 @@ describe('public API surface', () => { expect('inventory' in client).toBe(false); expect('remoteConfig' in client).toBe(false); expect('scenarios' in client).toBe(false); - expect('onBattlePass' in client.effects).toBe(false); - expect('onStoreOffer' in client.effects).toBe(false); - expect('onQuest' in client.effects).toBe(false); - expect('onLeaderboard' in client.effects).toBe(false); - expect('onConfigChanged' in client.effects).toBe(false); + expect('effects' in client).toBe(false); }); }); diff --git a/test/e2e-prod/_setup/harness.ts b/test/e2e-prod/_setup/harness.ts index f377e07..1e3ba4a 100644 --- a/test/e2e-prod/_setup/harness.ts +++ b/test/e2e-prod/_setup/harness.ts @@ -1,9 +1,7 @@ -// Test-side helpers: load the seed artifact, build SDK clients against prod, and -// reach the admin API for wallet grants / player inspection. +// Test-side helpers: load the seed artifact and build SDK clients against prod. import { RudderClient } from '../../../src/client/RudderClient.js'; import type { TokenStore } from '../../../src/token/TokenStore.js'; -import { createAdminClient, type AdminClient } from './adminClient.js'; import { readArtifact } from './artifact.js'; import type { SeedArtifact } from './types.js'; @@ -58,42 +56,3 @@ export async function freshPlayer( await client.auth.loginWithDevice({ region: 'en', language: 'en' }); return client; } - -/** Admin (operator) API client authenticated with the seeded operator token. */ -export function adminApi(artifact: SeedArtifact = loadArtifact()): AdminClient { - const api = createAdminClient(artifact.baseUrl); - api.setToken(artifact.adminToken); - return api; -} - -/** Credits a player's wallet through the admin batch endpoint. */ -export async function grantCurrency( - artifact: SeedArtifact, - playerId: string, - currencyCode: string, - amount: number, -): Promise { - await adminApi(artifact).post( - `/platform/v1/projects/${artifact.projectId}/players/wallet/adjust?environment=${artifact.environment}`, - { items: [{ playerId, currencyCode, amount, reason: 'e2e grant' }] }, - ); -} - -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -/** Retries a read until ok(result) holds — absorbs release/cache reload lag. */ -export async function withReadRetry( - fn: () => Promise, - ok: (value: T) => boolean, - tries = 8, - gapMs = 1500, -): Promise { - let last = await fn(); - if (ok(last)) return last; - for (let i = 1; i < tries; i++) { - await sleep(gapMs); - last = await fn(); - if (ok(last)) return last; - } - return last; -}