import { zombieMapBySlug, type ZombieGame } from './map-aliases.js'; export const autoscalingModes = ['off', 'create-destroy'] as const; export type AutoscalingMode = (typeof autoscalingModes)[number]; export interface ScalingSettings { game: ZombieGame; /** '*' applies the targets independently to every map. */ mapSlug: string; mode: AutoscalingMode; minimum: number; maximum: number; spares: number; emptyMinutes: number; } export interface ScalingPolicy extends ScalingSettings { id: string; revision: number; pausedReason: string | null; updatedAt: string; } export interface ScalingMember { name: string; mapSlug?: string; identity: string; state: 'occupied' | 'empty' | 'creating' | 'removing' | 'stopped' | 'unknown' | 'protected'; players: number | null; observedAt: number | null; source: string | null; emptySince: number | null; blocker: string | null; operationId: string | null; emptyStartedAt?: number | null; } export interface ScalingSnapshot { members: ScalingMember[]; reservedNames: string[]; keyPool: { total: number; available: number }; blockers: string[]; } export interface ScalingDecision { action: 'create' | 'destroy' | 'wait'; reason: string; server: string | null; desired: number; deficit: number; occupied: number; empty: number; pending: number; unknown: number; } export interface ScalingEvaluation { policy: ScalingPolicy; snapshot: ScalingSnapshot; decision: ScalingDecision; blockers: string[]; } export interface ScalingHistory { id: string; at: number; lastSeenAt: number; decision: ScalingDecision; operationId: string | null; operationState: string | null; } export interface ScalingView extends ScalingEvaluation { history: ScalingHistory[]; lastEvaluatedAt: number | null; totals: { observedMinutes: number; emptyLobbyHours: number; deficitMinutes: number; unknownMinutes: number; }; operations: { creates: number; removals: number; cancelled: number; failed: number; readinessSamples: number; readinessMeanSeconds: number | null; readinessMaxSeconds: number | null; }; ownsMap: boolean; unsettled: { id: string; action: string; state: string; stage: string }[]; } export interface ScalingReport { policies: ScalingView[]; globalPaused: boolean; liveAvailable: boolean; removalAvailable: boolean; } export const liveScaling = (mode: AutoscalingMode): boolean => mode === 'create-destroy'; export function scalingSettings(input: unknown): ScalingSettings { if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Provide an autoscaling policy.'); const value = input as Record; const { game, mapSlug } = value; if ( (game !== 't5' && game !== 't6') || typeof mapSlug !== 'string' || (mapSlug !== '*' && zombieMapBySlug(game, mapSlug)?.slug !== mapSlug) ) throw new Error('Choose a supported game and map variant, or all maps.'); const mode = value.mode ?? 'off'; if (!autoscalingModes.includes(mode as AutoscalingMode)) throw new Error('Choose a valid autoscaling mode.'); const integer = (key: string, fallback: number | undefined, max: number) => { const n = value[key] ?? fallback; if (typeof n !== 'number' || !Number.isInteger(n) || n < 1 || n > max) throw new Error(`${key} must be an integer from 1 to ${max}.`); return n; }; const minimum = integer('minimum', 1, 20), maximum = integer('maximum', undefined, 20), spares = integer('spares', 1, 20); if (minimum > maximum || spares > maximum) throw new Error('Minimum and spare target cannot exceed the maximum.'); return { game, mapSlug, mode: mode as AutoscalingMode, minimum, maximum, spares, emptyMinutes: integer('emptyMinutes', 15, 1440) }; }