// ── Asking a server how it is NOW, before saying no (W7, W11) ────────────── // // Every "is it loaded" refusal — Economics (its step and the staff page), // Backpacks, Dangerous Treasures, Guarded Crate, RaidableBases, ZoneDomes, // TruePVE, RunicNPC, and the console probe — reads the status the board poll // stored, and the poll runs every 30 seconds // (`boot.REFRESH_MS`). Right after a restart, or a plugin loaded by hand, the // stored status still says no for up to a minute while the game already says // yes. // // So a refusal is CONFIRMED before it is given: the step asks the sidecar once // more, writes what it said exactly as a poll tick would, and reads the stored // status again. Only a "no" that survives that is a refusal. A "yes" is never // re-asked: a plugin unloaded since the last poll still passes the dry run, and // the plugin itself refuses at the run, as it always has. // // One ask per server at a time, and none within `FRESH_MS` of the last, so a // form's dry run of ten steps against one server asks once. const core = require('./core') const log = core.logger('live-status') /** A status asked for this recently is as fresh as asking again. */ const FRESH_MS = 5 * 1000 /** * How long a step waits for the ask. `/boards` and `/health` are the sidecar's * own, no game round trip, so a sidecar that is up answers well inside it. The * bound keeps the ask plus the step's own call inside the step's `budgetMs` * (15 s) when a sidecar is slow; past it the stored status stands, and the ask * still finishes and writes, as a poll tick would. */ const ASK_MS = 3 * 1000 const inFlight = new Map() const askedAt = new Map() /** * Ask `server`'s sidecar now and store the answer, as a board poll does. * Resolves true once the stored status is current, false when it could not be * asked (the stored status stands). Never rejects. */ function refresh(server) { if (!server || !server.id) return Promise.resolve(false) const id = server.id if (inFlight.has(id)) return inFlight.get(id) if (Date.now() - (askedAt.get(id) || 0) < FRESH_MS) return Promise.resolve(true) // Required here, not at the top: boot.js requires the event steps that // require this file. const ask = Promise.resolve() .then(() => require('./boot').refreshOne(server)) .then( () => { askedAt.set(id, Date.now()) return true }, (err) => { log.warn('could not ask a server how it is', { server: id, error: err.message }) return false }, ) .finally(() => inFlight.delete(id)) inFlight.set(id, ask) // The caller waits at most `ASK_MS`; the ask itself runs to the end. let timer const late = new Promise((resolve) => { timer = setTimeout(() => resolve(false), ASK_MS) if (timer.unref) timer.unref() }) return Promise.race([ask, late]).finally(() => clearTimeout(timer)) } /** * `read()`'s answer, confirmed live when `refuses` says it would refuse: one * `refresh(server)`, then `read()` again. The answer the caller acts on. */ async function confirmed(server, read, refuses) { const first = await read() if (!refuses(first)) return first if (!(await module.exports.refresh(server))) return first return read() } /** For tests: forget every ask. */ function _reset() { inFlight.clear() askedAt.clear() } module.exports = { FRESH_MS, ASK_MS, refresh, confirmed, _reset }