feat(events): one lease and the participation verbs (Phase 11b) #30
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/events-p11b-leases-participation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
EVENTS_PLAN.mdPhase 11b, the UO half. No route added, no schema change, noMODULE_APIbump.The lease, and why the catalog has exactly one key in it
EVENTS.md§D describes the 258Config.Getcall sites as splitting into two patterns — cached at type initialisation, where a lease applies cleanly and does nothing, and read live, where it takes effect at once. Measured on ServUO 57.4:So the allowlist is not a curated subset of a large pool. It is nearly the whole of what exists, and the catalog Phase 12 inherits will be short for reasons no amount of care can change.
uo.playercaps.skillcapis the one that is both live and observable —CharacterCreation.csreads it inside the per-character path — which is what "proven" has to mean here, since the failure an allowlist exists to prevent is a key that applies cleanly and changes nothing.Three of the four callables have a shape where the obvious implementation is subtly wrong:
apply()sends a DURATION, not the deadline. An absolute time computed here and honoured there is a deadline measured against two clocks, and a shard running ten minutes fast would restore a ten-minute lease the instant it took it.untilMsstill rides along, for a console that wants to say when the hold ends in the operator's own terms.restore()turnslease.driftedinto{ ok: false, drifted: true, current }, never an error. Core records drift as a distinct successful outcome — the module did exactly what it was asked and found somebody else's value in place — while an error would put the row on the retry ladder and spend its attempts on a situation only a human can resolve. The sidecar answers it 200 for the same reason.inForce()asks whether the shard still HOLDS the lease, never whether the value still matches. A changed value is drift, whichrestore()reports; answering "not in force" here would orphan the row first and tell the operator the lease vanished rather than that somebody moved it. It matters most for the case core could not see at all beforewebsite#193: a config lease is memory-only on the shard, so a restart reverts it and clears the record.Participation
uo.participation.openledgers the venue and starts the count;uo.participation.collectfiles the tally on its success envelope, which is the path Phase 10 built and this is the first module to use.inspect, notchange. Nothing in the world moves and no player can see it: the shard starts keeping a tally about a place. §K puts the default-off line betweeninspectandchange, and a step that only watches is not one an operator should have to switch on before an event can record who came. It is stillreversible: 'ledger', because the shard IS holding one of a bounded number of counting slots on this run's behalf — reversibility is about what a run owes, not how loud it was in taking it.placeis sourced fromuo.options.landmarksand resolved to map + point through the atlas, so an author picks "Felucca/Britain" and never types a coordinate. A place this shard's atlas does not know is a refusal the dry run can show them, not a 400 mid-run.userIdis resolved here and only when the shard could resolve it. It is a foreign key intousers; core refuses anything that is not a positive integer rather than coercing, and a character serial passed there would either fail the insert or attribute somebody's attendance to a stranger who happened to hold that id. Most characters carry nowebIdat all.meta. A results table that can say "forty minutes and three kills" beside a number is one an operator can defend when a player argues with it.The one resource here that must NOT reconcile by boot stamp
⚠️ For a reviewer, because it is the single place this file departs from wave 1.
Every other resource in
uoEventActions.jsusesreconcileByBootId, and that works because a town-crier line and an event-owned news article live in shard memory — a changedbootIdis the proof they are gone.The participation ledger is written into the world save, specifically so that it survives the restart the boot stamp would report it lost by. Stamping it would orphan the one resource the phase went to the trouble of persisting. So
uo.participation.openasks instead, and only an explicit 404 takes a row out; a shard that is down, slow or refusing has not said the ledger is gone. There is a test that asserts itsreconcileis notreconcileByBootIdand that its resource carries no stamp, so nothing downstream can be tempted to compare one.Visibility
lease.appliedandlease.expiredare deliberately left offKIND_FEATURE, on the same reasoning that keepsaccount.login.resultoff it. They are operational records of the website changing this shard's configuration — which key, from what to what, on whose run, and whether the shard's own deadline had to put it back because nobody asked. Rule 2 fails an unmapped kind closed to admin-only, which is where an audit trail of the site's own writes belongs; mapping them would mean choosing a feature an operator could then widen, and there is no rung below admin these belong on. The comment says so, so a later reader does not "fix" the omission.The participation ledger emits no stream frame at all: a live feed of who is standing where would be a location tracker, and the ledger's whole justification is that it answers one bounded question about one run.
Verification
npm test --prefix server— 588 pass, 0 fail (17 new).npm test --prefix client— 42 pass.npm run build --prefix clientgreen.check:importsclean.servuo-plugins#22's table was collected through this module's own client shapes on the live rig — a real ServUO, the release sidecar, and the tally surviving a shard restart mid-run.The new suite is written against the places the obvious implementation is wrong rather than against the calls happening, which a rig proves better: drift-not-error,
inForceis not a comparison,applysends a duration, the ledger is not boot-stamped, and awebIdthat is not a positive integer resolves to nothing at all.Plugin: RunicGateway/servuo-plugins#22 · Sidecar: RunicGateway/link#37 · Core: RunicGateway/website#193 · Docs: RunicGateway/docs#220
🤖 Generated with Claude Code
The UO half of protocol 6 part b. No route added, no schema change, no MODULE_API bump. `uo.playercaps.skillcap` is the one lease, and the catalog is short because ServUO made it short: of the 158 non-Bridge `Config.Get` call sites in `Scripts/`, roughly eight are read live. This one is read inside `CharacterCreation.cs`'s per-character path, so it is both live and observable -- which is what "proven" has to mean, since the failure an allowlist exists to prevent is a key that applies cleanly and changes nothing. Its `apply()` sends a DURATION rather than the deadline: an absolute time computed here and honoured there is measured against two clocks, and a shard running ten minutes fast would restore a ten-minute lease the instant it took it. Its `restore()` turns `lease.drifted` into `{ drifted: true, current }` rather than an error, because core records drift as a distinct successful outcome and an error would put the row on the retry ladder. Its `inForce()` asks whether the shard still HOLDS the lease, never whether the value still matches -- see the core PR. `uo.participation.open` / `.collect` count who took part and file them on the success envelope. `open` is the one resource in this module that must NOT reconcile by boot stamp: every other resource here lives in shard memory, so a changed bootId IS the proof it is gone, while the participation ledger is written into the world save precisely so it survives that restart. It asks instead. Co-Authored-By: Claude <noreply@anthropic.com>