86 lines
4.1 KiB
TypeScript
86 lines
4.1 KiB
TypeScript
import { randomInt, randomUUID } from 'node:crypto';
|
|
import { eq } from 'drizzle-orm';
|
|
import { getGameDb, persist } from '@/lib/db/client';
|
|
import * as schema from '@/lib/db/schema';
|
|
import { loadAirportsGeoJson } from '@/lib/airports';
|
|
import { generatePackage, HUB_ICAO, JOB_LIFETIME_MS, mulberry32 } from '@/lib/game/seed';
|
|
import { tickNow } from '@/lib/sim/engine';
|
|
|
|
// TEST-ONLY hook (v0.7.0, adapted to packages in v0.12.0; reworked for wall-clock expiry in v0.65.0) —
|
|
// lets the e2e suite observe open-job expiry without waiting an hour of real time. Disabled unless
|
|
// FLIGHT_TEST_HOOKS=1, which is set in docker-compose.yml for the e2e instance only; every other
|
|
// deployment 404s here.
|
|
//
|
|
// POST /api/test/expire-package
|
|
// body { packageId?: string }
|
|
// - with packageId: ages that OPEN package's createdRealMs to HOOK_AGE_MS in the past so the next
|
|
// tick (run immediately via tickNow) flips it to 'expired'; 404 if unknown/not open.
|
|
// - without: creates a fresh open package at the home hub, already aged past its lifetime — visible
|
|
// while open (the hub is always in the pool), expired by the time the caller polls /api/state.
|
|
|
|
const HOOK_AGE_MS = JOB_LIFETIME_MS + 60_000; // comfortably past the job lifetime
|
|
|
|
export async function POST(req: Request) {
|
|
if (process.env.FLIGHT_TEST_HOOKS !== '1') {
|
|
return Response.json({ error: 'test hooks disabled' }, { status: 404 });
|
|
}
|
|
|
|
let body: unknown = {};
|
|
try {
|
|
const text = await req.text();
|
|
body = text ? JSON.parse(text) : {};
|
|
} catch {
|
|
return Response.json({ error: 'invalid JSON body' }, { status: 400 });
|
|
}
|
|
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
return Response.json({ error: 'body must be a JSON object' }, { status: 400 });
|
|
}
|
|
const packageId = (body as Record<string, unknown>).packageId;
|
|
if (packageId !== undefined && typeof packageId !== 'string') {
|
|
return Response.json({ error: 'packageId must be a string' }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
const inst = await getGameDb();
|
|
|
|
if (typeof packageId === 'string') {
|
|
const rows = await inst.db.select().from(schema.packages).where(eq(schema.packages.id, packageId));
|
|
const row = rows[0];
|
|
if (!row || row.status !== 'open') {
|
|
return Response.json({ error: 'package not found or not open' }, { status: 404 });
|
|
}
|
|
await inst.db.update(schema.packages).set({ createdRealMs: Date.now() - HOOK_AGE_MS }).where(eq(schema.packages.id, packageId));
|
|
await persist(inst);
|
|
// AFTER the mutation on purpose: tickNow runs a full tickOnce, so expireOpenPackages flips the
|
|
// aged row to 'expired' within this same call — callers can poll /api/state immediately.
|
|
await tickNow();
|
|
const updated = (await inst.db.select().from(schema.packages).where(eq(schema.packages.id, packageId)))[0];
|
|
return Response.json(updated ?? row);
|
|
}
|
|
|
|
const fc = await loadAirportsGeoJson();
|
|
const airports = fc.features.map((f) => ({
|
|
icao: f.properties.icao,
|
|
name: f.properties.name,
|
|
lat: f.properties.lat,
|
|
lon: f.properties.lon,
|
|
}));
|
|
const rng = mulberry32(randomInt(0, 2 ** 31));
|
|
// nowMs anchors createdSimTime/deadlineSimTime to the GAME clock (ISS-117) — only createdRealMs
|
|
// is real time. special: null — the hook never rolls a special status (v0.37.0). Origin forced to
|
|
// the home hub so the job is visible while open; already aged past its lifetime, it expires on
|
|
// the tick below.
|
|
const sim = (await inst.db.select().from(schema.simState))[0];
|
|
if (!sim) throw new Error('sim_state row missing');
|
|
const created = generatePackage({ id: randomUUID(), airports, rng, nowMs: sim.clockMs, originIcao: HUB_ICAO, special: null, createdRealMs: Date.now() - HOOK_AGE_MS });
|
|
await inst.db.insert(schema.packages).values(created);
|
|
await persist(inst);
|
|
await tickNow();
|
|
const updated = (await inst.db.select().from(schema.packages).where(eq(schema.packages.id, created.id)))[0];
|
|
return Response.json(updated ?? created, { status: 201 });
|
|
} catch (err) {
|
|
console.error('test hook failed', err);
|
|
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
|
}
|
|
}
|