Files
Module-Rust/server/liveStatus.js
wtclaude 3aa142bf06
All checks were successful
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 32s
PR Checks / frozen-manifest (pull_request) Successful in 44s
fix(rust): the last fixes' small ones (W3 W6 W7/W11 W8 W10 W13-W18)
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
2026-10-21 07:42:24 -05:00

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 }