From 4a167df587620f849b9e1ab995efa0eef7ebf73f Mon Sep 17 00:00:00 2001 From: edmand46 Date: Tue, 29 Sep 2026 16:43:15 +0300 Subject: [PATCH] chore: drop comments that restate the code --- src/auth/AuthService.ts | 79 ----------------------------- src/client/RudderClient.ts | 14 ----- src/client/RudderClientOptions.ts | 14 ----- src/client/RudderError.ts | 20 -------- src/core/context.ts | 8 --- src/core/uuid.ts | 1 - src/device/DeviceId.ts | 12 ----- src/domains/CatalogDomain.ts | 4 -- src/domains/ModulesDomain.ts | 11 ---- src/domains/PlayerDomain.ts | 5 -- src/index.ts | 10 ---- src/state/SyncEngine.ts | 17 ------- src/state/SyncedState.ts | 31 +---------- src/token/TokenStore.ts | 24 --------- src/transport/request.ts | 57 +++++---------------- src/transport/url.ts | 23 --------- test/e2e-prod/_setup/adminClient.ts | 8 +-- test/e2e-prod/_setup/globalSetup.ts | 9 ---- test/e2e-prod/_setup/harness.ts | 5 -- test/e2e-prod/_setup/seed.ts | 4 -- test/e2e-prod/_setup/types.ts | 4 -- test/e2e-prod/health.e2e.test.ts | 1 - test/e2e/modules.e2e.test.ts | 3 -- test/helpers/FakeTokenStore.ts | 1 - test/helpers/createClient.ts | 1 - 25 files changed, 16 insertions(+), 350 deletions(-) delete mode 100644 src/transport/url.ts diff --git a/src/auth/AuthService.ts b/src/auth/AuthService.ts index 4e5ede0..c4347e8 100644 --- a/src/auth/AuthService.ts +++ b/src/auth/AuthService.ts @@ -1,11 +1,3 @@ -/** - * AuthService — player authentication. - * - * Handles login via device ID (the primary auth flow for game clients), Google, - * Apple, or a custom backend webhook, identity linking/unlinking, logout - * (token clearing), and auth state observation. - */ - import type { RudderContext } from '../core/context.js'; import { getOrCreateDeviceId } from '../device/DeviceId.js'; import { api } from '../generated/api.js'; @@ -21,53 +13,35 @@ export type AuthState = 'signed-in' | 'signed-out'; export type AuthStateListener = (state: AuthState) => void; export interface LoginWithDeviceOptions { - /** Player's region code (default: "global"). */ region?: string; - /** Player's language code (default: "en"). */ language?: string; - /** Optional player nickname; omitted from the request when not set. */ nickname?: string; } export interface LoginWithCustomOptions { - /** Arbitrary payload forwarded to the developer's custom auth webhook. */ customData: Record; - /** Name of the custom auth provider; omitted means the default provider. */ provider?: string; - /** Player's region code (default: "global"). */ region?: string; - /** Player's language code (default: "en"). */ language?: string; - /** Optional player nickname; omitted from the request when not set. */ nickname?: string; } export interface LoginWithGoogleOptions { - /** Google ID token obtained from the Google sign-in flow. */ idToken: string; - /** Player's region code (default: "global"). */ region?: string; - /** Player's language code (default: "en"). */ language?: string; - /** Optional player nickname; omitted from the request when not set. */ nickname?: string; } export interface LoginWithAppleOptions { - /** Apple ID token (identity token) obtained from Sign in with Apple. */ idToken: string; - /** Player's region code (default: "global"). */ region?: string; - /** Player's language code (default: "en"). */ language?: string; - /** Optional player nickname; omitted from the request when not set. */ nickname?: string; } export interface LinkWithCustomOptions { - /** Arbitrary payload forwarded to the developer's custom auth webhook. */ customData: Record; - /** Name of the custom auth provider; omitted means the default provider. */ provider?: string; } @@ -83,19 +57,12 @@ export class AuthService { this.state = this.isAuthenticated ? 'signed-in' : 'signed-out'; } - /** True while an access token is present in the token store. */ get isAuthenticated(): boolean { return this.ctx.options.tokenStore.getAccessToken() !== null; } - /** - * Subscribes to auth state changes. The listener fires immediately with the - * current state (like `SyncedState.onChange`). Returns an unsubscribe function. - */ onAuthStateChange(listener: AuthStateListener): () => void { this.listeners.add(listener); - // Resync with the token store — tokens may have been written externally - // since construction. this.state = this.isAuthenticated ? 'signed-in' : 'signed-out'; listener(this.state); return () => { @@ -103,12 +70,6 @@ export class AuthService { }; } - /** - * Authenticates the current device and returns access + refresh tokens. - * - * The device ID is auto-generated on first call and persisted in localStorage. - * On success, tokens are saved to the client's TokenStore. - */ async loginWithDevice(options: LoginWithDeviceOptions = {}): Promise { const { region = 'global', language = 'en', nickname } = options; const response = await api.loginViaDevice(this.ctx, { @@ -130,13 +91,6 @@ export class AuthService { return response; } - /** - * Authenticates via the project's custom authorization webhook and returns - * access + refresh tokens. - * - * The payload is forwarded to the developer's backend; on success, tokens are - * saved to the client's TokenStore. - */ async loginWithCustom(options: LoginWithCustomOptions): Promise { const { customData, provider, region = 'global', language = 'en', nickname } = options; const response = await api.loginViaCustom(this.ctx, { @@ -159,11 +113,6 @@ export class AuthService { return response; } - /** - * Authenticates with a Google ID token and returns access + refresh tokens. - * - * On success, tokens are saved to the client's TokenStore. - */ async loginWithGoogle(options: LoginWithGoogleOptions): Promise { const { idToken, region = 'global', language = 'en', nickname } = options; const response = await api.loginViaGoogle(this.ctx, { @@ -185,11 +134,6 @@ export class AuthService { return response; } - /** - * Authenticates with an Apple ID token and returns access + refresh tokens. - * - * On success, tokens are saved to the client's TokenStore. - */ async loginWithApple(options: LoginWithAppleOptions): Promise { const { idToken, region = 'global', language = 'en', nickname } = options; const response = await api.loginViaApple(this.ctx, { @@ -211,26 +155,14 @@ export class AuthService { return response; } - /** - * Links a Google identity to the currently signed-in player. Requires an - * active session (the access token is sent automatically). - */ async linkWithGoogle(idToken: string): Promise { await api.linkViaGoogle(this.ctx, { idToken }); } - /** - * Links an Apple identity to the currently signed-in player. Requires an - * active session (the access token is sent automatically). - */ async linkWithApple(idToken: string): Promise { await api.linkViaApple(this.ctx, { idToken }); } - /** - * Links a custom identity to the currently signed-in player via the project's - * custom authorization webhook. Requires an active session. - */ async linkWithCustom(options: LinkWithCustomOptions): Promise { await api.linkViaCustom(this.ctx, { customData: options.customData, @@ -238,27 +170,16 @@ export class AuthService { }); } - /** - * Unlinks an identity from the currently signed-in player. The provider is - * the full provider string: `device`, `google`, `apple`, `custom`, or - * `custom:`. - */ async unlinkIdentity(provider: string): Promise { await api.unlinkIdentity(this.ctx, { provider }); } - /** Clears all stored tokens (logout). */ logout(): void { this.ctx.options.tokenStore.clear(); this.stopRuntime(); this.setState('signed-out'); } - /** - * Called by the transport when the session is unrecoverable (refresh failed). - * - * @internal - */ notifySessionExpired(): void { this.setState('signed-out'); } diff --git a/src/client/RudderClient.ts b/src/client/RudderClient.ts index 4408ee1..ba9eedf 100644 --- a/src/client/RudderClient.ts +++ b/src/client/RudderClient.ts @@ -1,14 +1,3 @@ -/** - * RudderClient — the main entry point of `@rudder/sdk`; create it with - * {@link createClient}. - * - * Composes the kernel domains (auth, player, catalog, storage, modules) and the HTTP transport every service uses to reach the API. - * - * The client builds a {@link RudderContext} first and injects it into every - * domain, so they depend on that narrow surface rather than on the client - * (which keeps the module graph acyclic). - */ - import type { ResolvedRudderClientOptions, RudderClientOptions, @@ -102,7 +91,6 @@ export class RudderClient { this.runtimeStarted = false; } - /** @internal */ async startRuntimeAfterLogin(): Promise { if (this.runtimeStarted) { this.invalidateAll(); @@ -112,7 +100,6 @@ export class RudderClient { this.syncEngine.start(); } - /** @internal */ stopRuntimeAfterLogout(): void { this.runtimeStarted = false; this.syncEngine.stop(); @@ -132,7 +119,6 @@ export class RudderClient { } } -/** Creates a client. Only `projectKey` is required. */ export function createClient(options: RudderClientOptions): RudderClient { return new RudderClient(options); } diff --git a/src/client/RudderClientOptions.ts b/src/client/RudderClientOptions.ts index 45dda67..4a51051 100644 --- a/src/client/RudderClientOptions.ts +++ b/src/client/RudderClientOptions.ts @@ -1,33 +1,19 @@ import type { TokenStore } from '../token/TokenStore.js'; export interface RudderClientOptions { - /** Project SDK key; it also selects the environment (staging or prod). */ projectKey: string; - /** - * Token store for persisting and retrieving authentication tokens. - * Defaults to localStorage with a silent in-memory fallback where - * localStorage is unavailable (SSR, private mode). - */ 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; - /** API base URL (default: "https://api.rudder.build"). */ baseUrl?: string; - /** Revision sync poll interval in milliseconds (default: 30000, ±20% jitter). */ syncIntervalMs?: number; } -/** Client options with all defaults resolved — what `client.options` exposes. */ export interface ResolvedRudderClientOptions extends RudderClientOptions { baseUrl: string; tokenStore: TokenStore; diff --git a/src/client/RudderError.ts b/src/client/RudderError.ts index cb8e204..cb44dc9 100644 --- a/src/client/RudderError.ts +++ b/src/client/RudderError.ts @@ -1,20 +1,7 @@ -/** - * Typed error hierarchy for the LiveOps Web SDK. - * - * 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 code (open-ended: platform - * codes and module business error codes) or an SDK-local code - * (e.g. `sdk/invalid-options` for client configuration errors). - */ - -/** 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'; -/** Base error class for all SDK errors. */ export class RudderError extends Error { public readonly code?: RudderErrorCodeLike; @@ -25,7 +12,6 @@ export class RudderError extends Error { } } -/** Thrown when a network request fails (fetch throws, timeout, DNS, etc.). */ export class RudderNetworkError extends RudderError { constructor(cause: unknown) { super(`Network error: ${cause instanceof Error ? cause.message : String(cause)}`, { cause }); @@ -33,7 +19,6 @@ export class RudderNetworkError extends RudderError { } } -/** Thrown when the server returns a non-2xx HTTP status (except 401). */ export class RudderHttpError extends RudderError { constructor( public readonly status: number, @@ -46,7 +31,6 @@ export class RudderHttpError extends RudderError { } } -/** Thrown when the server returns HTTP 401 Unauthorized. */ export class RudderAuthError extends RudderHttpError { constructor(statusText: string, body: string, code?: RudderErrorCodeLike) { super(401, statusText, body, code); @@ -54,10 +38,6 @@ export class RudderAuthError extends RudderHttpError { } } -/** - * Thrown by `modules.call` when a module function fails with HTTP 422 - * `{error, code, requestId}`. `C` narrows `code` to a generated error union. - */ export class RudderModuleError extends RudderHttpError { declare public readonly code: C; public readonly requestId?: string; diff --git a/src/core/context.ts b/src/core/context.ts index 2846432..530465c 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -1,14 +1,6 @@ import type { ResolvedRudderClientOptions } from '../client/RudderClientOptions.js'; import type { RequestOptions } from '../transport/request.js'; -/** - * RudderContext — the low-level surface every feature service depends on: - * 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 - * module graph stays acyclic (client → services → context, never back). - */ export interface RudderContext { readonly options: ResolvedRudderClientOptions; request( diff --git a/src/core/uuid.ts b/src/core/uuid.ts index e78b356..d2ddfcb 100644 --- a/src/core/uuid.ts +++ b/src/core/uuid.ts @@ -1,4 +1,3 @@ -/** UUID v4. Falls back to getRandomValues where randomUUID is missing (non-secure contexts, old runtimes). */ export function uuidv4(): string { if (typeof crypto.randomUUID === 'function') return crypto.randomUUID(); const b = crypto.getRandomValues(new Uint8Array(16)); diff --git a/src/device/DeviceId.ts b/src/device/DeviceId.ts index f31b7a0..11f5434 100644 --- a/src/device/DeviceId.ts +++ b/src/device/DeviceId.ts @@ -1,30 +1,18 @@ -/** - * Device ID provider — generates and persists a unique device identifier. - * - * Generates a UUID v4 and stores the generated UUID in localStorage under `rudder_device_id`. - */ - import { uuidv4 } from '../core/uuid.js'; const STORAGE_KEY = 'rudder_device_id'; -/** - * Returns the stored device ID or creates a new one if none exists. - * The device ID is persisted in localStorage and survives page reloads. - */ export function getOrCreateDeviceId(): string { try { const existing = localStorage.getItem(STORAGE_KEY); if (existing) return existing; } catch { - // localStorage unavailable — generate ephemeral ID. } const id = uuidv4(); try { localStorage.setItem(STORAGE_KEY, id); } catch { - // Storage unavailable — ID will be ephemeral for this session. } return id; } diff --git a/src/domains/CatalogDomain.ts b/src/domains/CatalogDomain.ts index 7c8a90c..b401abd 100644 --- a/src/domains/CatalogDomain.ts +++ b/src/domains/CatalogDomain.ts @@ -3,10 +3,6 @@ import type { RudderContext } from '../core/context.js'; import { api } from '../generated/api.js'; import type { CatalogCurrency } from '../generated/catalog.js'; -/** - * CatalogDomain — currency definitions, keyed by slug. Synced under revision - * key `catalog`. - */ export class CatalogDomain extends SyncedState> { readonly syncKey = 'catalog'; diff --git a/src/domains/ModulesDomain.ts b/src/domains/ModulesDomain.ts index 9a975da..f675f14 100644 --- a/src/domains/ModulesDomain.ts +++ b/src/domains/ModulesDomain.ts @@ -8,22 +8,12 @@ import type { } from '../generated/modules.js'; export interface ModuleCallOptions { - /** Version the caller was built against; sent as `X-Rudder-Module-Version: @`. */ moduleVersion?: string; } -/** - * ModulesDomain — calls module functions. Usually reached through a client - * generated by `rudder client generate`, not directly. - */ export class ModulesDomain { constructor(private readonly ctx: RudderContext) {} - /** - * Calls `module.fn` and returns its `result`. Each call gets one - * Idempotency-Key reused across retries (network errors, 5xx, 409 `conflict`). - * Rejects with {@link RudderModuleError} on 422. - */ async call( module: string, fn: string, @@ -49,7 +39,6 @@ export class ModulesDomain { try { payload = JSON.parse(err.body); } catch { - // err.code came from this body, so it is JSON; keep the fallback anyway. } throw new RudderModuleError(err.statusText, err.body, { ...payload, code: err.code }); } diff --git a/src/domains/PlayerDomain.ts b/src/domains/PlayerDomain.ts index c1afc43..fd2c519 100644 --- a/src/domains/PlayerDomain.ts +++ b/src/domains/PlayerDomain.ts @@ -3,11 +3,6 @@ import type { RudderContext } from '../core/context.js'; import { api } from '../generated/api.js'; import type { PlayerProfile } from '../generated/player.js'; -/** - * PlayerDomain — the observable player profile (player, identities, - * balances, items). - * Synced under revision key `profile`. - */ export class PlayerDomain extends SyncedState { readonly syncKey = 'profile'; diff --git a/src/index.ts b/src/index.ts index 1c5e365..b9204d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,3 @@ -// @rudder/sdk — Rudder client SDK for web games - -// Client export { RudderClient, createClient } from './client/RudderClient.js'; export type { RudderClientOptions } from './client/RudderClientOptions.js'; export { @@ -13,11 +10,9 @@ export { } from './client/RudderError.js'; export type { RudderErrorCodeLike } from './client/RudderError.js'; -// Token management export type { TokenStore } from './token/TokenStore.js'; export { createDefaultTokenStore, createLocalStorageTokenStore } from './token/TokenStore.js'; -// Services — types only; instances live on the client (client.auth). export type { AuthService, AuthState, @@ -29,7 +24,6 @@ export type { LoginWithGoogleOptions, } from './auth/AuthService.js'; -// Observable state primitives export { SyncedState } from './state/SyncedState.js'; export type { SyncedSnapshot, @@ -37,15 +31,11 @@ export type { SyncedListener, } from './state/SyncedState.js'; -// Domains — types only; instances live on the client -// (client.player, client.catalog, client.storage, client.modules). export type { PlayerDomain } from './domains/PlayerDomain.js'; export type { CatalogDomain } from './domains/CatalogDomain.js'; export type { ModulesDomain, ModuleCallOptions } from './domains/ModulesDomain.js'; export type { StorageDomain } from './domains/StorageDomain.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 { CurrencyBalance, InventoryStack, Player, PlayerProfile, PlayerProfileIdentity } from './generated/player.js'; export type { CatalogCurrency } from './generated/catalog.js'; export type { StorageItem, GetStorageResponse } from './generated/storage.js'; diff --git a/src/state/SyncEngine.ts b/src/state/SyncEngine.ts index 9b922d2..a79445e 100644 --- a/src/state/SyncEngine.ts +++ b/src/state/SyncEngine.ts @@ -6,21 +6,11 @@ export const DEFAULT_SYNC_INTERVAL_MS = 30_000; const REVISIONS_STORAGE_KEY = 'rudder_revisions'; const JITTER_RATIO = 0.2; -/** A domain the sync engine can refresh when its server revision grows. */ export interface SyncableDomain { readonly syncKey: string; invalidate(): void; } -/** - * SyncEngine — polls GET /sdk/v1/sync and invalidates state entities whose - * server-side revision grew. - * - * The first successful sync after login only fixes the baseline (entities - * were just warmed); revisions are persisted to localStorage and cleared on - * logout. Polling pauses while the document is hidden, and poll errors never - * stop the loop (401s are handled by the transport refresh flow). - */ export class SyncEngine { private timer: ReturnType | undefined; private revisions: Record = {}; @@ -89,7 +79,6 @@ export class SyncEngine { const revisions = await api.getRevisions(this.ctx); this.applyRevisions(revisions ?? {}); } catch { - // Network/server errors must not stop the loop. } finally { this.polling = false; } @@ -97,9 +86,6 @@ export class SyncEngine { } private applyRevisions(next: Record): void { - // On the first sync of a session, only establish the baseline UNLESS we - // restored revisions persisted by a previous session — in that case diff - // against them so entities changed while the tab was gone are refreshed. const skipDiff = !this.baselineEstablished && Object.keys(this.revisions).length === 0; this.baselineEstablished = true; if (!skipDiff) { @@ -115,7 +101,6 @@ export class SyncEngine { } private invalidate(key: string): void { - // Unknown keys have no registered domain and are ignored. this.byKey.get(key)?.invalidate(); } } @@ -137,7 +122,6 @@ function persistRevisions(revisions: Record): void { try { localStorage.setItem(REVISIONS_STORAGE_KEY, JSON.stringify(revisions)); } catch { - // Storage unavailable — best effort. } } @@ -145,6 +129,5 @@ function clearPersistedRevisions(): void { try { localStorage.removeItem(REVISIONS_STORAGE_KEY); } catch { - // Storage unavailable — silently ignore. } } diff --git a/src/state/SyncedState.ts b/src/state/SyncedState.ts index 46ef703..5a5b28b 100644 --- a/src/state/SyncedState.ts +++ b/src/state/SyncedState.ts @@ -1,12 +1,3 @@ -/** - * SyncedState — an observable cell for a single server-synced entity. - * - * Tracks `{ status, value, error }`, deduplicates parallel loads, and notifies - * listeners on every data/status change. `onChange` calls the callback - * immediately with the current snapshot, which makes syncing into external - * stores (React/zustand) trivial. - */ - export type SyncedStatus = 'idle' | 'loading' | 'ready' | 'error'; export interface SyncedSnapshot { @@ -26,7 +17,6 @@ export class SyncedState { constructor(private readonly loader: () => Promise) {} - /** Current data, or undefined when not loaded yet. */ get value(): T | undefined { return this.snapshot.value; } @@ -39,10 +29,6 @@ export class SyncedState { return this.snapshot.error; } - /** - * Subscribes to snapshot changes. The callback fires immediately with the - * current snapshot. Returns an unsubscribe function. - */ onChange(callback: SyncedListener): () => void { this.listeners.add(callback); callback(this.snapshot); @@ -51,7 +37,6 @@ export class SyncedState { }; } - /** Loads the entity if needed; deduplicates parallel loads. */ load(): Promise { if (this.snapshot.status === 'ready') { return Promise.resolve(this.snapshot.value as T); @@ -59,25 +44,16 @@ export class SyncedState { return this.startLoad(false); } - /** Forces a (re)load even when data is already present. */ reload(): Promise { return this.startLoad(true); } - /** - * Refetches when the entity is in use (has listeners or was loaded before); - * otherwise no-ops. Coalesced: multiple invalidations within the same tick - * (e.g. one mutation touching several boundaries) trigger a single reload. - * - * @internal - */ invalidate(): void { if (this.invalidateScheduled) return; if (this.listeners.size === 0 && this.snapshot.status === 'idle') return; this.invalidateScheduled = true; queueMicrotask(() => { - // A reset() between scheduling and flushing cancels the reload - // (the entity was dropped on logout). + // A reset() between scheduling and flushing cancels the reload. if (!this.invalidateScheduled) return; this.invalidateScheduled = false; if (this.listeners.size === 0 && this.snapshot.status === 'idle') return; @@ -85,11 +61,6 @@ export class SyncedState { }); } - /** - * Drops all state back to idle (logout). - * - * @internal - */ reset(): void { this.generation++; this.pending = undefined; diff --git a/src/token/TokenStore.ts b/src/token/TokenStore.ts index f4c0b89..6a160dc 100644 --- a/src/token/TokenStore.ts +++ b/src/token/TokenStore.ts @@ -1,33 +1,16 @@ -/** - * Token persistence interface and localStorage-backed default implementation. - * - * The TokenStore is injected via RudderClientOptions. When none is provided - * the client uses {@link createDefaultTokenStore}: localStorage in browsers, - * with a silent in-memory fallback where localStorage is unavailable - * (SSR, private mode). Consumers can provide a custom implementation for - * alternative storage backends (e.g., sessionStorage, secure cookies). - */ export interface TokenStore { - /** Returns the current access token, or null if not authenticated. */ getAccessToken(): string | null; - /** Returns the current refresh token, or null if not available. */ getRefreshToken(): string | null; - /** Persists access and refresh tokens after a successful login. */ saveTokens(accessToken: string, refreshToken: string): void; - /** Removes all stored tokens (logout). */ clear(): void; } const STORAGE_KEY_ACCESS = 'rudder_access_token'; const STORAGE_KEY_REFRESH = 'rudder_refresh_token'; -/** - * Creates the default TokenStore: localStorage-backed when usable, otherwise - * a silent in-memory fallback (tokens live for the page session only). - */ export function createDefaultTokenStore(): TokenStore { try { const probe = 'rudder_storage_probe'; @@ -39,11 +22,6 @@ export function createDefaultTokenStore(): TokenStore { } } -/** - * Creates a TokenStore backed by the browser's localStorage. - * - * Uses keys `rudder_access_token` and `rudder_refresh_token`. - */ export function createLocalStorageTokenStore(): TokenStore { return { getAccessToken(): string | null { @@ -67,7 +45,6 @@ export function createLocalStorageTokenStore(): TokenStore { localStorage.setItem(STORAGE_KEY_ACCESS, accessToken); localStorage.setItem(STORAGE_KEY_REFRESH, refreshToken); } catch { - // Storage full or unavailable — silently ignore. } }, @@ -76,7 +53,6 @@ export function createLocalStorageTokenStore(): TokenStore { localStorage.removeItem(STORAGE_KEY_ACCESS); localStorage.removeItem(STORAGE_KEY_REFRESH); } catch { - // Storage unavailable — silently ignore. } }, }; diff --git a/src/transport/request.ts b/src/transport/request.ts index c0b5da0..85e4998 100644 --- a/src/transport/request.ts +++ b/src/transport/request.ts @@ -1,8 +1,3 @@ -/** - * Core HTTP transport — fetch-based with auth injection, timeouts, retries, - * single-flight token refresh, and typed error mapping. - */ - import { RudderNetworkError, RudderHttpError, RudderAuthError } from '../client/RudderError.js'; import type { TokenStore } from '../token/TokenStore.js'; import type { @@ -15,14 +10,8 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; const MAX_RETRIES = 2; const RETRY_BASE_DELAY_MS = 300; -/** Per-call transport options. */ export interface RequestOptions { - /** Extra headers, sent unchanged on every attempt (retries and the post-refresh retry). */ headers?: Record; - /** - * Retry non-GET requests like GETs (network errors, 5xx) and also on 409 `conflict`. - * Only for calls made safe by an idempotency key. GETs always retry. - */ retry?: boolean; } @@ -30,28 +19,12 @@ export interface RequestContext { baseUrl: string; tokenStore: TokenStore; requestTimeoutMs?: number; - /** Sent with `platform: 'web'` on token refresh; omitted when not set. */ appVersion?: string; /** Single-flight refresh state shared by all requests of one client. */ refreshState?: { pending?: Promise }; - /** Called when the refresh flow fails — lets the client stop its runtime. */ onAuthFailure?: () => void; } -/** - * Sends an HTTP request to the LiveOps API. - * - * - Builds absolute URL from `baseUrl + path` - * - Injects `Authorization: Bearer ` header (if token is available) - * - Serializes body as JSON, deserializes response as JSON - * - Aborts the request after `requestTimeoutMs` (default 10s) - * - Retries up to 2 times (jittered 300ms × 2^n backoff): GETs on network errors and - * 5xx; `options.retry` requests also on 409 with code `conflict` - * - On 401 runs a single-flight token refresh and retries the request once; - * when the refresh fails, clears tokens and throws RudderAuthError - * - Maps HTTP errors to typed exceptions (RudderNetworkError, RudderHttpError, RudderAuthError) - * - Returns undefined for 204 No Content responses - */ export async function request( method: string, path: string, @@ -76,7 +49,6 @@ export async function request( if (!response.ok) { const bodyText = await response.text().catch(() => ''); - // Try to extract machine-readable code from error body. let code: string | undefined; try { const parsed = JSON.parse(bodyText); @@ -84,7 +56,6 @@ export async function request( code = parsed.code; } } catch { - // non-JSON body — ignore } if (response.status === 401) { @@ -93,7 +64,6 @@ export async function request( throw new RudderHttpError(response.status, response.statusText, bodyText, code); } - // 204 No Content — no body to parse. if (response.status === 204) { return undefined as unknown as TResponse; } @@ -101,7 +71,6 @@ export async function request( return response.json() as Promise; } -/** Fetches with per-attempt timeout; retries per {@link RequestOptions.retry}. */ async function requestWithRetry( method: string, path: string, @@ -168,10 +137,6 @@ async function fetchOnce( } } -/** - * Single-flight refresh: concurrent 401s share one refresh request. - * Resolves to true when a new token pair was stored. - */ async function refreshTokens(ctx: RequestContext): Promise { const state = (ctx.refreshState ??= {}); state.pending ??= doRefreshTokens(ctx).finally(() => { @@ -188,8 +153,9 @@ async function doRefreshTokens(ctx: RequestContext): Promise { const controller = new AbortController(); const timeout = ctx.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; const timer = setTimeout(() => controller.abort(), timeout); + let response: Response; try { - const response = await fetch(url, { + response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -199,19 +165,22 @@ async function doRefreshTokens(ctx: RequestContext): Promise { } satisfies RefreshAccessTokenRequest), signal: controller.signal, }); - if (!response.ok) return false; - const tokens = (await response.json()) as RefreshAccessTokenResponse; - if (!tokens.accessToken || !tokens.refreshToken) return false; - ctx.tokenStore.saveTokens(tokens.accessToken, tokens.refreshToken); - return true; - } catch { - return false; + } catch (err) { + throw new RudderNetworkError(err); } finally { clearTimeout(timer); } + if (response.status >= 400 && response.status < 500 && response.status !== 429) return false; + if (!response.ok) { + const bodyText = await response.text().catch(() => ''); + throw new RudderHttpError(response.status, response.statusText, bodyText); + } + const tokens = (await response.json()) as RefreshAccessTokenResponse; + if (!tokens.accessToken || !tokens.refreshToken) return false; + ctx.tokenStore.saveTokens(tokens.accessToken, tokens.refreshToken); + return true; } -/** 300ms × 2^attempt, scaled by a random factor in [0.5, 1.5). */ function backoff(attempt: number): Promise { const ms = RETRY_BASE_DELAY_MS * 2 ** attempt * (0.5 + Math.random()); return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/transport/url.ts b/src/transport/url.ts deleted file mode 100644 index 77abb68..0000000 --- a/src/transport/url.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * URL utility helpers for encoding and query string construction. - */ - -/** URL-encodes a single value for use in path/query segments. */ -export function encodeUrl(value: string): string { - return encodeURIComponent(value); -} - -/** - * Builds a query string from an array of key-value pairs. - * Null/undefined/empty values are skipped. - * Returns an empty string if no valid params, or a leading `?` followed by the query. - */ -export function buildQuery(params: Array<[string, string | null | undefined]>): string { - const parts: string[] = []; - for (const [key, value] of params) { - if (value != null && value !== '') { - parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); - } - } - return parts.length === 0 ? '' : `?${parts.join('&')}`; -} diff --git a/test/e2e-prod/_setup/adminClient.ts b/test/e2e-prod/_setup/adminClient.ts index 5de493a..eee073b 100644 --- a/test/e2e-prod/_setup/adminClient.ts +++ b/test/e2e-prod/_setup/adminClient.ts @@ -1,7 +1,3 @@ -// Minimal fetch wrapper for the LiveOps platform/admin HTTP API used by the seed -// and by tests' admin-side assertions. Retries network errors and 5xx with backoff; -// fails fast on 4xx. - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export class ApiError extends Error { @@ -54,8 +50,8 @@ export function createAdminClient(baseUrl: string): AdminClient { if (!res.ok) throw new ApiError(res.status, path, text); return (text ? JSON.parse(text) : undefined) as T; } catch (err) { - if (err instanceof ApiError) throw err; // 4xx — don't retry - lastErr = err; // network error — retry + if (err instanceof ApiError) throw err; + lastErr = err; await sleep(300 * (attempt + 1)); } } diff --git a/test/e2e-prod/_setup/globalSetup.ts b/test/e2e-prod/_setup/globalSetup.ts index 86d6f93..6898c44 100644 --- a/test/e2e-prod/_setup/globalSetup.ts +++ b/test/e2e-prod/_setup/globalSetup.ts @@ -1,9 +1,3 @@ -// Vitest globalSetup for the production e2e suite. -// -// Setup: seeds a fresh project on prod (unless RUDDER_E2E_PROJECT_KEY is provided) and -// writes .e2e-prod.local.json for the tests to read. -// Teardown: deletes the seeded project (default). Set RUDDER_E2E_KEEP=1 to keep it. - import { runSeed, deleteProject } from './seed.js'; import { readArtifact, writeArtifact, removeArtifact } from './artifact.js'; import type { SeedArtifact } from './types.js'; @@ -11,15 +5,12 @@ import type { SeedArtifact } from './types.js'; export default async function setup(): Promise<() => Promise> { const baseUrl = process.env.RUDDER_E2E_BASE_URL ?? 'https://api.rudder.build'; - // Externally-provided project: skip seeding, just verify an artifact/env exists. if (process.env.RUDDER_E2E_PROJECT_KEY) { const existing = readArtifact(); if (!existing) { writeArtifact({ baseUrl, projectKey: process.env.RUDDER_E2E_PROJECT_KEY, - // Remaining fields are unknown for an external project; tests relying on the - // seeded dataset/admin token will require a real seed run. } as unknown as SeedArtifact); } return async () => {}; diff --git a/test/e2e-prod/_setup/harness.ts b/test/e2e-prod/_setup/harness.ts index 1e330ad..513f822 100644 --- a/test/e2e-prod/_setup/harness.ts +++ b/test/e2e-prod/_setup/harness.ts @@ -1,11 +1,8 @@ -// Test-side helpers: load the seed artifact and build SDK clients against prod. - import { RudderClient, createClient } from '../../../src/client/RudderClient.js'; import type { TokenStore } from '../../../src/token/TokenStore.js'; import { readArtifact } from './artifact.js'; import type { SeedArtifact } from './types.js'; -/** Loads the seed artifact; RUDDER_E2E_* env vars override file values. */ export function loadArtifact(): SeedArtifact { const file = readArtifact(); if (!file) { @@ -37,7 +34,6 @@ export function createInMemoryTokenStore(): TokenStore { }; } -/** A client with no auth yet (in-memory tokens). */ export function makeProdClient( artifact: SeedArtifact = loadArtifact(), ): RudderClient { @@ -48,7 +44,6 @@ export function makeProdClient( }); } -/** A device-authenticated client. In Node each call mints a fresh player (ephemeral device id). */ export async function freshPlayer( artifact: SeedArtifact = loadArtifact(), ): Promise { diff --git a/test/e2e-prod/_setup/seed.ts b/test/e2e-prod/_setup/seed.ts index 5318ed5..0324b0b 100644 --- a/test/e2e-prod/_setup/seed.ts +++ b/test/e2e-prod/_setup/seed.ts @@ -1,7 +1,3 @@ -// Production seed: registers an operator and creates a project. Creating a -// project provisions staging + prod environments; the prod SDK key is what -// the runtime SDK sends as projectKey. - import { createAdminClient } from './adminClient.js'; import type { SeedArtifact } from './types.js'; diff --git a/test/e2e-prod/_setup/types.ts b/test/e2e-prod/_setup/types.ts index e116dd4..2c71fe2 100644 --- a/test/e2e-prod/_setup/types.ts +++ b/test/e2e-prod/_setup/types.ts @@ -1,11 +1,7 @@ -// Shared types for the production e2e harness. - -/** Persisted seed output, written to .e2e-prod.local.json and read by tests. */ export interface SeedArtifact { baseUrl: string; projectId: string; projectKey: string; - /** Operator (platform) bearer token — used for admin calls (wallet grant, player details). */ adminToken: string; adminEmail: string; environment: string; diff --git a/test/e2e-prod/health.e2e.test.ts b/test/e2e-prod/health.e2e.test.ts index 9da6e34..ab934ec 100644 --- a/test/e2e-prod/health.e2e.test.ts +++ b/test/e2e-prod/health.e2e.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { loadArtifact } from './_setup/harness.js'; -// Smoke gate: if egress to prod is blocked or the gateway is down, fail fast here. describe('e2e-prod: health', () => { let baseUrl: string; beforeAll(() => { diff --git a/test/e2e/modules.e2e.test.ts b/test/e2e/modules.e2e.test.ts index b890944..69f13fd 100644 --- a/test/e2e/modules.e2e.test.ts +++ b/test/e2e/modules.e2e.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createTestClient } from '../helpers/createClient.js'; import { createFakeGateway, type FakeGateway } from '../helpers/fakeGateway.js'; -// Checked-in output of `rudder client generate --lang ts` for -// test/fixtures/leaderboards.manifest.json; `npm run typecheck` proves the -// generated factory accepts RudderClient. import { modules, type LeaderboardsSubmitError } from '../fixtures/rudder.modules.js'; import { RudderModuleError } from '../../src/client/RudderError.js'; diff --git a/test/helpers/FakeTokenStore.ts b/test/helpers/FakeTokenStore.ts index 783780b..4367b61 100644 --- a/test/helpers/FakeTokenStore.ts +++ b/test/helpers/FakeTokenStore.ts @@ -1,6 +1,5 @@ import type { TokenStore } from '../../src/token/TokenStore.js'; -/** In-memory token store for testing. */ export function createFakeTokenStore(): TokenStore { let access: string | null = null; let refresh: string | null = null; diff --git a/test/helpers/createClient.ts b/test/helpers/createClient.ts index 5d40680..2a784e7 100644 --- a/test/helpers/createClient.ts +++ b/test/helpers/createClient.ts @@ -2,7 +2,6 @@ import { RudderClient, createClient } from '../../src/client/RudderClient.js'; import type { RudderClientOptions } from '../../src/client/RudderClientOptions.js'; import { createFakeTokenStore } from './FakeTokenStore.js'; -/** Creates a RudderClient with test-friendly defaults and a fake token store. */ export function createTestClient(overrides?: Partial): RudderClient { return createClient({ baseUrl: 'https://api.test.rudder.build',