docs: README, CHANGELOG and skill for @rudder/sdk 0.1.0 with generated module clients
This commit is contained in:
@@ -101,7 +101,7 @@ await client.auth.unlinkIdentity('custom:my-backend');
|
||||
`google`, `apple`, `custom`, or `custom:<name>`.
|
||||
|
||||
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 }`.
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
```ts
|
||||
@@ -165,7 +161,7 @@ backends (sessionStorage, cookies).
|
||||
|
||||
## Errors
|
||||
|
||||
- Constructor: missing `baseUrl`/`projectKey` → `RudderError`,
|
||||
- `createClient`: missing `projectKey` → `RudderError`,
|
||||
`code: 'sdk/invalid-options'`.
|
||||
- Login failure → `RudderHttpError` (e.g. unknown project key) or
|
||||
`RudderNetworkError`.
|
||||
|
||||
@@ -3,29 +3,75 @@
|
||||
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>
|
||||
// CallModuleFunctionClientResponse = { result?: unknown }
|
||||
## Generated clients
|
||||
|
||||
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
|
||||
const { result } = await client.modules.call('my_module', 'hello', { name: 'Bob' });
|
||||
```
|
||||
The file reads the manifests of the installed modules, generates only
|
||||
`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
|
||||
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.
|
||||
- `modules(client)`: `{ <module>: { <fn>(args): Promise<Result> } }`
|
||||
- `<Module><Fn>Args` / `<Module><Fn>Result` types from the manifest schemas
|
||||
- `<Module><Fn>Error`: the function's declared codes plus `RudderKernelError`
|
||||
|
||||
```ts
|
||||
import { createClient, RudderModuleError } from '@rudder/sdk';
|
||||
import { modules, type StoreBuyError } from './rudder.modules';
|
||||
|
||||
const m = modules(createClient({ projectKey }));
|
||||
try {
|
||||
await client.modules.call('my_module', 'hello');
|
||||
const result = await m.store.buy({ offer: 'starter_pack' });
|
||||
} catch (error) {
|
||||
if (error instanceof RudderHttpError && error.status === 422) {
|
||||
console.warn(error.code); // module-defined
|
||||
if (error instanceof RudderModuleError) {
|
||||
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
|
||||
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
|
||||
the profile and are credited/debited server-side (admin wallet adjust, module
|
||||
@@ -12,7 +13,7 @@ functions).
|
||||
`client.player` is a `SyncedState<PlayerProfile>`:
|
||||
|
||||
```ts
|
||||
client.player.data // PlayerProfile | undefined
|
||||
client.player.value // PlayerProfile | undefined
|
||||
client.player.status // 'idle' | 'loading' | 'ready' | 'error'
|
||||
client.player.onChange((snapshot) => { /* fires immediately */ });
|
||||
await client.player.load();
|
||||
@@ -23,6 +24,7 @@ await client.player.reload(); // force refetch
|
||||
|
||||
```ts
|
||||
interface PlayerProfile {
|
||||
counters?: { [slug: string]: number }; // client-visible number counters
|
||||
identities?: PlayerProfileIdentity[];
|
||||
player?: Player;
|
||||
wallets?: Wallet[];
|
||||
@@ -51,9 +53,12 @@ interface Wallet {
|
||||
|
||||
## 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).
|
||||
- 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.
|
||||
- React: `useSyncExternalStore` maps directly onto `onChange` (see the SDK
|
||||
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
|
||||
project-wide shared (`ProjectStorageDomain`, revision key `projectStorage`).
|
||||
Sources: `src/domains/StorageDomain.ts`, `src/domains/ProjectStorageDomain.ts`.
|
||||
|
||||
## Player storage
|
||||
Per-player storage, one item per `type`. `StorageDomain`, revision key
|
||||
`storage`. Source: `src/domains/StorageDomain.ts`.
|
||||
|
||||
```ts
|
||||
client.storage.data // GetStorageResponse | undefined
|
||||
await client.storage.save(items);
|
||||
client.storage.value // GetStorageResponse | undefined
|
||||
await client.storage.save(type, data);
|
||||
await client.storage.delete(type);
|
||||
```
|
||||
|
||||
@@ -18,52 +15,21 @@ interface GetStorageResponse {
|
||||
nextCursor?: string;
|
||||
}
|
||||
interface StorageItem {
|
||||
data?: string; // opaque payload, JSON-stringify yourself if needed
|
||||
id?: string;
|
||||
type?: string; // the storage "collection" key
|
||||
data?: string; // opaque payload, JSON-stringify yourself
|
||||
id?: string; // server-assigned
|
||||
type?: string; // the storage key
|
||||
}
|
||||
```
|
||||
|
||||
- `save(items: StorageItem[]): Promise<void>` — upserts items, then
|
||||
invalidates the domain so subscribers refetch.
|
||||
- `delete(type: string): Promise<void>` — deletes **all** player storage items
|
||||
of that type, then 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.
|
||||
- `save(type: string, data: string): Promise<void>`: upserts the item of
|
||||
`type` (the server keeps one item per player and type), then invalidates.
|
||||
- `delete(type: string): Promise<void>`: deletes the item of `type`, then
|
||||
invalidates.
|
||||
|
||||
## Limits and notes
|
||||
|
||||
- Both domains load with `{ limit: 100 }` — the observable snapshot holds at
|
||||
most 100 items and `nextCursor` pagination is not surfaced by the domain.
|
||||
- `data` is a raw string on the wire; serialize/deserialize JSON yourself.
|
||||
- Both are observable (`SyncedState`) and warmed by the revision poll only when
|
||||
in use; mutations self-invalidate.
|
||||
- The state loads with `{ limit: 100 }`; `nextCursor` pagination is not
|
||||
surfaced.
|
||||
- `data` is a raw string on the wire; serialize and parse JSON yourself.
|
||||
- Project storage is not reachable from the client SDK. It stays available to
|
||||
modules, the dashboard and `/game/v1`.
|
||||
|
||||
Reference in New Issue
Block a user