// ── What moves on the map, asked for while somebody is looking ──────────── // // D111: positions are request/reply, never a board. A board would run with // nobody looking and keep where everybody is at rest in the sidecar's database; // this asks `map.live` only when a page does, and keeps the answer IN MEMORY for // five seconds. // // **Any number of viewers cost one ask.** Every request inside the window is // served from the same answer, and a request that arrives while an ask is in // flight waits for that ask rather than starting a second — so the sidecar sees // at most one `map.live` per server per window, whoever and however many are // watching (§30.4 step 7). // // The cache holds EVERY layer, unfiltered; each viewer's answer is projected // from it by `model/map`. That is why it never leaves this process. const sidecar = require('./sidecarClient') const model = require('./model/map/map.model') /** serverId → { at, answer } for the last good answer, and { pending } while one is in flight. */ const cache = new Map() /** * One server's live answer, from the cache when it is fresh. Resolves `{ ok, * data, status }` like every sidecar call; never rejects. */ async function live(server, now = Date.now) { const hit = cache.get(server.id) if (hit && hit.answer && now() - hit.at < model.LIVE_CACHE_MS) return hit.answer if (hit && hit.pending) return hit.pending const pending = (async () => { const res = await sidecar.mapLive(server) const refused = res.ok && res.data && res.data.kind === 'map.error' const answer = res.ok && !refused ? { ok: true, status: 'ok', data: res.data } : { ok: false, status: refused ? res.data.reason || 'refused' : res.status, data: null } // A failure is cached for the window too: a game that is down must not be // asked once per viewer per poll while it stays down. cache.set(server.id, { at: now(), answer }) return answer })() cache.set(server.id, { ...(hit || {}), pending }) try { return await pending } finally { const entry = cache.get(server.id) if (entry && entry.pending === pending) delete entry.pending } } /** Test seam. */ function _reset() { cache.clear() } module.exports = { live, _reset }