v0.65.0: per-airport job pools with real-time refresh — open jobs spawn per pool airport (grounded/flying/queued locations) in a fixed POOL_BANDS composition, expire on the wall clock after JOB_LIFETIME_MS even while the world is frozen, and refill taken slots after a per-band cooldown; /api/state + SSE filter open jobs to pool origins and expose expiresRealMs for the right-pane countdown
CI / build + unit + e2e (pull_request) Failing after 17m26s

This commit is contained in:
2026-09-23 13:09:49 +00:00
parent 9c2c64fe89
commit 12d900b22a
24 changed files with 556 additions and 344 deletions
+24 -15
View File
@@ -3,21 +3,22 @@ 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, mulberry32 } from '@/lib/game/seed';
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) — lets the e2e suite
// observe package expiry without waiting hours of game time. Disabled unless
// FLIGHT_TEST_HOOKS=1, which is set in docker-compose.yml for the e2e instance
// only; every other deployment 404s here.
// 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: moves that OPEN package's deadline to now + HOOK_DEADLINE_MS
// (game time) so the next fast tick expires it; 404 if unknown/not open.
// - without: creates a fresh open package with the same short deadline.
// - 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_DEADLINE_MS = 10_000; // game ms past the current clock (≈ 0.01 s real at the fastest 1000×)
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') {
@@ -41,8 +42,6 @@ export async function POST(req: Request) {
try {
const inst = await getGameDb();
const sim = await tickNow(); // clock is current before we set the deadline
const deadlineSimTime = Math.round(sim.clockMs + HOOK_DEADLINE_MS);
if (typeof packageId === 'string') {
const rows = await inst.db.select().from(schema.packages).where(eq(schema.packages.id, packageId));
@@ -50,8 +49,11 @@ export async function POST(req: Request) {
if (!row || row.status !== 'open') {
return Response.json({ error: 'package not found or not open' }, { status: 404 });
}
await inst.db.update(schema.packages).set({ deadlineSimTime }).where(eq(schema.packages.id, packageId));
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);
}
@@ -64,11 +66,18 @@ export async function POST(req: Request) {
lon: f.properties.lon,
}));
const rng = mulberry32(randomInt(0, 2 ** 31));
// special: null — the hook never rolls a special status (v0.37.0).
const created = generatePackage({ id: randomUUID(), airports, rng, nowMs: sim.clockMs, deadlineOffsetMs: HOOK_DEADLINE_MS, special: null });
// 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);
return Response.json(created, { status: 201 });
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 });