v0.59.0: SSE frames drop terminal package history (sseFrame projection) — ~597 KB → ~10 KB per frame (#133)
CI / build + unit + e2e (pull_request) Successful in 17m7s
CI / build + unit + e2e (pull_request) Successful in 17m7s
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SSE_PACKAGE_STATUSES, sseFrame, type PackageState, type StateSnapshot } from './state';
|
||||
|
||||
// ISS-133 (v0.59.0) — the SSE projection keeps only the package statuses any component renders.
|
||||
|
||||
const pkg = (status: PackageState['status'], id: string): PackageState => ({
|
||||
id,
|
||||
originIcao: 'LEMD',
|
||||
originName: 'Madrid–Barajas',
|
||||
destIcao: 'EHAM',
|
||||
destName: 'Amsterdam Schiphol',
|
||||
reward: 1000,
|
||||
cargoType: 'mail',
|
||||
cargoCount: 3,
|
||||
weightKg: 900,
|
||||
special: null,
|
||||
distanceKm: 2000,
|
||||
deadlineSimTime: 1_000_000,
|
||||
status,
|
||||
planeId: null,
|
||||
createdSimTime: 0,
|
||||
});
|
||||
|
||||
const snapshot = (packages: PackageState[]): StateSnapshot => ({
|
||||
account: { cash: 20_000, reputation: 1 },
|
||||
simState: { clockMs: 42 },
|
||||
catalog: [],
|
||||
fuelPricePerL: 0.8,
|
||||
sellFraction: 0.5,
|
||||
planes: [],
|
||||
packages,
|
||||
});
|
||||
|
||||
describe('sseFrame', () => {
|
||||
it('keeps exactly the open/loaded/expired packages and drops terminal history', () => {
|
||||
const all = [
|
||||
pkg('open', 'p-open'),
|
||||
pkg('delivered', 'p-delivered'),
|
||||
pkg('loaded', 'p-loaded'),
|
||||
pkg('abandoned', 'p-abandoned'),
|
||||
pkg('expired', 'p-expired'),
|
||||
pkg('ignored', 'p-ignored'),
|
||||
];
|
||||
const frame = sseFrame(snapshot(all));
|
||||
expect(frame.packages.map((p) => p.id)).toEqual(['p-open', 'p-loaded', 'p-expired']);
|
||||
});
|
||||
|
||||
it('passes the rest of the snapshot through untouched and does not mutate its input', () => {
|
||||
const all = [pkg('open', 'a'), pkg('delivered', 'b')];
|
||||
const snap = snapshot(all);
|
||||
const frame = sseFrame(snap);
|
||||
expect(frame.account).toEqual({ cash: 20_000, reputation: 1 });
|
||||
expect(frame.simState).toEqual({ clockMs: 42 });
|
||||
expect(frame.planes).toEqual([]);
|
||||
expect(all.map((p) => p.status)).toContain('delivered'); // input intact
|
||||
});
|
||||
|
||||
it('SSE_PACKAGE_STATUSES matches what the client renders (open market, on-board cargo)', () => {
|
||||
expect([...SSE_PACKAGE_STATUSES].sort()).toEqual(['expired', 'loaded', 'open']);
|
||||
});
|
||||
});
|
||||
@@ -85,6 +85,16 @@ export interface StateSnapshot {
|
||||
sellFraction: number;
|
||||
}
|
||||
|
||||
// ISS-133 (v0.59.0): the SSE projection of a snapshot — only the package statuses any component
|
||||
// renders: open (market list), loaded + expired (on-board cargo, which keeps its payload slot).
|
||||
// Terminal history (delivered/abandoned/ignored) is unbounded in the DB and never read by the
|
||||
// client, so it stays out of every pushed frame; /api/state and action responses keep the full set.
|
||||
export const SSE_PACKAGE_STATUSES = ['open', 'loaded', 'expired'] as const;
|
||||
|
||||
export function sseFrame(snapshot: StateSnapshot): StateSnapshot {
|
||||
return { ...snapshot, packages: snapshot.packages.filter((p) => (SSE_PACKAGE_STATUSES as readonly string[]).includes(p.status)) };
|
||||
}
|
||||
|
||||
export async function getStateSnapshot(): Promise<StateSnapshot> {
|
||||
const { db } = await getGameDb();
|
||||
// NOTE: use plain select(), not the relational db.query.* API — in drizzle-orm
|
||||
|
||||
+6
-5
@@ -7,7 +7,7 @@ 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 { getStateSnapshot, parseIcaoQueue } from '@/lib/game/state';
|
||||
import { getStateSnapshot, parseIcaoQueue, sseFrame } from '@/lib/game/state';
|
||||
import { broadcast } from '@/lib/sse';
|
||||
import { IDLE_WORLD_TIME_SCALE } from '@/lib/worldClock';
|
||||
import {
|
||||
@@ -420,12 +420,13 @@ 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.
|
||||
// subscribers (/api/events). ISS-133 (v0.59.0): frames carry the sseFrame projection — terminal
|
||||
// package history is dropped, everything else matches GET /api/state exactly. 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()));
|
||||
broadcast(JSON.stringify(sseFrame(await getStateSnapshot())));
|
||||
} catch (err) {
|
||||
console.error('flight: state broadcast failed', err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user