129 lines
4.0 KiB
TypeScript
129 lines
4.0 KiB
TypeScript
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<string, unknown>;
|
|
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)
|
|
};
|
|
}
|