@@ -0,0 +1,24 @@
|
||||
import { SyncedState } from '../state/SyncedState.js';
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import type { CatalogItem } from '../generated/catalog.js';
|
||||
|
||||
/**
|
||||
* CatalogDomain — the item catalog, keyed by slug. Synced under revision
|
||||
* key `catalog`. Feeds the merged {@link InventoryDomain} view.
|
||||
*/
|
||||
export class CatalogDomain extends SyncedState<Map<string, CatalogItem>> {
|
||||
readonly syncKey = 'catalog';
|
||||
|
||||
constructor(ctx: RudderContext) {
|
||||
super(async () => {
|
||||
const response = await api.listCatalogItems(ctx);
|
||||
const map = new Map<string, CatalogItem>();
|
||||
for (const item of response.items ?? []) {
|
||||
if (!item.slug) continue;
|
||||
map.set(item.slug, item);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { RemoteConfigState, type RemoteConfigShape } from '../state/RemoteConfigState.js';
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import type { RemoteConfig } from '../generated/remote-config.js';
|
||||
|
||||
/**
|
||||
* ConfigDomain — typed remote configuration as an observable entity. Synced
|
||||
* under revision key `config`. Extends {@link RemoteConfigState}, which adds
|
||||
* typed `get()` and scenario override support.
|
||||
*/
|
||||
export class ConfigDomain<
|
||||
TConfig extends RemoteConfigShape = RemoteConfigShape,
|
||||
> extends RemoteConfigState<TConfig> {
|
||||
readonly syncKey = 'config';
|
||||
|
||||
constructor(ctx: RudderContext) {
|
||||
super(async () => {
|
||||
const response = await api.listSdkRemoteConfigs(ctx);
|
||||
const map = new Map<string, RemoteConfig>();
|
||||
if (response.configs) {
|
||||
for (const [key, config] of Object.entries(response.configs)) {
|
||||
// The server already filters inactive configs out (the game cache skips
|
||||
// !active) and may omit `active` on the wire, so keep anything that is
|
||||
// not explicitly inactive instead of dropping configs that carry no flag.
|
||||
if (config && config.active !== false) {
|
||||
map.set(config.key ?? key, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { SyncedState } from '../state/SyncedState.js';
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import { mergeInventory, type InventoryItem } from '../state/inventory.js';
|
||||
import type { CatalogDomain } from './CatalogDomain.js';
|
||||
|
||||
/**
|
||||
* InventoryDomain — owned items merged with their catalog entries. Synced
|
||||
* under revision key `inventory`; also refreshes when the catalog changes.
|
||||
*/
|
||||
export class InventoryDomain extends SyncedState<InventoryItem[]> {
|
||||
readonly syncKey = 'inventory';
|
||||
|
||||
constructor(ctx: RudderContext, catalog: CatalogDomain) {
|
||||
super(async () => {
|
||||
const [response, catalogItems] = await Promise.all([
|
||||
api.getInventory(ctx),
|
||||
catalog.load(),
|
||||
]);
|
||||
return mergeInventory(response.items ?? [], catalogItems);
|
||||
});
|
||||
|
||||
// Catalog changes flow into the merged inventory view.
|
||||
let previousCatalog = catalog.data;
|
||||
catalog.onChange((snapshot) => {
|
||||
if (snapshot.data === previousCatalog) return;
|
||||
previousCatalog = snapshot.data;
|
||||
this.invalidate();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { SyncedState } from '../state/SyncedState.js';
|
||||
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 (identity + wallets).
|
||||
* Synced under revision key `profile`.
|
||||
*/
|
||||
export class PlayerDomain extends SyncedState<PlayerProfile> {
|
||||
readonly syncKey = 'profile';
|
||||
|
||||
constructor(ctx: RudderContext) {
|
||||
super(() => api.getPlayerInformation(ctx));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { SyncedState } from '../state/SyncedState.js';
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import type { GetStorageResponse, StorageItem } from '../generated/storage.js';
|
||||
|
||||
/**
|
||||
* StorageDomain — player key/value storage as an observable entity, plus its
|
||||
* mutations. Synced under revision key `storage`; mutations invalidate it so
|
||||
* subscribers observe fresh data.
|
||||
*/
|
||||
export class StorageDomain extends SyncedState<GetStorageResponse> {
|
||||
readonly syncKey = 'storage';
|
||||
|
||||
constructor(private readonly ctx: RudderContext) {
|
||||
super(() => api.getStorage(ctx, { limit: 100 }));
|
||||
}
|
||||
|
||||
/** Saves player storage items, then invalidates. */
|
||||
async save(items: StorageItem[]): Promise<void> {
|
||||
await api.updateStorage(this.ctx, { items });
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
/** Deletes player storage items of the given type, then invalidates. */
|
||||
async delete(type: string): Promise<void> {
|
||||
await api.deleteStorage(this.ctx, { type });
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SyncedState } from '../state/SyncedState.js';
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import { ShopHandle, type BuyOptions, type PurchaseOffer } from '../state/shops.js';
|
||||
import type { PurchaseOfferResponse } from '../generated/stores.js';
|
||||
|
||||
/**
|
||||
* StoresDomain — the observable list of stores, plus the purchase executor
|
||||
* that every offer/store handle shares. Synced under revision key `stores`.
|
||||
*
|
||||
* A successful purchase runs `onPurchase`, which the client wires to invalidate
|
||||
* the profile, inventory, and store list so subscribers observe fresh data.
|
||||
*/
|
||||
export class StoresDomain extends SyncedState<ShopHandle[]> {
|
||||
readonly syncKey = 'stores';
|
||||
|
||||
/** Shared purchase executor: `stores.purchase(slug, offerId, opts)`. */
|
||||
readonly purchase: PurchaseOffer;
|
||||
|
||||
private readonly ctx: RudderContext;
|
||||
|
||||
constructor(ctx: RudderContext, onPurchase: () => void) {
|
||||
// Built before super() so it captures params, not `this`; the store/offer
|
||||
// handles created in the loader all delegate to this one executor.
|
||||
const purchase: PurchaseOffer = async (
|
||||
storeSlug: string,
|
||||
offerId: string,
|
||||
options: BuyOptions = {},
|
||||
): Promise<PurchaseOfferResponse> => {
|
||||
const response = await api.purchaseOffer(
|
||||
ctx,
|
||||
{ storeSlug, offerId },
|
||||
{
|
||||
storeSlug,
|
||||
offerId,
|
||||
idempotencyKey: options.idempotencyKey ?? crypto.randomUUID(),
|
||||
},
|
||||
);
|
||||
onPurchase();
|
||||
return response;
|
||||
};
|
||||
|
||||
super(async () => {
|
||||
const response = await api.listSdkStores(ctx);
|
||||
return (response.stores ?? []).map((store) => new ShopHandle(purchase, store));
|
||||
});
|
||||
|
||||
this.ctx = ctx;
|
||||
this.purchase = purchase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a store by slug (cache-first). Used by scenario store nodes.
|
||||
* @internal
|
||||
*/
|
||||
async getBySlug(slug: string): Promise<ShopHandle> {
|
||||
const cached = this.data?.find((store) => store.slug === slug);
|
||||
if (cached) return cached;
|
||||
const store = await api.getStore(this.ctx, { slug });
|
||||
return new ShopHandle(this.purchase, store);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user