feat(rust): the leases and their option sources (phase 12, protocol 8)
Four leases on core.lease: rust.decay.scale, rust.population, rust.spawn.scalar and rust.group.permission. Every lease is targeted and the target names the server (D73). Also the three target option sources plus rust.options.servers, with no budgets (D79). Held for up to seven days (D77). A key that is already held reads as its baseline. Drift is an answer, not a failure. inForce reads the plugin's holds and never compares values. Lease calls get a 4.5s timeout so that two of them fit in core.lease's 10s budget, and a timed-out apply is followed by a release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -52,13 +52,15 @@ const TIMEOUT_MS = 12000
|
||||
* here, `PROTOCOL_VERSION` in the sidecar, `ProtocolVersion` in the bridge
|
||||
* plugin, and `protocol` in its `overlay.toml`.
|
||||
*
|
||||
* **7 — the raid frame.** Protocol 2 was the read path, 3 the first
|
||||
* **8 — the leases.** Protocol 2 was the read path, 3 the first
|
||||
* message the WEBSITE originates (`link.confirm`), 4 the first that writes to
|
||||
* the game's permission store, 5 the first that writes to the game HOST'S
|
||||
* FILESYSTEM; 6 adds the `clans` board and five clan events core's Teams are
|
||||
* built from, and no route at all; **7** widens `entity.destroyed` to doors,
|
||||
* walls and the cupboard and names who is authorised there, which is what the
|
||||
* raid alert is sent to (PLAN.md §25). The bump lands here in the same change as the emitters,
|
||||
* raid alert is sent to (PLAN.md §25); **8** adds the leases — `GET /lease`,
|
||||
* `POST /lease` and `POST /lease/release` — which is what lets an event borrow
|
||||
* a value on a server and give it back (PLAN.md §27). The bump lands here in the same change as the emitters,
|
||||
* because the sidecar refuses a client declaring a different version with a
|
||||
* `409`: a module left on 2 would stop being able to read the server board it
|
||||
* has been reading all along. A constant that lags the deployment is not a safe
|
||||
@@ -68,7 +70,7 @@ const TIMEOUT_MS = 12000
|
||||
* deployment into a `409` naming both numbers instead of a parse failure three
|
||||
* layers further in.
|
||||
*/
|
||||
const PROTOCOL_VERSION = 7
|
||||
const PROTOCOL_VERSION = 8
|
||||
|
||||
/** What a caller gets back. Shaped once so every call site reads the same. */
|
||||
function reply(ok, status, data = null) {
|
||||
@@ -97,8 +99,10 @@ function joinUrl(baseUrl, path) {
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.method]
|
||||
* @param {object} [options.body]
|
||||
* @param {number} [options.timeoutMs] shorter than `TIMEOUT_MS` only where a
|
||||
* caller has a tighter budget of its own to fit inside — see `LEASE_TIMEOUT_MS`
|
||||
*/
|
||||
async function request(server, path, { method = 'GET', body = null } = {}) {
|
||||
async function request(server, path, { method = 'GET', body = null, timeoutMs = TIMEOUT_MS } = {}) {
|
||||
if (!server || !server.baseUrl) return reply(false, 'not-configured')
|
||||
|
||||
// A sidecar with auth off does not exist — it generates and persists a token on
|
||||
@@ -108,7 +112,7 @@ async function request(server, path, { method = 'GET', body = null } = {}) {
|
||||
if (!server.token) return reply(false, 'no-token')
|
||||
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
const res = await fetch(joinUrl(server.baseUrl, path), {
|
||||
@@ -276,8 +280,61 @@ const configFile = (server, path) =>
|
||||
*/
|
||||
const configWrite = (server, body) => request(server, '/config/write', { method: 'POST', body })
|
||||
|
||||
/**
|
||||
* How long ONE lease call waits — shorter than every other call here, and for
|
||||
* the same rule `TIMEOUT_MS` is written for, applied to a caller with a tighter
|
||||
* budget.
|
||||
*
|
||||
* A lease is taken by core's `core.lease`, which declares no `budgetMs` and so
|
||||
* runs under the dispatcher's default of 10 seconds — and inside that it makes
|
||||
* TWO calls into this module, `read()` for the baseline and then `apply()`. At
|
||||
* `TIMEOUT_MS` each, one slow read would let the dispatcher give up and call the
|
||||
* attempt a retry while the module is still waiting, which is the ordering the
|
||||
* header of this file exists to forbid. Two of these fit inside core's budget
|
||||
* with a second to spare, and `leases.test.js` asserts the arithmetic rather than
|
||||
* trusting it.
|
||||
*
|
||||
* It is below the sidecar's own ten-second reply timeout, so this end can give
|
||||
* up on a call the game is still going to answer. For `read` that costs nothing.
|
||||
* For `apply` it would leave a value held that core believes it never took — so
|
||||
* `eventLeases.js` follows a timed-out apply with a release (§27.3).
|
||||
*/
|
||||
const LEASE_TIMEOUT_MS = 4500
|
||||
|
||||
/** The dispatcher's default action budget, which is what `core.lease` runs under. Mirrored, not imported: core does not export it. */
|
||||
const CORE_LEASE_BUDGET_MS = 10000
|
||||
|
||||
/**
|
||||
* What one server lends, what it holds now, and every hold in force
|
||||
* (protocol 8). Narrowed to one key and target when given.
|
||||
*/
|
||||
const leaseList = (server, { key, target } = {}) => {
|
||||
const q = []
|
||||
if (key) q.push(`key=${encodeURIComponent(key)}`)
|
||||
if (target) q.push(`target=${encodeURIComponent(target)}`)
|
||||
return request(server, `/lease${q.length ? `?${q.join('&')}` : ''}`, { timeoutMs: LEASE_TIMEOUT_MS })
|
||||
}
|
||||
|
||||
/**
|
||||
* Borrow a value (protocol 8). Like every write on this bridge, a refusal
|
||||
* comes back `{ ok: true }` with `data.kind` of `lease.error`; the transport
|
||||
* keeps its own codes.
|
||||
*/
|
||||
const leaseApply = (server, body) =>
|
||||
request(server, '/lease', { method: 'POST', body, timeoutMs: LEASE_TIMEOUT_MS })
|
||||
|
||||
/**
|
||||
* Give a value back — compare-and-set at the far end. `data.kind` is
|
||||
* `lease.ok`, `lease.drifted` (a 200: the plugin compared and declined to
|
||||
* overwrite somebody's change) or `lease.error`.
|
||||
*/
|
||||
const leaseRelease = (server, body) =>
|
||||
request(server, '/lease/release', { method: 'POST', body, timeoutMs: LEASE_TIMEOUT_MS })
|
||||
|
||||
module.exports = {
|
||||
TIMEOUT_MS,
|
||||
LEASE_TIMEOUT_MS,
|
||||
CORE_LEASE_BUDGET_MS,
|
||||
PROTOCOL_VERSION,
|
||||
request,
|
||||
health,
|
||||
@@ -292,5 +349,8 @@ module.exports = {
|
||||
configFiles,
|
||||
configFile,
|
||||
configWrite,
|
||||
leaseList,
|
||||
leaseApply,
|
||||
leaseRelease,
|
||||
joinUrl,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user