docs: README, CHANGELOG and skill for @rudder/sdk 0.1.0 with generated module clients
This commit is contained in:
+22
-167
@@ -1,173 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Unreleased
|
## 0.1.0
|
||||||
|
|
||||||
- Removed `client.effects`, the `Effects` / `EffectUnsubscribe` /
|
First release as `@rudder/sdk`. Not compatible with `@rudder/js-sdk` 2.x.
|
||||||
`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.
|
|
||||||
- Module business errors reject with `RudderHttpError` (`status` 422, body
|
|
||||||
`{ error, code, requestId }`). `code` is defined by the module and is an
|
|
||||||
open string.
|
|
||||||
- `RudderErrorCodes` and the `RudderErrorCode` type are no longer exported
|
|
||||||
from the package. The generated platform codes are now only `forbidden` and
|
|
||||||
`row_referenced`; the scenario and quest codes (`early_completion`,
|
|
||||||
`level_not_reached`, `node_not_active`, `objectives_incomplete`,
|
|
||||||
`run_expired`, `run_not_active`, `scenario_not_active`, `unknown_run`) are
|
|
||||||
removed with those domains. Compare `error.code` with string literals.
|
|
||||||
- Login and token refresh send `platform: 'web'` and, when the new optional
|
|
||||||
`appVersion` client option is set, `appVersion`. The backend stores them on
|
|
||||||
the player for segment rules (`player.platform`, `player.appVersion`).
|
|
||||||
|
|
||||||
## 2.0.0
|
- `createClient({ projectKey })` creates the client; `baseUrl` defaults to
|
||||||
|
`https://api.rudder.build`.
|
||||||
|
- Kernel surface only: `auth`, `player` (player, identities, wallets,
|
||||||
|
`counters`), `catalog`, `storage`, `modules`. Revision sync keys are
|
||||||
|
`profile`, `catalog` and `storage`.
|
||||||
|
- State objects expose `value` (was `data`), `status`, `error`, `load()`,
|
||||||
|
`reload()` and `onChange()`.
|
||||||
|
- `storage.save(type, data)` upserts the single item of a type;
|
||||||
|
`storage.delete(type)` removes it.
|
||||||
|
- `modules.call<T>(module, fn, args?, { moduleVersion? })` resolves to
|
||||||
|
`result` as `T`. It always sends `args` (`{}` when omitted), one
|
||||||
|
`Idempotency-Key` per call reused on retries, and
|
||||||
|
`X-Rudder-Module-Version: <module>@<version>` when a version is given.
|
||||||
|
Retries up to 2 times with jittered backoff on network errors, 5xx and 409
|
||||||
|
`conflict`. A 422 rejects with `RudderModuleError<C>` (`code`, `message`,
|
||||||
|
`requestId`).
|
||||||
|
- Typed module clients come from `rudder client generate --lang ts`.
|
||||||
|
- Removed: `client.resources(slug)`, `client.projectStorage` and their types.
|
||||||
|
|
||||||
Breaking change — kernel surface. Module-specific client methods are removed.
|
## 2.x (@rudder/js-sdk)
|
||||||
The SDK now exposes the kernel only: auth, player (including wallets), catalog
|
|
||||||
currencies, storage, project storage, revision sync,
|
|
||||||
`client.modules.call(module, fn, args)`, and `client.resources(slug)`.
|
|
||||||
|
|
||||||
### Removed
|
Deprecated. Its history is in git before the 0.1.0 commit.
|
||||||
|
|
||||||
- `client.battlePass` / `BattlePassService`
|
|
||||||
- `client.quests` / `QuestsService` / `QuestMetrics`
|
|
||||||
- `client.leaderboards` / `LeaderboardsService` / `LeaderboardHandle`
|
|
||||||
- `client.stores` / `StoresDomain` / `ShopHandle` / `OfferHandle`
|
|
||||||
- `client.inventory` / `InventoryDomain`
|
|
||||||
- `client.remoteConfig` / `ConfigDomain` / `RemoteConfigState`
|
|
||||||
- Scenario runtime (`ScenarioService`, login-event trigger, pending poll)
|
|
||||||
- Module-specific effects: `onStoreOffer`, `onLeaderboard`, `onQuest`,
|
|
||||||
`onBattlePass`, `onBattlePassLevel`, `onConfigChanged`
|
|
||||||
- Generated wire types for battle pass, quests, stores, inventory,
|
|
||||||
leaderboards, remote config, and scenarios
|
|
||||||
|
|
||||||
Call module functions with `client.modules.call(module, fn, args)` and read or
|
|
||||||
write resource rows with `client.resources(slug)` (`list` / `get` / `create` /
|
|
||||||
`update` / `delete`).
|
|
||||||
|
|
||||||
Also required by the backend environments release: every project now has
|
|
||||||
exactly two environments, `staging` and `prod`, and the SDK key passed as
|
|
||||||
`projectKey` decides which one the player belongs to. The environment never
|
|
||||||
appears in the client API: it is resolved at login and carried inside the
|
|
||||||
access and refresh tokens. Tokens issued before this release have no
|
|
||||||
environment claim and are rejected with 401, so the first call after the
|
|
||||||
backend upgrade refreshes, fails, clears the token store and emits
|
|
||||||
`'signed-out'` to every `onAuthStateChange` listener. Log the player in again
|
|
||||||
with `client.auth.loginWithDevice()` / `loginWithCustom()`.
|
|
||||||
|
|
||||||
## 1.0.0
|
|
||||||
|
|
||||||
- Scenario execution moved server-side. The SDK no longer walks a local DAG
|
|
||||||
or persists plan state to IndexedDB.
|
|
||||||
- Removed `RudderClientOptions.runtime.planStateStore` and the
|
|
||||||
`PlanStateStore` type.
|
|
||||||
- `GET /sdk/v1/scenarios/pending` is polled on login, on a ~30s heartbeat,
|
|
||||||
and at each effect `waitDeadline`. Completions POST
|
|
||||||
`/sdk/v1/scenarios/callback`; the response may carry the next
|
|
||||||
`PendingEffect`.
|
|
||||||
- Scenario `remote_config_override` nodes no longer emit `onConfigChanged`
|
|
||||||
(the server applies the override). The `ConfigChangedEffect` type and
|
|
||||||
`onConfigChanged` subscription remain.
|
|
||||||
|
|
||||||
## 0.6.0
|
|
||||||
|
|
||||||
- `LeaderboardSession.claim()` / effect `claim()` completes the leaderboard
|
|
||||||
node with handle `onClaim`. The server matches live rank to an authored
|
|
||||||
place and continues from that place (Grant Reward and/or In-App Message).
|
|
||||||
- `rewardClaimed()` is a deprecated alias for `claim()` and will be removed
|
|
||||||
in the next SDK version.
|
|
||||||
- `rank_not_eligible` on Claim leaves the session open (retry Claim or End);
|
|
||||||
it no longer fails the run.
|
|
||||||
|
|
||||||
## 0.5.1
|
|
||||||
|
|
||||||
- `QuestMetrics` / `reportProgress`: custom free-text metrics no longer
|
|
||||||
progress quests. Report a released catalog counter slug instead.
|
|
||||||
`purchaseOffer` / `purchaseItem` helpers are unchanged (server-side shop
|
|
||||||
fan-out).
|
|
||||||
|
|
||||||
## 0.5.0
|
|
||||||
|
|
||||||
- `client.auth.loginWithCustom({ customData, region?, language?, nickname? })`
|
|
||||||
— custom webhook auth, same token + runtime start as `loginWithDevice`.
|
|
||||||
- `QuestMetrics.purchaseOffer` / `purchaseItem` helpers for the shop purchase
|
|
||||||
metric format.
|
|
||||||
- Typecheck against regenerated models: `listQuests` no longer takes a body,
|
|
||||||
battle-pass `track` is `'free' | 'premium'`, scenario run status has no
|
|
||||||
`unknown_run`.
|
|
||||||
|
|
||||||
## 0.4.0
|
|
||||||
|
|
||||||
- Fixed `BattlePassLevelSession.level` reading node data key `level` — the
|
|
||||||
admin editor writes `levelNumber`, so tiers always reported `0`. The session
|
|
||||||
now reads `levelNumber`, matching the editor and server validation.
|
|
||||||
- UGC support removed from the generated wire types (`src/generated/ugc.ts`
|
|
||||||
deleted by apigen); no UGC surface was ever exposed by this SDK.
|
|
||||||
- Generated API types regenerated from the gateway openapi spec, adding the
|
|
||||||
new platform operations.
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
Breaking changes — the package was aligned with the Rudder SDK glossary and
|
|
||||||
the regenerated wire types (`src/generated`, apigen). No compatibility shims
|
|
||||||
are provided.
|
|
||||||
|
|
||||||
### Auth
|
|
||||||
|
|
||||||
- `client.auth.loginViaDevice(region, language, nickname?)` replaced by
|
|
||||||
`client.auth.loginWithDevice(options?: { region?, language?, nickname? })`.
|
|
||||||
Default region is now `'global'` (was `'en'`); default language stays `'en'`.
|
|
||||||
- New `client.auth.onAuthStateChange(cb)` — fires immediately with the current
|
|
||||||
state, then on every `'signed-in' | 'signed-out'` transition (login, logout,
|
|
||||||
and unrecoverable session expiry in the transport).
|
|
||||||
- New `client.auth.isAuthenticated`.
|
|
||||||
|
|
||||||
### Client configuration
|
|
||||||
|
|
||||||
- `tokenStore` in `RudderClientOptions` is now optional. Default:
|
|
||||||
localStorage-backed store with a silent in-memory fallback where
|
|
||||||
localStorage is unavailable (SSR, private mode). New `createDefaultTokenStore()` export.
|
|
||||||
- Invalid client options (`baseUrl` / `projectKey` missing) now throw
|
|
||||||
`RudderError` with `code: 'sdk/invalid-options'` instead of a plain `Error`.
|
|
||||||
- New `onEffectError` option — called when an effect handler throws
|
|
||||||
(default: `console.error`); previously such errors were swallowed silently.
|
|
||||||
|
|
||||||
### Renames
|
|
||||||
|
|
||||||
- `client.config` → `client.remoteConfig` (canonical glossary name).
|
|
||||||
- `client.battlepass` → `client.battlePass`; `BattlepassService` → `BattlePassService`.
|
|
||||||
- Effects: `onBattlepass` / `BattlepassEffect` → `onBattlePass` / `BattlePassEffect`;
|
|
||||||
`onBattlepassLevel` / `BattlepassLevelEffect` → `onBattlePassLevel` / `BattlePassLevelEffect`.
|
|
||||||
- Generated types: `SdkQuest` → `Quest`; `*Battlepass*` request/response types → `*BattlePass*`.
|
|
||||||
- Internal transport `sendAsync` → `request` (not part of the public surface).
|
|
||||||
|
|
||||||
### Leaderboards
|
|
||||||
|
|
||||||
- Removed `LeaderboardsService.getRanking(slug, limit)` and
|
|
||||||
`LeaderboardsService.submitScore(slug, score)`. Single path:
|
|
||||||
`client.leaderboards.findBySlug(slug)` → `handle.list(limit?)` / `handle.submit(score)`.
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
- `RudderNetworkError.inner` → standard `Error.cause`.
|
|
||||||
- `RudderHttpError.code` is typed as `RudderErrorCode | (string & {})`
|
|
||||||
(generated union of server error codes, plus room for unknown strings).
|
|
||||||
- New exports: `RudderErrorCode` (type), `RudderErrorCodes` (constants),
|
|
||||||
`SDK_ERROR_INVALID_OPTIONS`, `RudderErrorCodeLike` (type).
|
|
||||||
|
|
||||||
### Public surface
|
|
||||||
|
|
||||||
- The barrel no longer exports `getOrCreateDeviceId`, domain classes
|
|
||||||
(`PlayerDomain`, …) or service constructors (`AuthService`, …) as values —
|
|
||||||
they remain available as types. Instances live on the client.
|
|
||||||
- `SubmittedBy` is no longer re-exported (UGC is unsupported on web).
|
|
||||||
|
|
||||||
### Bundle
|
|
||||||
|
|
||||||
- The scenario engine and the IndexedDB plan store are loaded lazily (dynamic
|
|
||||||
import) on first login / first runtime access — a thin client never pulls
|
|
||||||
the scenario machinery into the initial chunk.
|
|
||||||
- The package now ships both ESM (`dist/index.js`) and CJS (`dist/index.cjs`)
|
|
||||||
builds with matching `exports` entries.
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# @rudder/js-sdk
|
# @rudder/sdk
|
||||||
|
|
||||||
LiveOps Web SDK for browser games — device auth, observable player state,
|
Rudder client SDK for web games: device and social auth, player state
|
||||||
catalog currencies, storage, generic resources, and module function calls.
|
(profile, wallets, counters), catalog currencies, player storage, and calls to
|
||||||
|
installed modules through generated typed clients.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
The package is published to a private registry, not npmjs. Point the
|
The package is published to the Gitea registry at hub.rudder.build, not
|
||||||
`@rudder` scope at it in your project's `.npmrc`:
|
npmjs. Point the `@rudder` scope at it in your project's `.npmrc`:
|
||||||
|
|
||||||
```
|
```
|
||||||
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/
|
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/
|
||||||
@@ -15,50 +16,87 @@ The package is published to a private registry, not npmjs. Point the
|
|||||||
Then install as usual (anonymous read access, no token needed):
|
Then install as usual (anonymous read access, no token needed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @rudder/js-sdk
|
npm install @rudder/sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { RudderClient } from '@rudder/js-sdk';
|
import { createClient } from '@rudder/sdk';
|
||||||
|
|
||||||
const client = new RudderClient({
|
const client = createClient({
|
||||||
baseUrl: 'https://api.rudder.build',
|
projectKey: 'your-project-key', // the key selects the environment
|
||||||
projectKey: 'your-project-key',
|
// baseUrl: 'https://api.rudder.build' (default)
|
||||||
// tokenStore is optional: defaults to localStorage with an in-memory
|
// tokenStore: localStorage with an in-memory fallback (default)
|
||||||
// fallback where localStorage is unavailable (SSR, private mode).
|
// appVersion: '1.4.2', sent with platform 'web' on login and refresh
|
||||||
appVersion: '1.4.2', // optional; sent with platform 'web' on login and refresh
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.auth.loginWithDevice(); // region 'global', language 'en'
|
await client.auth.loginWithDevice();
|
||||||
// or: await client.auth.loginWithDevice({ region: 'eu', language: 'de', nickname: 'Bob' });
|
|
||||||
|
|
||||||
const profile = client.player.data;
|
const profile = client.player.value;
|
||||||
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
|
const gold = profile?.wallets?.find((w) => w.currency === 'gold')?.balance;
|
||||||
|
const wins = profile?.counters?.wins ?? 0;
|
||||||
|
|
||||||
const items = await client.resources('items').list();
|
await client.storage.save('progress', JSON.stringify({ level: 7 }));
|
||||||
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' });
|
|
||||||
|
|
||||||
client.dispose();
|
client.dispose();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Module clients
|
||||||
|
|
||||||
|
Game features are installed modules. Generate a typed client for the modules
|
||||||
|
installed in an environment with the `rudder` CLI and commit the file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...
|
||||||
|
rudder client generate --lang ts --out src/rudder.modules.ts # --environment staging (default) | prod
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createClient, RudderModuleError } from '@rudder/sdk';
|
||||||
|
import { modules, type LeaderboardsSubmitError } from './rudder.modules';
|
||||||
|
|
||||||
|
const client = createClient({ projectKey });
|
||||||
|
const m = modules(client);
|
||||||
|
|
||||||
|
await m.leaderboards.submit({ slug: 'weekly', score: 120 });
|
||||||
|
const top = await m.leaderboards.top({ slug: 'weekly', limit: 10 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await m.leaderboards.submit({ slug: 'weekly', score: 120 });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RudderModuleError) {
|
||||||
|
const code = error.code as LeaderboardsSubmitError;
|
||||||
|
console.warn(code, error.message, error.requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only `trigger: client` functions are generated. Each generated function calls
|
||||||
|
`client.modules.call<Result>(module, fn, args, { moduleVersion })`, which you
|
||||||
|
can also call directly:
|
||||||
|
|
||||||
|
- `args` is always sent (`{}` when omitted); the promise resolves to `result`.
|
||||||
|
- Every call gets one `Idempotency-Key`, reused on its retries, so a retried
|
||||||
|
call runs at most once on the server.
|
||||||
|
- Retries: up to 2, with jittered backoff, on network errors, 5xx and 409
|
||||||
|
`conflict`. Other 4xx responses are never retried.
|
||||||
|
- `moduleVersion` is sent as `X-Rudder-Module-Version: <module>@<version>`.
|
||||||
|
The backend logs a warning on a major mismatch and still runs the call.
|
||||||
|
- A 422 rejects with `RudderModuleError` (`code`, `message`, `requestId`).
|
||||||
|
|
||||||
## Surface
|
## Surface
|
||||||
|
|
||||||
| Area | Access |
|
| Area | Access |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Auth | `client.auth.loginWithDevice()`, `loginWithGoogle({ idToken })`, `loginWithApple({ idToken })`, `loginWithCustom({ customData })`, `linkWithGoogle(idToken)`, `linkWithApple(idToken)`, `linkWithCustom({ customData })`, `unlinkIdentity(provider)`, `logout()`, `isAuthenticated`, `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) |
|
| Player | `client.player` state: `player`, `identities`, `wallets`, `counters` |
|
||||||
| Catalog currencies | `client.catalog` (observable) |
|
| Catalog currencies | `client.catalog` state (`Map<slug, CatalogCurrency>`) |
|
||||||
| Storage | `client.storage` (observable) + `.save(items)` / `.delete(type)` |
|
| Storage | `client.storage` state + `.save(type, data)` / `.delete(type)` |
|
||||||
| Project storage | `client.projectStorage` (observable) + `.save(items)` |
|
| Modules | `client.modules.call<T>(module, fn, args?, { moduleVersion? })`, usually through a generated client |
|
||||||
| Modules | `client.modules.call(module, fn, args?)` |
|
|
||||||
| Resources | `client.resources(slug)` → `list` / `get` / `create` / `update` / `delete` |
|
|
||||||
|
|
||||||
Game features are installed modules, not SDK services. The first-party
|
`counters` holds the values of the project's client-visible number counters
|
||||||
modules are items, inventory, reward, remote_config, leaderboards, quests,
|
by slug; currency balances are in `wallets`.
|
||||||
store and battlepass. Call a module through `client.modules.call` and read its
|
|
||||||
rows through `client.resources(slug)`.
|
|
||||||
|
|
||||||
## Error handling
|
## Error handling
|
||||||
|
|
||||||
@@ -71,30 +109,31 @@ import {
|
|||||||
RudderNetworkError,
|
RudderNetworkError,
|
||||||
RudderHttpError,
|
RudderHttpError,
|
||||||
RudderAuthError,
|
RudderAuthError,
|
||||||
} from '@rudder/js-sdk';
|
RudderModuleError,
|
||||||
|
} from '@rudder/sdk';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.player.reload();
|
await client.player.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RudderAuthError) {
|
if (error instanceof RudderAuthError) {
|
||||||
// 401 — session expired and the refresh failed; the client already
|
// 401: session expired and the refresh failed; the client already
|
||||||
// cleared tokens and emitted 'signed-out' via onAuthStateChange.
|
// cleared tokens and emitted 'signed-out' via onAuthStateChange.
|
||||||
showLogin();
|
showLogin();
|
||||||
} else if (error instanceof RudderHttpError) {
|
} else if (error instanceof RudderHttpError) {
|
||||||
console.error(error.status, error.code, error.body);
|
console.error(error.status, error.code, error.body);
|
||||||
} else if (error instanceof RudderNetworkError) {
|
} else if (error instanceof RudderNetworkError) {
|
||||||
// fetch failed / timeout — the underlying error is in error.cause
|
// fetch failed or timed out; the underlying error is in error.cause
|
||||||
console.error('offline?', error.cause);
|
console.error('offline?', error.cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
A module business error rejects with `RudderHttpError` (`status` 422) whose
|
`RudderModuleError` extends `RudderHttpError` (`status` 422). Its type
|
||||||
`code` is the module's own open string code. The platform codes are
|
parameter narrows `code` to a generated union such as
|
||||||
`forbidden` and `row_referenced`.
|
`LeaderboardsSubmitError`. A module trap is a 500 and a timeout a 504.
|
||||||
|
|
||||||
Invalid client options (missing `baseUrl` / `projectKey`) throw `RudderError`
|
A missing `projectKey` throws `RudderError` with
|
||||||
with `code: 'sdk/invalid-options'` from the constructor.
|
`code: 'sdk/invalid-options'` from `createClient`.
|
||||||
|
|
||||||
Track the session lifecycle without polling the token store:
|
Track the session lifecycle without polling the token store:
|
||||||
|
|
||||||
@@ -107,7 +146,7 @@ client.auth.onAuthStateChange((state) => {
|
|||||||
|
|
||||||
## Data liveness model
|
## Data liveness model
|
||||||
|
|
||||||
Observable domains (`player`, `catalog`, `storage`, `projectStorage`) are
|
State objects (`player`, `catalog`, `storage`) are
|
||||||
warmed at login (`player` and `catalog`) or on first use, then kept fresh by
|
warmed at login (`player` and `catalog`) or on first use, then kept fresh by
|
||||||
a revision poll: the client asks the backend 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.
|
**30 s (±20% jitter)** and reloads only the entities whose revision grew.
|
||||||
@@ -124,10 +163,10 @@ straight onto `useSyncExternalStore`:
|
|||||||
```tsx
|
```tsx
|
||||||
import { useSyncExternalStore } from 'react';
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
function useSynced<T>(state: { onChange(cb: () => void): () => void; data: T | undefined }) {
|
function useSynced<T>(state: { onChange(cb: () => void): () => void; value: T | undefined }) {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
(onStoreChange) => state.onChange(() => onStoreChange()),
|
(onStoreChange) => state.onChange(() => onStoreChange()),
|
||||||
() => state.data,
|
() => state.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,3 +187,11 @@ npm run test:e2e-prod # drives a seeded project on the live backend
|
|||||||
|
|
||||||
Generated wire types (`src/generated`) come from `liveops-backend/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.
|
`npm run generate` to refresh them. Do not edit them by hand.
|
||||||
|
|
||||||
|
`test/fixtures/rudder.modules.ts` is CLI output for
|
||||||
|
`test/fixtures/leaderboards.manifest.json`; `npm run typecheck` checks that it
|
||||||
|
accepts `RudderClient`. Refresh it after generator changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rudder client generate --lang ts --manifests <dir with leaderboards.manifest.json> --out test/fixtures/rudder.modules.ts
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
---
|
---
|
||||||
name: rudder-web-sdk
|
name: rudder-web-sdk
|
||||||
description: Use when working with the Rudder TypeScript/JavaScript SDK (@rudder/js-sdk) — the browser/player-facing SDK for the Rudder LiveOps platform. Covers client setup, device/google/apple/custom auth, identity linking, observable kernel domains (player, catalog, storage), modules.call, resources(slug), and error handling. Load this whenever code imports from @rudder/js-sdk or RudderClient appears.
|
description: Use when working with the Rudder TypeScript/JavaScript client SDK (@rudder/sdk) for web games. Covers createClient, device/google/apple/custom auth, identity linking, state objects (player with wallets and counters, catalog, storage), module calls through clients generated by `rudder client generate`, idempotent retries, and error handling. Load this whenever code imports from @rudder/sdk or a generated rudder.modules.ts.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Rudder Web SDK (@rudder/js-sdk)
|
# Rudder Web SDK (@rudder/sdk)
|
||||||
|
|
||||||
Browser-facing player SDK for the Rudder LiveOps platform. Source of truth:
|
Client SDK for web games on the Rudder LiveOps platform. Source of truth:
|
||||||
`liveops-web-sdk/src` (generated wire types in `src/generated` come from
|
`liveops-web-sdk/src` (wire types in `src/generated` come from
|
||||||
`liveops-backend/apigen` — never edit them by hand).
|
`liveops-backend/apigen`; never edit them by hand).
|
||||||
|
|
||||||
2.0 kernel surface: auth, player (wallets), catalog currencies, storage,
|
0.1.0 covers only the kernel: auth, player, catalog, storage, module calls and
|
||||||
project storage, revision sync, `client.modules.call(module, fn, args)`,
|
internal revision sync. Game features are installed modules, reached through a
|
||||||
and `client.resources(slug)`. Module-specific methods (battle pass, quests,
|
typed client generated from their manifests. There is no `resources(slug)` and
|
||||||
leaderboards, stores, inventory, remote config, scenarios) are gone.
|
no `projectStorage`; game-facing configuration comes from modules such as
|
||||||
|
`remote_config`.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Published to a private registry, not npmjs. Point the `@rudder` scope at it in
|
Published to the Gitea registry at hub.rudder.build, not npmjs. Point the
|
||||||
`.npmrc` (anonymous read, no token):
|
`@rudder` scope at it in `.npmrc` (anonymous read, no token):
|
||||||
|
|
||||||
```
|
```
|
||||||
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/
|
@rudder:registry=https://hub.rudder.build/api/packages/rudder/npm/
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @rudder/js-sdk
|
npm install @rudder/sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
|
ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
|
||||||
@@ -32,22 +33,21 @@ ESM + CJS + `.d.ts`; `type: module`; zero runtime dependencies.
|
|||||||
## Init
|
## Init
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { RudderClient } from '@rudder/js-sdk';
|
import { createClient } from '@rudder/sdk';
|
||||||
|
|
||||||
const client = new RudderClient({
|
const client = createClient({
|
||||||
baseUrl: 'https://api.rudder.build', // required
|
projectKey: 'your-project-key', // required; selects staging or prod
|
||||||
projectKey: 'your-project-key', // required
|
// baseUrl?: string — default 'https://api.rudder.build'
|
||||||
// tokenStore?: TokenStore — default: localStorage, in-memory fallback
|
// tokenStore?: TokenStore — default: localStorage, in-memory fallback
|
||||||
|
// appVersion?: string — sent with platform 'web' on login/refresh
|
||||||
// requestTimeoutMs?: number — default 10000
|
// requestTimeoutMs?: number — default 10000
|
||||||
// syncIntervalMs?: number — revision poll, default 30000 (±20% jitter)
|
// syncIntervalMs?: number — revision poll, default 30000 (±20% jitter)
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Missing `baseUrl`/`projectKey` throws `RudderError` with
|
A missing `projectKey` throws `RudderError` with
|
||||||
`code: 'sdk/invalid-options'` from the constructor.
|
`code: 'sdk/invalid-options'`. Call `client.dispose()` on unmount/HMR to stop
|
||||||
|
the sync poll and drop cached state.
|
||||||
Call `client.dispose()` on unmount/HMR to stop the sync poll and drop cached
|
|
||||||
state.
|
|
||||||
|
|
||||||
## Auth essentials
|
## Auth essentials
|
||||||
|
|
||||||
@@ -65,12 +65,9 @@ client.auth.isAuthenticated;
|
|||||||
client.auth.onAuthStateChange(cb);
|
client.auth.onAuthStateChange(cb);
|
||||||
```
|
```
|
||||||
|
|
||||||
Linked identities are listed on the profile: `client.player.data.identities`
|
|
||||||
(`PlayerProfileIdentity { provider, subject, createdAt }`).
|
|
||||||
|
|
||||||
Tokens are saved to the `TokenStore` automatically. The transport injects
|
Tokens are saved to the `TokenStore` automatically. The transport injects
|
||||||
`Authorization: Bearer <token>`, retries GETs, and single-flight refreshes on
|
`Authorization: Bearer <token>` and single-flight refreshes on 401; a failed
|
||||||
401; a failed refresh clears tokens and emits `signed-out`. Details:
|
refresh clears tokens and emits `signed-out`. Details:
|
||||||
[reference/auth.md](reference/auth.md).
|
[reference/auth.md](reference/auth.md).
|
||||||
|
|
||||||
## Client surface
|
## Client surface
|
||||||
@@ -78,64 +75,68 @@ Tokens are saved to the `TokenStore` automatically. The transport injects
|
|||||||
| Area | Access | Kind |
|
| Area | Access | Kind |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Auth | `client.auth` | service |
|
| Auth | `client.auth` | service |
|
||||||
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
|
| Player | `client.player` (`player`, `identities`, `wallets`, `counters`) | `SyncedState<PlayerProfile>` |
|
||||||
| Catalog currencies | `client.catalog` | observable `SyncedState<Map<string, CatalogCurrency>>` |
|
| Catalog currencies | `client.catalog` | `SyncedState<Map<string, CatalogCurrency>>` |
|
||||||
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations |
|
| Player storage | `client.storage` + `.save(type, data)` / `.delete(type)` | `SyncedState<GetStorageResponse>` + mutations |
|
||||||
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation |
|
| Modules | `client.modules.call<T>(module, fn, args?, { moduleVersion? })` | resolves to `result` as `T` |
|
||||||
| Modules | `client.modules.call(module, fn, args?)` | `CallModuleFunctionClientResponse` (`{ result }`) |
|
|
||||||
| Resources | `client.resources(slug)` | `list` / `get` / `create` / `update` / `delete` |
|
|
||||||
|
|
||||||
Wallets live on `client.player.data.wallets`. Game features are installed
|
## Module calls through generated clients
|
||||||
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
|
```bash
|
||||||
|
RUDDER_URL=... RUDDER_TOKEN=... RUDDER_PROJECT=... \
|
||||||
|
rudder client generate --lang ts --out src/rudder.modules.ts [--environment staging|prod]
|
||||||
|
```
|
||||||
|
|
||||||
`player`, `catalog`, `storage`, and `projectStorage` all extend
|
```ts
|
||||||
`SyncedState<T>`:
|
import { modules } from './rudder.modules';
|
||||||
|
|
||||||
- `.data: T | undefined`, `.status: 'idle' | 'loading' | 'ready' | 'error'`,
|
const m = modules(client);
|
||||||
|
await m.leaderboards.submit({ slug: 'weekly', score: 120 });
|
||||||
|
const top = await m.leaderboards.top({ slug: 'weekly', limit: 10 });
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated file is a factory over `client.modules.call`, with argument and
|
||||||
|
result types, and a `<Module><Fn>Error` code union per function. Regenerate
|
||||||
|
and commit it when installed modules change. Details:
|
||||||
|
[reference/modules.md](reference/modules.md).
|
||||||
|
|
||||||
|
## State objects: the shared pattern
|
||||||
|
|
||||||
|
`player`, `catalog` and `storage` extend `SyncedState<T>`:
|
||||||
|
|
||||||
|
- `.value: T | undefined`, `.status: 'idle' | 'loading' | 'ready' | 'error'`,
|
||||||
`.error?: Error`
|
`.error?: Error`
|
||||||
- `onChange(cb: (snapshot: SyncedSnapshot<T>) => void): () => void` — fires
|
- `onChange(cb: (snapshot: SyncedSnapshot<T>) => void): () => void`: fires
|
||||||
immediately with the current snapshot; maps onto React `useSyncExternalStore`
|
immediately with `{ status, value, error }`; maps onto React
|
||||||
- `load(): Promise<T>` — deduplicated, no-op when ready
|
`useSyncExternalStore`
|
||||||
- `reload(): Promise<T>` — forced refetch
|
- `load(): Promise<T>`: deduplicated, no-op when ready
|
||||||
|
- `reload(): Promise<T>`: forced refetch
|
||||||
|
|
||||||
`player` and `catalog` are warmed once at login; `storage` and
|
`player` and `catalog` are warmed at login; `storage` loads on first use. A
|
||||||
`projectStorage` load on first use. All observable domains are then kept fresh
|
revision poll (`GET /sdk/v1/sync`, keys `profile`, `catalog`, `storage`) runs
|
||||||
by a revision poll every 30 s (±20% jitter); polling pauses while the tab is
|
every 30 s (±20% jitter), pauses while the tab is hidden, and reloads the
|
||||||
hidden. Storage mutations invalidate their domain immediately. Call `reload()`
|
states whose revision grew. Storage mutations invalidate immediately. Call
|
||||||
when you need a freshness guarantee.
|
`player.reload()` after a module call that changes wallets or counters when
|
||||||
|
you need fresh values right away.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
All SDK errors extend `RudderError` (optional machine-readable `code`):
|
All SDK errors extend `RudderError` (optional machine-readable `code`):
|
||||||
|
|
||||||
- `RudderNetworkError` — fetch failed / timeout (`error.cause` holds the original)
|
- `RudderNetworkError`: fetch failed or timed out (`error.cause` holds the original)
|
||||||
- `RudderHttpError` — non-2xx, carries `status`, `statusText`, `body`, `code`
|
- `RudderHttpError`: non-2xx, carries `status`, `statusText`, `body`, `code`
|
||||||
- `RudderAuthError extends RudderHttpError` — 401 after failed refresh; tokens
|
- `RudderAuthError extends RudderHttpError`: 401 after a failed refresh;
|
||||||
already cleared, `signed-out` already emitted
|
tokens already cleared, `signed-out` already emitted
|
||||||
|
- `RudderModuleError<C> extends RudderHttpError`: a module call returned 422
|
||||||
|
`{ error, code, requestId }`; exposes `code: C`, `message`, `requestId`
|
||||||
- `RudderError` with `code: 'sdk/invalid-options'` (`SDK_ERROR_INVALID_OPTIONS`)
|
- `RudderError` with `code: 'sdk/invalid-options'` (`SDK_ERROR_INVALID_OPTIONS`)
|
||||||
— constructor validation
|
|
||||||
|
|
||||||
The platform error codes are `forbidden` and `row_referenced`
|
Compare `error.code` with string literals. A module trap is a 500 and a
|
||||||
(`src/generated/errors.ts`, not exported from the package entry); compare
|
timeout a 504, both without details.
|
||||||
`error.code === 'forbidden'`.
|
|
||||||
|
|
||||||
`client.modules.call` resolves to `{ result }` only; console output and the
|
|
||||||
kernel call log never reach players. A module business error rejects with
|
|
||||||
`RudderHttpError` whose `status` is 422 and whose `code` is the module's own
|
|
||||||
code (the body is `{ error, code, requestId }`). Module codes are open strings
|
|
||||||
with no generated constants, so compare against the codes the module
|
|
||||||
documents. A module trap is a 500 and a timeout a 504, both without details.
|
|
||||||
|
|
||||||
## Reference files
|
## Reference files
|
||||||
|
|
||||||
- [reference/auth.md](reference/auth.md) — login flows, options, token store, session lifecycle
|
- [reference/auth.md](reference/auth.md): login flows, options, token store, session lifecycle
|
||||||
- [reference/player.md](reference/player.md) — profile + wallets (`PlayerProfile`, `Wallet`)
|
- [reference/player.md](reference/player.md): profile, wallets, counters
|
||||||
- [reference/storage.md](reference/storage.md) — player + project storage, limits
|
- [reference/storage.md](reference/storage.md): player storage, limits
|
||||||
- [reference/modules.md](reference/modules.md) — `modules.call`, response, module errors
|
- [reference/modules.md](reference/modules.md): generated clients, `modules.call`, idempotency, retries, errors
|
||||||
- [reference/resources.md](reference/resources.md) — `resources(slug)` row operations
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ await client.auth.unlinkIdentity('custom:my-backend');
|
|||||||
`google`, `apple`, `custom`, or `custom:<name>`.
|
`google`, `apple`, `custom`, or `custom:<name>`.
|
||||||
|
|
||||||
Linked identities appear on the player profile:
|
Linked identities appear on the player profile:
|
||||||
`client.player.data.identities: PlayerProfileIdentity[]` with
|
`client.player.value.identities: PlayerProfileIdentity[]` with
|
||||||
`{ provider: string; subject: string; createdAt: string }`.
|
`{ provider: string; subject: string; createdAt: string }`.
|
||||||
|
|
||||||
## Environments
|
## Environments
|
||||||
@@ -113,10 +113,6 @@ the client API takes an environment argument, and a player created in one
|
|||||||
environment is invisible in the other. Content released only to `staging` is
|
environment is invisible in the other. Content released only to `staging` is
|
||||||
empty for a `prod` key and vice versa.
|
empty for a `prod` key and vice versa.
|
||||||
|
|
||||||
Tokens issued before SDK 2.0.0 carry no environment claim and are rejected with
|
|
||||||
401. The transport's refresh then fails, clears the token store and emits
|
|
||||||
`'signed-out'` — log the player in again.
|
|
||||||
|
|
||||||
## Auth state
|
## Auth state
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -165,7 +161,7 @@ backends (sessionStorage, cookies).
|
|||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
- Constructor: missing `baseUrl`/`projectKey` → `RudderError`,
|
- `createClient`: missing `projectKey` → `RudderError`,
|
||||||
`code: 'sdk/invalid-options'`.
|
`code: 'sdk/invalid-options'`.
|
||||||
- Login failure → `RudderHttpError` (e.g. unknown project key) or
|
- Login failure → `RudderHttpError` (e.g. unknown project key) or
|
||||||
`RudderNetworkError`.
|
`RudderNetworkError`.
|
||||||
|
|||||||
@@ -3,29 +3,75 @@
|
|||||||
Source: `src/domains/ModulesDomain.ts`. Calls an installed module function as
|
Source: `src/domains/ModulesDomain.ts`. Calls an installed module function as
|
||||||
the signed-in player through `POST /sdk/v1/modules/{module}/{fn}`.
|
the signed-in player through `POST /sdk/v1/modules/{module}/{fn}`.
|
||||||
|
|
||||||
```ts
|
## Generated clients
|
||||||
client.modules.call(module: string, fn: string, args?: unknown): Promise<CallModuleFunctionClientResponse>
|
|
||||||
// CallModuleFunctionClientResponse = { result?: unknown }
|
Use `rudder client generate` from `liveops-module-sdk/cli` instead of calling
|
||||||
|
modules by name:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=...
|
||||||
|
rudder client generate --lang ts --out src/rudder.modules.ts # staging by default
|
||||||
|
rudder client generate --lang ts --out src/rudder.modules.ts --environment prod
|
||||||
|
rudder client generate --lang ts --manifests ./modules --out src/rudder.modules.ts # local manifests
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
The file reads the manifests of the installed modules, generates only
|
||||||
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' });
|
`trigger: client` functions, and lists every module with its version in the
|
||||||
```
|
header. It exports:
|
||||||
|
|
||||||
`args` is sent as `{ args }`. The response carries only `result`; console
|
- `modules(client)`: `{ <module>: { <fn>(args): Promise<Result> } }`
|
||||||
output and the kernel call log stay on the platform dry run.
|
- `<Module><Fn>Args` / `<Module><Fn>Result` types from the manifest schemas
|
||||||
|
- `<Module><Fn>Error`: the function's declared codes plus `RudderKernelError`
|
||||||
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
|
```ts
|
||||||
|
import { createClient, RudderModuleError } from '@rudder/sdk';
|
||||||
|
import { modules, type StoreBuyError } from './rudder.modules';
|
||||||
|
|
||||||
|
const m = modules(createClient({ projectKey }));
|
||||||
try {
|
try {
|
||||||
await client.modules.call('my_module', 'hello');
|
const result = await m.store.buy({ offer: 'starter_pack' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RudderHttpError && error.status === 422) {
|
if (error instanceof RudderModuleError) {
|
||||||
console.warn(error.code); // module-defined
|
switch (error.code as StoreBuyError) {
|
||||||
|
case 'insufficient_funds': /* ... */ break;
|
||||||
|
case 'offer_not_found': /* ... */ break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Commit the generated file and regenerate it when installed modules change.
|
||||||
|
Do not edit it by hand.
|
||||||
|
|
||||||
|
## `modules.call`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
client.modules.call<T = unknown>(
|
||||||
|
module: string,
|
||||||
|
fn: string,
|
||||||
|
args?: unknown,
|
||||||
|
options?: { moduleVersion?: string },
|
||||||
|
): Promise<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Sends `{ args }`; `args` is `{}` when omitted. Resolves to the response
|
||||||
|
`result`. Console output and the kernel call log never reach players.
|
||||||
|
- Idempotency: each call generates one `Idempotency-Key` (UUID v4) and sends
|
||||||
|
the same key on every retry, so the server runs the function at most once
|
||||||
|
and returns the stored response for a repeated key. A new `call()` gets a
|
||||||
|
new key.
|
||||||
|
- Retries: up to 2, with jittered exponential backoff, on network errors, 5xx
|
||||||
|
and 409 with code `conflict`. Other 4xx responses (including other 409
|
||||||
|
codes) are never retried.
|
||||||
|
- `moduleVersion` (set by generated clients) is sent as
|
||||||
|
`X-Rudder-Module-Version: <module>@<version>`. A major mismatch with the
|
||||||
|
installed module is logged on the backend; the call still runs.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
- 422 `{ error, code, requestId }` rejects with `RudderModuleError<C>`
|
||||||
|
(`status` 422, `code`, `message` from `error`, `requestId`). `C` defaults
|
||||||
|
to `string`; use the generated union to narrow it.
|
||||||
|
- A trap is a 500 and a timeout a 504, both `RudderHttpError` without details
|
||||||
|
(after the retries above).
|
||||||
|
- 401 follows the normal refresh flow (`RudderAuthError` when it fails).
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Player profile + wallets — `client.player`
|
# Player — `client.player`
|
||||||
|
|
||||||
`PlayerDomain` (source: `src/domains/PlayerDomain.ts`) — the observable player
|
`PlayerDomain` (source: `src/domains/PlayerDomain.ts`) — the observable player
|
||||||
profile: identity plus currency wallets. Synced under revision key `profile`.
|
profile: player, linked identities, currency wallets and
|
||||||
|
number counters. Synced under revision key `profile`.
|
||||||
|
|
||||||
There is no separate wallet service in this SDK. Wallet balances are read from
|
There is no separate wallet service in this SDK. Wallet balances are read from
|
||||||
the profile and are credited/debited server-side (admin wallet adjust, module
|
the profile and are credited/debited server-side (admin wallet adjust, module
|
||||||
@@ -12,7 +13,7 @@ functions).
|
|||||||
`client.player` is a `SyncedState<PlayerProfile>`:
|
`client.player` is a `SyncedState<PlayerProfile>`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
client.player.data // PlayerProfile | undefined
|
client.player.value // PlayerProfile | undefined
|
||||||
client.player.status // 'idle' | 'loading' | 'ready' | 'error'
|
client.player.status // 'idle' | 'loading' | 'ready' | 'error'
|
||||||
client.player.onChange((snapshot) => { /* fires immediately */ });
|
client.player.onChange((snapshot) => { /* fires immediately */ });
|
||||||
await client.player.load();
|
await client.player.load();
|
||||||
@@ -23,6 +24,7 @@ await client.player.reload(); // force refetch
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface PlayerProfile {
|
interface PlayerProfile {
|
||||||
|
counters?: { [slug: string]: number }; // client-visible number counters
|
||||||
identities?: PlayerProfileIdentity[];
|
identities?: PlayerProfileIdentity[];
|
||||||
player?: Player;
|
player?: Player;
|
||||||
wallets?: Wallet[];
|
wallets?: Wallet[];
|
||||||
@@ -51,9 +53,12 @@ interface Wallet {
|
|||||||
|
|
||||||
## Behavior notes
|
## Behavior notes
|
||||||
|
|
||||||
|
- `counters` holds the values of `number` counters whose definition is
|
||||||
|
`client_visible`; hidden counters are left out. Currency counters are in
|
||||||
|
`wallets`, not in `counters`. Counter changes bump the `profile` revision.
|
||||||
- Warmed automatically at login (with catalog).
|
- Warmed automatically at login (with catalog).
|
||||||
- Kept fresh by the revision poll; call `reload()` after a module function
|
- Kept fresh by the revision poll; call `reload()` after a module function
|
||||||
that mutates wallets.
|
that changes wallets or counters.
|
||||||
- All fields are optional on the wire (`?`); code defensively.
|
- All fields are optional on the wire (`?`); code defensively.
|
||||||
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
|
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
|
||||||
README "React recipe").
|
README "React recipe").
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# 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<ListResourceRowsResponse> // GET /{slug} → { items?: ResourceRow[], total?: number }
|
|
||||||
items.get(id): Promise<ResourceRow> // GET /{slug}/{id}
|
|
||||||
items.create({ id?, data? }): Promise<ResourceRow> // POST /{slug}
|
|
||||||
items.update(id, { data?, version? }): Promise<ResourceRow> // PATCH /{slug}/{id}
|
|
||||||
items.delete(id): Promise<void> // 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.
|
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
# Storage — `client.storage` + `client.projectStorage`
|
# Storage — `client.storage`
|
||||||
|
|
||||||
Two key/value stores: per-player (`StorageDomain`, revision key `storage`) and
|
Per-player storage, one item per `type`. `StorageDomain`, revision key
|
||||||
project-wide shared (`ProjectStorageDomain`, revision key `projectStorage`).
|
`storage`. Source: `src/domains/StorageDomain.ts`.
|
||||||
Sources: `src/domains/StorageDomain.ts`, `src/domains/ProjectStorageDomain.ts`.
|
|
||||||
|
|
||||||
## Player storage
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
client.storage.data // GetStorageResponse | undefined
|
client.storage.value // GetStorageResponse | undefined
|
||||||
await client.storage.save(items);
|
await client.storage.save(type, data);
|
||||||
await client.storage.delete(type);
|
await client.storage.delete(type);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -18,52 +15,21 @@ interface GetStorageResponse {
|
|||||||
nextCursor?: string;
|
nextCursor?: string;
|
||||||
}
|
}
|
||||||
interface StorageItem {
|
interface StorageItem {
|
||||||
data?: string; // opaque payload, JSON-stringify yourself if needed
|
data?: string; // opaque payload, JSON-stringify yourself
|
||||||
id?: string;
|
id?: string; // server-assigned
|
||||||
type?: string; // the storage "collection" key
|
type?: string; // the storage key
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `save(items: StorageItem[]): Promise<void>` — upserts items, then
|
- `save(type: string, data: string): Promise<void>`: upserts the item of
|
||||||
invalidates the domain so subscribers refetch.
|
`type` (the server keeps one item per player and type), then invalidates.
|
||||||
- `delete(type: string): Promise<void>` — deletes **all** player storage items
|
- `delete(type: string): Promise<void>`: deletes the item of `type`, then
|
||||||
of that type, then invalidates.
|
invalidates.
|
||||||
|
|
||||||
## Project storage
|
|
||||||
|
|
||||||
```ts
|
|
||||||
client.projectStorage.data // GetProjectStorageResponse | undefined
|
|
||||||
await client.projectStorage.save(items);
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface GetProjectStorageResponse {
|
|
||||||
items?: ProjectStorageItem[];
|
|
||||||
nextCursor?: string;
|
|
||||||
}
|
|
||||||
interface ProjectStorageItem {
|
|
||||||
data?: string;
|
|
||||||
expiresAt?: string;
|
|
||||||
id?: string;
|
|
||||||
readPermission?: 'public' | 'serverOnly';
|
|
||||||
size?: number;
|
|
||||||
type?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
version?: number;
|
|
||||||
writePermission?: 'public' | 'serverOnly';
|
|
||||||
}
|
|
||||||
interface ProjectStorageUpdateItem { data?: string; type?: string }
|
|
||||||
```
|
|
||||||
|
|
||||||
- `save(items: ProjectStorageUpdateItem[]): Promise<void>` — upserts, then
|
|
||||||
invalidates. Writing is only possible for items whose `writePermission` is
|
|
||||||
`public`; `serverOnly` items are read-only for clients.
|
|
||||||
- The SDK exposes no client-side project-storage delete.
|
|
||||||
|
|
||||||
## Limits and notes
|
## Limits and notes
|
||||||
|
|
||||||
- Both domains load with `{ limit: 100 }` — the observable snapshot holds at
|
- The state loads with `{ limit: 100 }`; `nextCursor` pagination is not
|
||||||
most 100 items and `nextCursor` pagination is not surfaced by the domain.
|
surfaced.
|
||||||
- `data` is a raw string on the wire; serialize/deserialize JSON yourself.
|
- `data` is a raw string on the wire; serialize and parse JSON yourself.
|
||||||
- Both are observable (`SyncedState`) and warmed by the revision poll only when
|
- Project storage is not reachable from the client SDK. It stays available to
|
||||||
in use; mutations self-invalidate.
|
modules, the dashboard and `/game/v1`.
|
||||||
|
|||||||
Reference in New Issue
Block a user