Files
electric-cherry/src/lib/afk.ts
T
2026-10-17 17:52:03 -04:00

144 lines
4.3 KiB
TypeScript

export const afkPageSize = 25;
export const afkLedgerKinds = [
'free_grant',
'purchase',
'reward',
'reserve',
'start',
'release',
'unresolved',
'consume',
'correction'
] as const;
export type AfkLedgerKind = (typeof afkLedgerKinds)[number];
export type AfkSessionState = 'reserved' | 'started' | 'settled' | 'unresolved' | 'corrected';
export interface AfkQuery {
view: 'accounts' | 'sessions' | 'ledger';
q: string;
balance: '' | 'saved' | 'reserved' | 'unresolved';
sort: 'saved' | 'available' | 'recent';
game: '' | 't5' | 't6';
server: string;
state: '' | AfkSessionState;
kind: '' | AfkLedgerKind;
from: string;
to: string;
offset: number;
player: string;
}
export function readAfkQuery(params: URLSearchParams): AfkQuery {
function choice<T extends string>(key: string, values: readonly T[], fallback: T): T {
const value = params.get(key) || fallback;
if (!values.includes(value as T)) throw new Error(`Invalid ${key} filter.`);
return value as T;
}
function text(key: string, max: number) {
const value = (params.get(key) ?? '').trim();
if (value.length > max) throw new Error(`${key} is too long.`);
return value;
}
function date(key: string) {
const value = text(key, 10);
if (
value &&
(!/^\d{4}-\d{2}-\d{2}$/.test(value) ||
!Number.isFinite(Date.parse(value)) ||
new Date(value).toISOString().slice(0, 10) !== value)
)
throw new Error('Use a valid date in YYYY-MM-DD format.');
return value;
}
const offset = Number(params.get('offset') || 0);
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error('Invalid page offset.');
const from = date('from');
const to = date('to');
if (from && to && from > to) throw new Error('The end date must follow the start date.');
return {
view: choice('view', ['accounts', 'sessions', 'ledger'], 'accounts'),
q: text('q', 100),
balance: choice('balance', ['', 'saved', 'reserved', 'unresolved'], ''),
sort: choice('sort', ['saved', 'available', 'recent'], 'saved'),
game: choice('game', ['', 't5', 't6'], ''),
server: text('server', 256),
state: choice(
'state',
['', 'reserved', 'started', 'settled', 'unresolved', 'corrected'],
''
),
kind: choice('kind', ['', ...afkLedgerKinds], ''),
from,
to,
offset: Math.floor(offset / afkPageSize) * afkPageSize,
player: text('player', 128)
};
}
export interface AfkIdentity {
guid: string;
name: string | null;
}
export interface AfkAccount extends AfkIdentity {
freeSeconds: number;
savedSeconds: number;
availableSeconds: number;
reservedSeconds: number;
reservedSavedSeconds: number;
unresolvedSessions: number;
lastActivity: string | null;
freeRenewsAt: string | null;
}
export interface AfkSession extends AfkIdentity {
sessionId: string;
server: string;
game: string | null;
matchId: number | null;
matchAvailable: number;
state: AfkSessionState;
authorizedSeconds: number;
consumedSeconds: number | null;
chargedSeconds: number | null;
confirmedSeconds: number;
endReason: string | null;
createdAt: string;
startedAt: string | null;
endedAt: string | null;
}
export interface AfkLedgerEntry extends AfkIdentity {
id: number;
server: string;
game: string | null;
matchId: number | null;
matchAvailable: number;
kind: AfkLedgerKind;
freeDelta: number;
savedDelta: number;
freeSeconds: number;
savedSeconds: number;
operationId: string;
sessionId: string | null;
eventAt: string;
createdAt: string;
}
export interface AfkPage<T> {
entries: T[];
total: number;
offset: number;
}
export interface AfkOverview {
query: AfkQuery;
checkedAt: string;
capacitySeconds: number;
summary: {
accounts: number;
savedSeconds: number;
reservedSeconds: number;
unresolvedSessions: number;
};
servers: string[];
accounts: AfkPage<AfkAccount>;
sessions: AfkPage<AfkSession>;
ledger: AfkPage<AfkLedgerEntry>;
}