feat: modules and resources domains, drop removed domains, regenerated models

This commit is contained in:
edmand46
2026-09-17 12:08:03 +03:00
parent f45261439f
commit 6f37b00fb4
71 changed files with 516 additions and 4184 deletions
-54
View File
@@ -1,54 +0,0 @@
import type { RemoteConfig } from '../generated/remote-config.js';
import { SyncedState } from './SyncedState.js';
export type RemoteConfigShape = Record<string, unknown>;
type RemoteConfigKey<TConfig extends RemoteConfigShape> = Extract<keyof TConfig, string>;
/**
* RemoteConfigState — typed remote configuration as an observable entity.
*
* `get()` parses values synchronously from the loaded snapshot based on their
* declared `valueType` and returns the default value while not loaded.
*/
export class RemoteConfigState<
TConfig extends RemoteConfigShape = RemoteConfigShape,
> extends SyncedState<Map<string, RemoteConfig>> {
get<TKey extends RemoteConfigKey<TConfig>>(key: TKey): TConfig[TKey] | undefined;
get<TKey extends RemoteConfigKey<TConfig>>(key: TKey, defaultValue: TConfig[TKey]): TConfig[TKey];
get<TKey extends RemoteConfigKey<TConfig>>(
key: TKey,
defaultValue?: TConfig[TKey],
): TConfig[TKey] | undefined {
const config = this.data?.get(key);
if (!config || config.value == null) return defaultValue;
return parseValue(config.value, config.valueType, defaultValue);
}
}
function parseValue<T>(
value: string,
valueType: string | undefined,
defaultValue: T | undefined,
): T | undefined {
try {
switch ((valueType ?? '').toLowerCase()) {
case 'int':
case 'integer':
return Number.parseInt(value, 10) as unknown as T;
case 'float':
case 'double':
case 'number':
return Number.parseFloat(value) as unknown as T;
case 'bool':
case 'boolean':
return (value === 'true') as unknown as T;
case 'json':
case 'object':
return JSON.parse(value) as T;
default:
return value as unknown as T;
}
} catch {
return defaultValue;
}
}
-29
View File
@@ -1,29 +0,0 @@
import type { CatalogItem } from '../generated/catalog.js';
import type { PlayerInventoryItem } from '../generated/inventory.js';
/** A fully-resolved owned item: player inventory row merged with its catalog entry. */
export interface InventoryItem {
slug: string;
amount: number;
name: string;
properties: Record<string, unknown>;
tags: string[];
}
/** @internal Merges owned inventory rows with their catalog entries. */
export function mergeInventory(
owned: PlayerInventoryItem[],
catalog: Map<string, CatalogItem>,
): InventoryItem[] {
return owned.map((item) => {
const slug = item.slug ?? '';
const entry = catalog.get(slug);
return {
slug,
amount: item.amount ?? 0,
name: item.nameOverride || entry?.name || slug,
properties: { ...(entry?.properties ?? {}), ...(item.propertiesOverride ?? {}) },
tags: entry?.tags ?? [],
};
});
}
-77
View File
@@ -1,77 +0,0 @@
import type {
Offer,
OfferContent,
OfferPrice,
Store,
PurchaseOfferResponse,
} from '../generated/stores.js';
export interface BuyOptions {
idempotencyKey?: string;
}
/**
* Wire-level purchase executor provided by the stores domain
* (`StoresDomain.purchase`).
*/
export type PurchaseOffer = (
storeSlug: string,
offerSlug: string,
options?: BuyOptions,
) => Promise<PurchaseOfferResponse>;
export class OfferHandle {
public readonly id: string;
public readonly slug: string;
public readonly name?: string;
public readonly price?: OfferPrice;
public readonly contents: OfferContent[];
public readonly maxPurchases?: number;
constructor(
private readonly purchase: PurchaseOffer,
private readonly storeSlug: string,
private readonly offer: Offer,
) {
if (!offer.id) {
throw new Error('Rudder Stores: offer.id is required');
}
if (!offer.slug) {
throw new Error('Rudder Stores: offer.slug is required');
}
this.id = offer.id;
this.slug = offer.slug;
this.name = offer.name;
this.price = offer.price;
this.contents = offer.contents ?? [];
this.maxPurchases = offer.maxPurchases;
}
async buy(options: BuyOptions = {}): Promise<PurchaseOfferResponse> {
return this.purchase(this.storeSlug, this.slug, options);
}
}
export class ShopHandle {
public readonly slug: string;
public readonly name?: string;
public readonly description?: string;
public readonly data?: { [key: string]: unknown };
public readonly offers: OfferHandle[];
constructor(
purchase: PurchaseOffer,
store: Store,
) {
if (!store.slug) {
throw new Error('Rudder Stores: store.slug is required');
}
this.slug = store.slug;
this.name = store.name;
this.description = store.description;
this.data = store.data;
this.offers = (store.offers ?? []).map(
(offer) => new OfferHandle(purchase, this.slug, offer),
);
}
}