Browser; server clients
Player stats and aggregates
@silicon-jungle/inkwell-sdk/statsTyped account values, increments, average rates, validation policy and aggregate history.
Exact source · npm package · Reference version 0.0.8
API at a glance
stats.get(name, { username? }?), list({ username?, offset? }?)stats.schema({ name?, offset? }?): Promise<{ stats: GameStatSchema[]; nextOffset: number | null }>stats.set(name, value, { requestId? }?)stats.increment(name, amount?, { requestId? }?)stats.updateAverage(name, count, seconds, { requestId? }?)stats.reset({ achievements? }?)stats.aggregate({ historyDays?, offset? }?)stats.onChange(listener): unsubscribe; onStatChange(listener)context.stats.definitions(offset?), define(definition), update(definition)context.stats.forPlayer(username)context.stats.batchFor(username, changes, { requestId?, epoch? }?)context.stats.forPlayer(username).batch(changes, options?)createStats(request?), createServerStats(request)
Definitions choose int, float or avgrate, defaults, bounds, maxChange, increment-only, public-read, server-write, aggregation and rate-window policies.
schema exposes current-game definition metadata to browsers and backends, including signed-out guests with valid game access. It never returns saved values or management rights. Results are name-sorted, at most 100 per page, with optional exact-name selection before pagination. publicRead governs other-player values, not schema access.
Backend batches atomically apply 1–100 unique stat/achievement changes for one player, including linked awards and aggregate deltas. Explicit clears override auto-awards and advance the save epoch once. Keep a stable requestId; duplicate receipts return original results without repeating writes or notices, even after a later reset. Batches are never browser/offline operations.
Supply a stable requestId to retry the same logical increment or average update; generating a fresh ID means a new write. Average durations must be finite and positive.
reset is destructive for authorized player data and can also clear achievements. It is not a reset of all players or the aggregate’s history.
Cached reads can include offline, cachedAt and pendingWrites. Queued writes are not confirmed awards; inspect their discriminant.
onChange is a browser-only hint after backend writes/reset or reconnect, with id, kind (updated/reset/refresh) and names, never authoritative values. The host invalidates confirmed cached reads before forwarding it. Re-read and unsubscribe on teardown; callbacks are not a reliable event log.
Aggregate responses include totalExact and history deltaExact strings for precision-sensitive use; numeric total/delta values can be approximate. Aggregated maxChange caps each signed global contribution independently from the player value. Nonaggregated maxChange rejects an excessive player-value change.
All exported symbols
AggregateGameStatAggregateGameStatQueryGameStatGameStatChangeGameStatDefinitionGameStatSchemaProgressBatchProgressBatchOptionsProgressBatchResultStatUpdatecreateServerStatscreateStatsonStatChangestats
Exact TypeScript API
Generated from the SDK source, including input options, return values, public types and overloads. Relative imports below are links between SDK source modules, not additional package subpaths. Only the entrypoints listed in the reference index are supported import paths.
import { type GameServiceRequest } from "./game-services.js";
import type { CachedGameRead, QueuedGameWrite } from './offline.js';
export type GameStatChange = {
id: string;
kind: 'updated' | 'reset' | 'refresh';
names: string[];
};
/** Browser invalidation hint, not a persisted value or a reliable event log. */
export declare function onStatChange(listener: (change: GameStatChange) => void): () => void;
export type GameStatDefinition = {
name: string;
title?: string;
kind?: "int" | "float" | "avgrate";
defaultValue?: number;
minValue?: number;
maxValue?: number;
maxChange?: number | null;
incrementOnly?: boolean;
serverWritesOnly?: boolean;
publicRead?: boolean;
aggregated?: boolean;
windowSeconds?: number;
};
export type GameStat = {
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
};
/** Read-only definition metadata. publicRead governs values, not this schema. */
export type GameStatSchema = Required<Omit<GameStatDefinition, 'windowSeconds'>> & {
windowSeconds: number | null;
};
export type StatUpdate = {
name: string;
value: number;
unlocked: string[];
queued?: false;
};
export type AggregateGameStat = {
name: string;
/** Approximate JavaScript number. Use totalExact when precision matters. */
total: number;
/** Exact sum of recorded player contributions, capped per upload by maxChange. */
totalExact: string;
/** UTC days, today first, including days with no activity. */
history: {
day: string;
/** Approximate JavaScript number. */
delta: number;
/** Exact decimal change for this UTC day. */
deltaExact: string;
}[];
};
/** UTC date ranges are inclusive and at most60 days; omit history for totals only. */
export type AggregateGameStatQuery = {
names?: string[];
offset?: number;
} & ({
historyDays?: number;
startDate?: never;
endDate?: never;
} | {
startDate: string;
endDate: string;
historyDays?: never;
});
type RetryOptions = {
requestId?: string;
};
export type ProgressBatch = {
stats?: ({
name: string;
value: number;
mode?: 'set' | 'increment';
} | {
name: string;
value: number;
mode: 'average';
seconds: number;
})[];
achievements?: {
name: string;
unlocked: boolean;
}[];
};
export type ProgressBatchResult = {
stats: {
name: string;
value: number;
}[];
/** Newly unlocked names at the original commit; repeated receipts retain this list. */
unlocked: string[];
cleared: string[];
duplicate: boolean;
};
export type ProgressBatchOptions = RetryOptions & {
epoch?: number;
};
export declare function createStats(request?: GameServiceRequest): Readonly<{
onChange: typeof onStatChange;
schema: (options?: {
name?: string;
offset?: number;
}) => Promise<{
stats: GameStatSchema[];
nextOffset: number | null;
}>;
get(name: string, options?: {
username?: string;
}): Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
} | null>;
reset: (options?: {
achievements?: boolean;
}) => Promise<{
statsCleared: number;
achievementsCleared: number;
}>;
list: (options?: {
username?: string;
offset?: number;
}) => Promise<{
stats: GameStat[];
nextOffset: number | null;
} & CachedGameRead>;
set: (name: string, value: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
increment: (name: string, amount?: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
updateAverage: (name: string, count: number, seconds: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
aggregate: (options?: AggregateGameStatQuery) => Promise<{
stats: AggregateGameStat[];
nextOffset: number | null;
}>;
}>;
export declare function createServerStats(request: GameServiceRequest): Readonly<{
batchFor: (username: string, changes: ProgressBatch, options?: ProgressBatchOptions) => Promise<ProgressBatchResult>;
definitions: (offset?: number) => Promise<{
stats: GameStatDefinition[];
nextOffset: number | null;
}>;
define: (definition: GameStatDefinition) => Promise<{
stat: GameStatDefinition;
}>;
update: (definition: GameStatDefinition) => Promise<{
stat: GameStatDefinition;
}>;
forPlayer: (username: string) => Readonly<{
batch: (changes: ProgressBatch, options?: ProgressBatchOptions) => Promise<ProgressBatchResult>;
onChange: typeof onStatChange;
schema: (options?: {
name?: string;
offset?: number;
}) => Promise<{
stats: GameStatSchema[];
nextOffset: number | null;
}>;
get: (name: string, options?: {
username?: string;
}) => Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
} | null>;
reset: (options?: {
achievements?: boolean;
}) => Promise<{
statsCleared: number;
achievementsCleared: number;
}>;
list: (options?: {
username?: string;
offset?: number;
}) => Promise<{
stats: GameStat[];
nextOffset: number | null;
} & CachedGameRead>;
set: (name: string, value: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
increment: (name: string, amount?: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
updateAverage: (name: string, count: number, seconds: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
aggregate: (options?: AggregateGameStatQuery) => Promise<{
stats: AggregateGameStat[];
nextOffset: number | null;
}>;
}>;
onChange: typeof onStatChange;
schema: (options?: {
name?: string;
offset?: number;
}) => Promise<{
stats: GameStatSchema[];
nextOffset: number | null;
}>;
get: (name: string, options?: {
username?: string;
}) => Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
} | null>;
reset: (options?: {
achievements?: boolean;
}) => Promise<{
statsCleared: number;
achievementsCleared: number;
}>;
list: (options?: {
username?: string;
offset?: number;
}) => Promise<{
stats: GameStat[];
nextOffset: number | null;
} & CachedGameRead>;
set: (name: string, value: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
increment: (name: string, amount?: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
updateAverage: (name: string, count: number, seconds: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
aggregate: (options?: AggregateGameStatQuery) => Promise<{
stats: AggregateGameStat[];
nextOffset: number | null;
}>;
}>;
export declare const stats: Readonly<{
onChange: typeof onStatChange;
schema: (options?: {
name?: string;
offset?: number;
}) => Promise<{
stats: GameStatSchema[];
nextOffset: number | null;
}>;
get(name: string, options?: {
username?: string;
}): Promise<{
offline: boolean | undefined;
cachedAt: number | undefined;
pendingWrites: number | undefined;
name: string;
title: string;
kind: "int" | "float" | "avgrate";
value: number;
updatedAt: string | null;
} | null>;
reset: (options?: {
achievements?: boolean;
}) => Promise<{
statsCleared: number;
achievementsCleared: number;
}>;
list: (options?: {
username?: string;
offset?: number;
}) => Promise<{
stats: GameStat[];
nextOffset: number | null;
} & CachedGameRead>;
set: (name: string, value: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
increment: (name: string, amount?: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
updateAverage: (name: string, count: number, seconds: number, options?: RetryOptions) => Promise<QueuedGameWrite | StatUpdate>;
aggregate: (options?: AggregateGameStatQuery) => Promise<{
stats: AggregateGameStat[];
nextOffset: number | null;
}>;
}>;
export {};
Supporting internal type declarations
These clarify types referenced by the generated signatures; do not import these internal paths directly.
game-services.d.ts
export declare class GameServiceError extends Error {
readonly status: number;
readonly code: string;
constructor(message: string, status?: number, code?: string);
}
export type GameServiceRequest = <T>(service: string, request: Record<string, unknown>) => Promise<T>;
export declare const requestGameService: GameServiceRequest;
/** Server-only adapter. Credentials must never be bundled into browser games. */
export declare function createGameServiceRequest(options: {
baseUrl: string;
token: string;
fetch?: typeof fetch;
}): GameServiceRequest;
protocol.d.ts
export declare const SDK_SOURCE: "inkwell-sdk";
export declare const SDK_VERSION: 1;
export type InkwellMessageType = "ready" | "loading.progress" | "loading.error" | "session.complete" | "analytics.track" | "assets.progress" | "player.get" | "presence.get" | "performance.sample" | "backend.connect" | "backend.fetch";
export type InkwellMessage = {
source: typeof SDK_SOURCE;
version: typeof SDK_VERSION;
type: InkwellMessageType;
payload?: Record<string, unknown>;
sentAt: number;
};
export declare function parentOrigin(): string | null;
export declare function requestId(): string;
export declare function emit(type: InkwellMessageType, payload?: Record<string, unknown>): boolean;