Stage 2 of PLAN_LAST_FIXES (docs#366). - W7/W11: an "is it loaded" refusal is confirmed live before it is given. New liveStatus.js asks the sidecar once (boot.refreshOne, as a poll tick would), at most once per server per 5 s and waiting at most 3 s, then reads the stored status again. Used by Economics (step and staff page), Backpacks, Dangerous Treasures, Guarded Crate, RaidableBases, ZoneDomes, TruePVE and RunicNPC. - W3: a world call answered `already-out` (the bridge half comes in stage 3) succeeds as called 0, saying a Travelling Vendor is already out. - W6: "A locked crate dropped by a Chinook is being hacked." - W8: a RaidableBases profile's audit row names RaidableBases, not "@data". - W10: the backpack sizes picker lists each size once, naming its servers. - W13: a RaidableBases name both servers describe differently shows each server's own description. - W14: each RaidableBases note after the first starts its own sentence. - W15: a staff deposit refused at Economics' limit answers 409. - W16: a payment's failedNote explains only the reasons that happened. - W17: the sync state counts player grants that did not land (waitingToLand), and the Permissions page says "in sync · 1 waits for their player to join" instead of plain "in sync". - W18: Guarded Crate's kept note drops the "30 minutes" it could not know. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
96 lines
3.4 KiB
JavaScript
96 lines
3.4 KiB
JavaScript
// ── 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 }
|