| 126 | * relative timing, shifted to the present so staleness math behaves. |
| 127 | */ |
| 128 | export class ReplayUpstream implements Upstream { |
| 129 | private aircraft = new Map<string, Aircraft>(); |
| 130 | private config: Config = DEFAULT_CONFIG; |
| 131 | private timers: ReturnType<typeof setTimeout>[] = []; |
| 132 | private running = false; |
| 133 | |
| 134 | constructor( |
| 135 | private file: string, |
| 136 | private events: UpstreamEvents = {}, |
| 137 | private speed = 1, |
| 138 | ) {} |
| 139 | |
| 140 | start(): void { |
| 141 | if (this.running) return; |
| 142 | this.running = true; |
| 143 | const lines = readFileSync(this.file, "utf8").split("\n").filter(Boolean); |
| 144 | const snaps: { t: number; aircraft: Aircraft[] }[] = []; |
| 145 | for (const line of lines) { |
| 146 | try { |
| 147 | const rec = JSON.parse(line); |
| 148 | if (rec.kind === "snapshot") snaps.push({ t: rec.t, aircraft: rec.aircraft }); |
| 149 | } catch { |
| 150 | // tolerate partial lines from crashed sessions |
| 151 | } |
| 152 | } |
| 153 | if (!snaps.length) { |
| 154 | console.error(`[replay] no snapshots in ${this.file}`); |
| 155 | return; |
| 156 | } |
| 157 | console.log(`[replay] ${snaps.length} snapshots from ${this.file}`); |
| 158 | const t0 = snaps[0].t; |
| 159 | const wall0 = Date.now(); |
| 160 | for (const snap of snaps) { |
| 161 | const delay = (snap.t - t0) / this.speed; |
| 162 | this.timers.push( |
| 163 | setTimeout(() => { |
| 164 | const now = Date.now(); |
| 165 | const shift = now - snap.t; // re-stamp into the present |
| 166 | this.aircraft.clear(); |
| 167 | const shifted = snap.aircraft.map((ac) => ({ |
| 168 | ...ac, |
| 169 | ts: (ac.ts ?? snap.t) + shift, |
| 170 | })); |
| 171 | for (const ac of shifted) this.aircraft.set(ac.hex, ac); |
| 172 | this.events.onSnapshot?.(wall0 + delay * this.speed, shifted); |
| 173 | }, delay), |
| 174 | ); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | stop(): void { |
| 179 | for (const t of this.timers) clearTimeout(t); |
| 180 | this.timers = []; |
| 181 | this.running = false; |
| 182 | } |
| 183 | |
| 184 | isConnected(): boolean { |
| 185 | return this.running; |
nothing calls this directly
no outgoing calls
no test coverage detected