import { db } from '../db/index.js'; import { getServer } from '../fleet/servers.js'; import { liveScaling, type ScalingPolicy } from '../../autoscaling.js'; import { mapIdentity } from './snapshot.js'; function owningPolicy(game: string, mapSlug: string) { return db() .prepare( "SELECT id,mode,paused_reason FROM autoscaling_policies WHERE game=? AND (map_slug=? OR map_slug='*' OR ?='*') AND owns_map=1" ) .get(game, mapSlug, mapSlug) as { id: string; mode: ScalingPolicy['mode']; paused_reason: string | null } | undefined; } /** Durable ownership prevents overlapping policies even while scaling is paused. */ export function scalingOwner(game: string, mapSlug: string): string | null { return owningPolicy(game, mapSlug)?.id ?? null; } const ownershipMessage = (owner: string) => `Autoscaling manages this map. Pause its policy before changing the lineup: /autoscaling#${owner}`; function manualLineupBlocker(game: string, mapSlug: string): string | null { const owner = owningPolicy(game, mapSlug); if (!owner || !liveScaling(owner.mode) || owner.paused_reason) return null; if ( ( db().prepare("SELECT value FROM settings WHERE key='autoscaling_paused'").get() as { value: string } | undefined )?.value === '1' ) return null; return ownershipMessage(owner.id); } export function scalingOwnershipBlocker(server: string): string | null { const identity = mapIdentity(getServer(server) ?? { name: server }); return identity ? manualLineupBlocker(identity.game, identity.mapSlug) : null; } export function assertScalingOwnership(game: string, mapSlug: string, decisionId?: string): void { if (!decisionId) { const blocker = manualLineupBlocker(game, mapSlug); if (blocker) throw new Error(blocker); return; } const owner = scalingOwner(game, mapSlug); if (!owner) throw new Error('Autoscaling no longer owns this map.'); if ( db() .prepare( 'SELECT 1 FROM autoscaling_decisions WHERE id=? AND policy_id=? AND resolved=0 AND operation_id IS NULL' ) .get(decisionId, owner) ) return; throw new Error(ownershipMessage(owner)); } /** Operator cancellation is a policy pause, not a fresh scaling signal. */ export function pauseScalingForOperation(operationId: string): void { const row = db() .prepare('SELECT policy_id FROM autoscaling_decisions WHERE operation_id=?') .get(operationId) as { policy_id: string } | undefined; if (!row) return; db() .prepare( "UPDATE autoscaling_policies SET mode='off',revision=revision+1,paused_reason='Paused after operator cancellation.',updated_at=? WHERE id=?" ) .run(new Date().toISOString(), row.policy_id); db() .prepare( 'UPDATE autoscaling_authorizations SET revoked_at=? WHERE policy_id=? AND claimed_at IS NULL' ) .run(Date.now(), row.policy_id); }