push updated state to clients over SSE (ISS-129)
CI / build + unit + e2e (pull_request) Successful in 11m7s
CI / build + unit + e2e (pull_request) Successful in 11m7s
This commit is contained in:
+27
-8
@@ -7,7 +7,8 @@ import { haversineKm, initialBearingDeg } from '@/lib/geo';
|
||||
import { weatherFor } from '@/lib/weather';
|
||||
import { FUEL_DISCOUNT_PER_REP, FUEL_PRICE_PER_L, MAX_FUEL_DISCOUNT, PLANE_TYPES, REPAIR_DURATION_MS, repRate, WEAR_PER_KM } from '@/lib/game/catalog';
|
||||
import { SPECIALS } from '@/lib/game/specials';
|
||||
import { parseIcaoQueue } from '@/lib/game/state';
|
||||
import { getStateSnapshot, parseIcaoQueue } from '@/lib/game/state';
|
||||
import { broadcast } from '@/lib/sse';
|
||||
import { IDLE_WORLD_TIME_SCALE } from '@/lib/worldClock';
|
||||
import {
|
||||
generatePackage,
|
||||
@@ -46,13 +47,15 @@ export function reputationGainFor(km: number): number {
|
||||
return Math.round((REP_BASE + REP_PER_KM * km) * 100) / 100;
|
||||
}
|
||||
|
||||
// Serialize ticks across concurrent requests (Next.js route handlers run in the
|
||||
// same process): each tick re-reads sim_state after the previous one wrote it, so
|
||||
// no advance is ever applied twice.
|
||||
let chain: Promise<unknown> = Promise.resolve();
|
||||
// Serialize ticks across concurrent requests (Next.js route handlers run in the same process): each
|
||||
// tick re-reads sim_state after the previous one wrote it, so no advance is ever applied twice.
|
||||
// ISS-129 (v0.57.0): the chain lives on globalThis — instrumentation.ts and route handlers get
|
||||
// separate module instances from Next.js, so a plain `let` would give each layer its own lock and let
|
||||
// their ticks interleave against the shared DB below.
|
||||
const g = globalThis as unknown as { __flightTickChain?: Promise<unknown> };
|
||||
function withLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const run = chain.then(fn);
|
||||
chain = run.then(
|
||||
const run = (g.__flightTickChain ??= Promise.resolve()).then(fn);
|
||||
g.__flightTickChain = run.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
@@ -416,8 +419,24 @@ export async function spawnCoveragePackages(
|
||||
|
||||
// --- Public API (singleton DB + lock) — call from every read/action endpoint ---
|
||||
|
||||
// ISS-129 (v0.57.0): after each tick on the singleton path, push a fresh snapshot to all SSE
|
||||
// subscribers (/api/events) — the exact same body GET /api/state returns (getStateSnapshot), so
|
||||
// pushed and polled states can never diverge in shape. Failures are logged, not fatal: polling
|
||||
// still works either way. Background ticks (~1 s) cover action mutations for other clients too.
|
||||
async function broadcastState(): Promise<void> {
|
||||
try {
|
||||
broadcast(JSON.stringify(await getStateSnapshot()));
|
||||
} catch (err) {
|
||||
console.error('flight: state broadcast failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function tickNow(): Promise<schema.SimStateRow> {
|
||||
return withLock(async () => tickOnce(await getGameDb()));
|
||||
return withLock(async () => {
|
||||
const row = await tickOnce(await getGameDb());
|
||||
await broadcastState();
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
// ISS-117: the world clock runs even with nobody polling — a ~1 s interval drives the same locked
|
||||
|
||||
Reference in New Issue
Block a user