2.0.0: changelog and skill docs for slugs and environments
Claude-Session: https://claude.ai/code/session_01SMCvdwDmuxoaqGgvGBLk1V
This commit is contained in:
@@ -1,5 +1,38 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
Breaking change — major bump, 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()`.
|
||||||
|
|
||||||
|
Quests, scenarios and offers are addressed by their slug instead of their id,
|
||||||
|
because ids differ between staging and prod while slugs are stable. Renamed
|
||||||
|
accordingly: `Quest.id` is now `Quest.slug`, `client.quests.claim()` takes a
|
||||||
|
quest slug, `reportProgress()` resolves to the slugs of the completed quests
|
||||||
|
(`ReportQuestProgressResponse.completedQuestSlugs`),
|
||||||
|
`client.stores.purchase(storeSlug, offerSlug, options?)` takes an offer slug
|
||||||
|
and `OfferHandle` carries a `slug` alongside its `id`,
|
||||||
|
`QuestMetrics.purchaseOffer(offerSlug)` builds its metric from the offer slug,
|
||||||
|
and every scenario-scoped type exposes `scenarioSlug` instead of `scenarioId` —
|
||||||
|
`PendingEffect`, `ScenarioCompletedEffect`, `ScenarioFailedEffect`, the battle
|
||||||
|
pass requests and `client.battlePass.getProgress(scenarioSlug, nodeId)`.
|
||||||
|
`offer.buy()` is unchanged; it now binds the slug for you. Leaderboards, items
|
||||||
|
and stores already used slugs and are untouched.
|
||||||
|
|
||||||
|
Also shipped here, previously committed but never published: the effects client
|
||||||
|
reconciles against every `GET /sdk/v1/scenarios/pending` response, so a run
|
||||||
|
that disappears server-side (finished elsewhere, expired after a promote) now
|
||||||
|
emits `onScenarioCompleted` instead of lingering; and `run_not_active` joins
|
||||||
|
`unknown_run` and `run_expired` as a terminal rejection that drops the run and
|
||||||
|
emits `onScenarioFailed`.
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
- Scenario execution moved server-side. The SDK no longer walks a local DAG
|
- Scenario execution moved server-side. The SDK no longer walks a local DAG
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@rudder/js-sdk",
|
"name": "@rudder/js-sdk",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@rudder/js-sdk",
|
"name": "@rudder/js-sdk",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rudder/js-sdk",
|
"name": "@rudder/js-sdk",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://hub.rudder.build/api/packages/rudder/npm/"
|
"registry": "https://hub.rudder.build/api/packages/rudder/npm/"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ service, or standalone economy service in this SDK — wallets live on
|
|||||||
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
|
| Player profile + wallets | `client.player` | observable `SyncedState<PlayerProfile>` |
|
||||||
| Inventory (catalog-merged) | `client.inventory` | observable `SyncedState<InventoryItem[]>` |
|
| Inventory (catalog-merged) | `client.inventory` | observable `SyncedState<InventoryItem[]>` |
|
||||||
| Catalog | `client.catalog` | observable `SyncedState<Map<string, CatalogItem>>` |
|
| Catalog | `client.catalog` | observable `SyncedState<Map<string, CatalogItem>>` |
|
||||||
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerId, opts?)` | observable `SyncedState<ShopHandle[]>` |
|
| Stores / purchases | `client.stores` + `client.stores.purchase(slug, offerSlug, opts?)` | observable `SyncedState<ShopHandle[]>` |
|
||||||
| Remote config | `client.remoteConfig` + `.get(key, default?)` | observable, typed |
|
| Remote config | `client.remoteConfig` + `.get(key, default?)` | observable, typed |
|
||||||
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations |
|
| Player storage | `client.storage` + `.save(items)` / `.delete(type)` | observable + mutations |
|
||||||
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation |
|
| Project storage | `client.projectStorage` + `.save(items)` | observable + mutation |
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ On success both tokens are saved to the client's `TokenStore`, the runtime
|
|||||||
starts (domains warmed, scenario engine restored, `player_login` event fired),
|
starts (domains warmed, scenario engine restored, `player_login` event fired),
|
||||||
and the state flips to `'signed-in'`.
|
and the state flips to `'signed-in'`.
|
||||||
|
|
||||||
|
## Environments
|
||||||
|
|
||||||
|
A project has two environments, `staging` and `prod`. The SDK key you pass as
|
||||||
|
`RudderClientOptions.projectKey` belongs to one of them, so the environment is
|
||||||
|
resolved at login and carried inside the access and refresh tokens; nothing in
|
||||||
|
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
|
## Auth state
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
`BattlePassService` (source: `src/battlepass/BattlePassService.ts`).
|
`BattlePassService` (source: `src/battlepass/BattlePassService.ts`).
|
||||||
Call-and-response access to battle pass endpoints. Battle pass state is tied to
|
Call-and-response access to battle pass endpoints. Battle pass state is tied to
|
||||||
a **scenario battle pass node**, so calls carry `scenarioId` + `nodeId` (plus
|
a **scenario battle pass node**, so calls carry `scenarioSlug` + `nodeId` (plus
|
||||||
`runId` for mutating calls). NOT observable — re-fetch progress explicitly
|
`runId` for mutating calls). NOT observable — re-fetch progress explicitly
|
||||||
after a mutation.
|
after a mutation.
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ you already know the scenario/node/run identifiers.
|
|||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
getProgress(scenarioId: string, nodeId: string): Promise<GetBattlePassProgressResponse>
|
getProgress(scenarioSlug: string, nodeId: string): Promise<GetBattlePassProgressResponse>
|
||||||
addXp(request: AddBattlePassXpRequest): Promise<AddBattlePassXpResponse>
|
addXp(request: AddBattlePassXpRequest): Promise<AddBattlePassXpResponse>
|
||||||
claimReward(request: ClaimBattlePassRewardRequest): Promise<ClaimBattlePassRewardResponse>
|
claimReward(request: ClaimBattlePassRewardRequest): Promise<ClaimBattlePassRewardResponse>
|
||||||
purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise<PurchaseBattlePassPremiumResponse>
|
purchasePremium(request: PurchaseBattlePassPremiumRequest): Promise<PurchaseBattlePassPremiumResponse>
|
||||||
@@ -27,7 +27,7 @@ interface AddBattlePassXpRequest {
|
|||||||
amount?: number;
|
amount?: number;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
scenarioId?: string;
|
scenarioSlug?: string;
|
||||||
source?: string; // configured XP source
|
source?: string; // configured XP source
|
||||||
}
|
}
|
||||||
interface AddBattlePassXpResponse {
|
interface AddBattlePassXpResponse {
|
||||||
@@ -41,7 +41,7 @@ interface ClaimBattlePassRewardRequest {
|
|||||||
level?: number;
|
level?: number;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
scenarioId?: string;
|
scenarioSlug?: string;
|
||||||
track?: 'free' | 'premium';
|
track?: 'free' | 'premium';
|
||||||
}
|
}
|
||||||
interface ClaimBattlePassRewardResponse {
|
interface ClaimBattlePassRewardResponse {
|
||||||
@@ -62,7 +62,7 @@ interface PurchaseBattlePassPremiumRequest {
|
|||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
scenarioId?: string;
|
scenarioSlug?: string;
|
||||||
}
|
}
|
||||||
interface PurchaseBattlePassPremiumResponse {
|
interface PurchaseBattlePassPremiumResponse {
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -80,5 +80,5 @@ interface PurchaseBattlePassPremiumResponse {
|
|||||||
in the response and the typed error `code` (`RudderErrorCodes`).
|
in the response and the typed error `code` (`RudderErrorCodes`).
|
||||||
- `purchasePremium` charges the player's wallet; idempotent; pass your own
|
- `purchasePremium` charges the player's wallet; idempotent; pass your own
|
||||||
`idempotencyKey` for safe retries.
|
`idempotencyKey` for safe retries.
|
||||||
- Prefer the `onBattlePass` effect session, which binds `scenarioId` /
|
- Prefer the `onBattlePass` effect session, which binds `scenarioSlug` /
|
||||||
`nodeId` / `runId` and posts scenario callbacks for you.
|
`nodeId` / `runId` and posts scenario callbacks for you.
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ Distinct from scenario quest nodes, which advance through
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
list(): Promise<Quest[]>
|
list(): Promise<Quest[]>
|
||||||
claim(questId: string): Promise<ClaimQuestResponse>
|
claim(questSlug: string): Promise<ClaimQuestResponse>
|
||||||
reportProgress(metric: string, amount: number): Promise<string[]> // ids of quests completed by this report
|
reportProgress(metric: string, amount: number): Promise<string[]> // slugs of quests completed by this report
|
||||||
```
|
```
|
||||||
|
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface Quest {
|
interface Quest {
|
||||||
id?: string;
|
slug?: string; // stable across environments — what claim() takes
|
||||||
name?: string;
|
name?: string;
|
||||||
objectives?: QuestObjectiveProgress[];
|
objectives?: QuestObjectiveProgress[];
|
||||||
rewards?: Reward[];
|
rewards?: Reward[];
|
||||||
@@ -47,12 +47,12 @@ interface ClaimQuestResponse {
|
|||||||
```ts
|
```ts
|
||||||
const quests = await client.quests.list();
|
const quests = await client.quests.list();
|
||||||
for (const quest of quests) {
|
for (const quest of quests) {
|
||||||
if (quest.status === 'completed' && quest.id) {
|
if (quest.status === 'completed' && quest.slug) {
|
||||||
const res = await client.quests.claim(quest.id);
|
const res = await client.quests.claim(quest.slug);
|
||||||
// res.success / res.alreadyClaimed / res.granted
|
// res.success / res.alreadyClaimed / res.granted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const completedIds = await client.quests.reportProgress('kills', 1);
|
const completedSlugs = await client.quests.reportProgress('kills', 1);
|
||||||
```
|
```
|
||||||
|
|
||||||
- `claim` is idempotent server-side; check `success` / `alreadyClaimed` /
|
- `claim` is idempotent server-side; check `success` / `alreadyClaimed` /
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ counter response may carry the next `PendingEffect`.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These wrap `client.battlePass` with the run's `scenarioId`/`nodeId`/`runId`
|
These wrap `client.battlePass` with the run's `scenarioSlug`/`nodeId`/`runId`
|
||||||
already bound — prefer them over calling the service manually.
|
already bound — prefer them over calling the service manually.
|
||||||
|
|
||||||
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
|
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
|
||||||
@@ -134,9 +134,9 @@ already bound — prefer them over calling the service manually.
|
|||||||
### Run lifecycle effects
|
### Run lifecycle effects
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioId: string }
|
interface ScenarioCompletedEffect { readonly runId: string; readonly scenarioSlug: string }
|
||||||
interface ScenarioFailedEffect {
|
interface ScenarioFailedEffect {
|
||||||
readonly runId: string; readonly scenarioId: string; readonly nodeId: string;
|
readonly runId: string; readonly scenarioSlug: string; readonly nodeId: string;
|
||||||
readonly error: Error;
|
readonly error: Error;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -154,7 +154,7 @@ not delivered to the client.
|
|||||||
## Reliability notes
|
## Reliability notes
|
||||||
|
|
||||||
- Completion methods POST `/sdk/v1/scenarios/callback` with
|
- Completion methods POST `/sdk/v1/scenarios/callback` with
|
||||||
`{scenarioId, runId, nodeId, handle}` using the handles `output`,
|
`{scenarioSlug, runId, nodeId, handle}` using the handles `output`,
|
||||||
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
|
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
|
||||||
`onPremiumPurchase`.
|
`onPremiumPurchase`.
|
||||||
- Transient callback failures (network error, 5xx) leave the effect active
|
- Transient callback failures (network error, 5xx) leave the effect active
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ client.stores.onChange(cb);
|
|||||||
await client.stores.load() / .reload();
|
await client.stores.load() / .reload();
|
||||||
|
|
||||||
// Direct purchase (bypassing handles):
|
// Direct purchase (bypassing handles):
|
||||||
await client.stores.purchase(storeSlug, offerId, options?);
|
await client.stores.purchase(storeSlug, offerSlug, options?);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Handles
|
## Handles
|
||||||
@@ -28,7 +28,8 @@ class ShopHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OfferHandle {
|
class OfferHandle {
|
||||||
readonly id: string;
|
readonly id: string; // authoring id, differs between staging and prod
|
||||||
|
readonly slug: string; // stable across environments — what purchases use
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
readonly price?: OfferPrice; // { amount?: number; currency?: string }
|
readonly price?: OfferPrice; // { amount?: number; currency?: string }
|
||||||
readonly contents: OfferContent[]; // { amount?: number; itemId?: string }[]
|
readonly contents: OfferContent[]; // { amount?: number; itemId?: string }[]
|
||||||
@@ -48,15 +49,15 @@ interface PurchaseOfferResponse {
|
|||||||
|
|
||||||
## Purchase semantics
|
## Purchase semantics
|
||||||
|
|
||||||
- `stores.purchase(storeSlug, offerId, options?)` and `offer.buy(options?)` are
|
- `stores.purchase(storeSlug, offerSlug, options?)` and `offer.buy(options?)` are
|
||||||
the same executor; handles just bind the slug/id.
|
the same executor; handles just bind the store slug and the offer slug.
|
||||||
- When `options.idempotencyKey` is omitted the SDK generates
|
- When `options.idempotencyKey` is omitted the SDK generates
|
||||||
`crypto.randomUUID()` per call — safe retries require passing your own key.
|
`crypto.randomUUID()` per call — safe retries require passing your own key.
|
||||||
- On success the executor invalidates `player`, `inventory`, and `stores`, so
|
- On success the executor invalidates `player`, `inventory`, and `stores`, so
|
||||||
subscribers observe fresh wallet/inventory/store data immediately.
|
subscribers observe fresh wallet/inventory/store data immediately.
|
||||||
- Check `response.success` / `response.error` — a failed purchase is a resolved
|
- Check `response.success` / `response.error` — a failed purchase is a resolved
|
||||||
response, not necessarily a thrown error.
|
response, not necessarily a thrown error.
|
||||||
- Purchase metrics (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) are
|
- Purchase metrics (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) are
|
||||||
reported to quests automatically server-side — do not report them manually
|
reported to quests automatically server-side — do not report them manually
|
||||||
(see `reference/quests.md`).
|
(see `reference/quests.md`).
|
||||||
|
|
||||||
@@ -71,10 +72,11 @@ interface Store {
|
|||||||
}
|
}
|
||||||
interface Offer {
|
interface Offer {
|
||||||
contents?: OfferContent[]; createdAt?: string; id?: string;
|
contents?: OfferContent[]; createdAt?: string; id?: string;
|
||||||
maxPurchases?: number; name?: string; price?: OfferPrice; updatedAt?: string;
|
maxPurchases?: number; name?: string; price?: OfferPrice; slug?: string;
|
||||||
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
|
`ShopHandle`/`OfferHandle` throw a plain `Error` if constructed from a store
|
||||||
without `slug` / an offer without `id` — in practice the domain only builds
|
without `slug` / an offer without `id` or `slug` — in practice the domain only
|
||||||
handles from server data that has both.
|
builds handles from server data that has all of them.
|
||||||
|
|||||||
Reference in New Issue
Block a user