feat(events): what an author borrows, and two one-shots (Phase 12b) #32

Merged
whitlocktech merged 1 commits from feature/events-p12b-borrowed-and-oneshots into edge 2026-09-07 16:23:35 +00:00
Member

Events Phase 12b, module half. One of five: website (website#194), servuo-plugins (servuo-plugins#24), link (link#39), Module-uo, docs (docs#222). Spec of record: docs/link/v7.md §11–§14.

Five leases, one factory

uo.spawner.maxcount, .mindelay, .maxdelay, .running and uo.seasonal.status. The four callables differ only in which key they name, so they are built rather than repeated: five copies would be five chances for one of them to forget the drift check, which is the one thing §F says a lease must not be allowed to skip.

It is MaxCount, not the Amount EVENTS_PLAN.md named — there is no such property on ServUO 57.4. MinDelay/MaxDelay are TimeSpans, so the wire carries seconds: the spawn files' own DelayInSec flag proves both units are in use on a real tree, and a unit that cannot express five seconds cannot express this shard's own data.

The seasonal lease is a three-value enum over eight events. §G called GetEntry(type).Status "a nine-value enum" and had it backwards: EventStatus has three values and it is EventType that has nine entries. Eight rather than nine because TreasuresOfTokuno is excluded — IsActive() reads its own DropEra rather than Status, so leasing it would apply cleanly, read back, restore cleanly and do nothing at all.

Two behaviours worth the review. inForce() reads the frame's holds rather than a row's held flag, because a catalog walk can enumerate the keys but never the holds on a targeted one. And a target that vanished mid-run is a successful restore: there is nothing to give back, and reporting it failed would leave a ledger row unresolved for ever over an object that is gone — 12a's gone in the lease plane's vocabulary.

The grant names a run, never a recipient list

Core has the participants in event_run_participants, but a module cannot read core's tables — so the alternative was a new core surface handing them over. Not needed: the shard has held the run's ledger since it opened, keyed by the same serials core stores as member_key.

And the grant is retryable. §G called it un-retryable because a lost acknowledgement and a grant that never applied were the same event, which is exactly the argument that made uo.broadcast answer retry: false in Phase 9. Protocol 6's idempotency key closes it. uo.rewards counts items rather than grants: 500 gold to forty people and a candle to forty people are not the same imposition.

The atlas keeps UniqueId again, and the spawner source searches

The parser has read <UniqueId> and thrown it away since the atlas shipped, on a line citing a committed artifact — there is no committed artifact, as spawnAtlasSource.js says in its own header. It is the only name for one particular spawner that exists off the shard, so a property lease could not have had a dropdown without it. PARSER_VERSION → 4 so an unchanged tree is re-read.

uo.options.spawners is the first searchable source and the first that had to be: 6,707 spawn points against MAX_OPTIONS' 2,000, so a flat list would drop two thirds of the world and say nothing about which two thirds.

One defect in already-merged code, and it would have broken everything

The protocol pin never left 5. uo_link_config.protocol reaches the sidecar as X-UOLink-Version on every REST call and an exact mismatch is a 409, so from Phase 11a onward every sidecar call on a real deployment would have been refused — the whole event plane dead, loudly, for a reason nobody would look here for. 11a took the wire to 6 and 12a to 7; neither moved the pin, in either of the two places this repo declares it.

It survived both because both live walks set the column by hand while standing the rig up, which is exactly what makes a migration nobody runs invisible. All three sites go to 7.

The test that guards them is worth understanding before trusting it: schemaFragment.test.js asserts the three declarations agree with each other — a real check they once failed — but all three being equally stale passes it, and nothing in this repo can anchor it to the wire. Recorded in the model's own header so the next reader knows.

Checks

npm test: 620 pass, 0 fail (was 605). check:imports and check:externals clean; the client builds and its 42 tests pass.

check:swagger reports the fragment stale — it is already stale on edge (verified by stashing this branch's changes and re-running), and this phase adds no route, so it is left alone rather than regenerated inside an unrelated change. Same as 12a.

Two bugs the new tests caught in this branch's own code before it left: counted() returns .count and the grant read .value, so every grant went out with amount: undefined and the non-stackable guard never fired; and optionalInt's ok was ignored, so a bad hue passed silently instead of refusing.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Events Phase 12b, module half. One of **five**: `website` (website#194), `servuo-plugins` (servuo-plugins#24), `link` (link#39), `Module-uo`, `docs` (docs#222). Spec of record: [`docs/link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/222) §11–§14. ## Five leases, one factory `uo.spawner.maxcount`, `.mindelay`, `.maxdelay`, `.running` and `uo.seasonal.status`. The four callables differ only in which key they name, so they are **built rather than repeated**: five copies would be five chances for one of them to forget the drift check, which is the one thing §F says a lease must not be allowed to skip. It is **`MaxCount`, not the `Amount`** `EVENTS_PLAN.md` named — there is no such property on ServUO 57.4. `MinDelay`/`MaxDelay` are `TimeSpan`s, so the wire carries **seconds**: the spawn files' own `DelayInSec` flag proves both units are in use on a real tree, and a unit that cannot express five seconds cannot express this shard's own data. The seasonal lease is a **three-value enum over eight events**. §G called `GetEntry(type).Status` *"a nine-value enum"* and had it backwards: `EventStatus` has three values and it is `EventType` that has nine entries. Eight rather than nine because `TreasuresOfTokuno` is excluded — `IsActive()` reads its own `DropEra` rather than `Status`, so leasing it would apply cleanly, read back, restore cleanly and do nothing at all. Two behaviours worth the review. **`inForce()` reads the frame's `holds`** rather than a row's `held` flag, because a catalog walk can enumerate the keys but never the holds on a targeted one. And **a target that vanished mid-run is a successful restore**: there is nothing to give back, and reporting it failed would leave a ledger row unresolved for ever over an object that is gone — 12a's `gone` in the lease plane's vocabulary. ## The grant names a run, never a recipient list Core has the participants in `event_run_participants`, but a module cannot read core's tables — so the alternative was a new core surface handing them over. Not needed: the shard has held the run's ledger since it opened, keyed by the same serials core stores as `member_key`. And **the grant is retryable.** §G called it un-retryable because a lost acknowledgement and a grant that never applied were the same event, which is exactly the argument that made `uo.broadcast` answer `retry: false` in Phase 9. Protocol 6's idempotency key closes it. `uo.rewards` counts **items** rather than grants: 500 gold to forty people and a candle to forty people are not the same imposition. ## The atlas keeps `UniqueId` again, and the spawner source searches The parser has read `<UniqueId>` and thrown it away since the atlas shipped, on a line citing a committed artifact — **there is no committed artifact**, as `spawnAtlasSource.js` says in its own header. It is the *only* name for one particular spawner that exists off the shard, so a property lease could not have had a dropdown without it. `PARSER_VERSION` → 4 so an unchanged tree is re-read. `uo.options.spawners` is the first **searchable** source and the first that had to be: 6,707 spawn points against `MAX_OPTIONS`' 2,000, so a flat list would drop two thirds of the world and say nothing about which two thirds. ## One defect in already-merged code, and it would have broken everything **The protocol pin never left 5.** `uo_link_config.protocol` reaches the sidecar as `X-UOLink-Version` on every REST call and an exact mismatch is a `409`, so from Phase 11a onward **every sidecar call on a real deployment would have been refused** — the whole event plane dead, loudly, for a reason nobody would look here for. 11a took the wire to 6 and 12a to 7; neither moved the pin, in either of the two places this repo declares it. It survived both because **both live walks set the column by hand while standing the rig up**, which is exactly what makes a migration nobody runs invisible. All three sites go to 7. The test that guards them is worth understanding before trusting it: `schemaFragment.test.js` asserts the three declarations agree **with each other** — a real check they once failed — but *all three being equally stale passes it*, and nothing in this repo can anchor it to the wire. Recorded in the model's own header so the next reader knows. ## Checks `npm test`: **620 pass, 0 fail** (was 605). `check:imports` and `check:externals` clean; the client builds and its 42 tests pass. `check:swagger` reports the fragment stale — **it is already stale on `edge`** (verified by stashing this branch's changes and re-running), and this phase adds no route, so it is left alone rather than regenerated inside an unrelated change. Same as 12a. **Two bugs the new tests caught in this branch's own code before it left:** `counted()` returns `.count` and the grant read `.value`, so every grant went out with `amount: undefined` and the non-stackable guard never fired; and `optionalInt`'s `ok` was ignored, so a bad hue passed silently instead of refusing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-07 13:11:49 +00:00
feat(events): what an author borrows, and two one-shots (Phase 12b)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 38s
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 53s
10fde87724
Five targeted leases over two planes, the item grant, the world save, and the
atlas work the spawner dropdown needed.

FIVE LEASES, ONE FACTORY

`uo.spawner.maxcount`, `.mindelay`, `.maxdelay`, `.running` and
`uo.seasonal.status`. The four callables differ only in which key they name, so
they are built rather than repeated: five copies would be five chances for one of
them to forget the drift check, which is the one thing §F says a lease must not
be allowed to skip.

It is `MaxCount`, not the `Amount` EVENTS_PLAN.md named -- there is no such
property on ServUO 57.4. `MinDelay`/`MaxDelay` are TimeSpans, so the wire carries
SECONDS: the spawn files' own `DelayInSec` flag proves both units are in use on a
real tree, and a unit that cannot express five seconds cannot express this
shard's own data.

The seasonal lease is a THREE-value enum over EIGHT events. §G called
`GetEntry(type).Status` "a nine-value enum" and had it backwards: `EventStatus`
has three values and it is `EventType` that has nine entries. Eight rather than
nine because `TreasuresOfTokuno` is excluded -- `IsActive()` reads its own
`DropEra` rather than `Status`, so leasing it would apply cleanly, read back,
restore cleanly and do nothing at all.

Two behaviours worth the review. `inForce()` reads the frame's `holds` rather
than a row's `held` flag, because a catalog walk can enumerate the keys but never
the holds on a targeted one. And a target that VANISHED mid-run is a SUCCESSFUL
restore: there is nothing to give back, and reporting it failed would leave a
ledger row unresolved for ever over an object that is gone -- 12a's `gone` in the
lease plane's vocabulary.

THE GRANT NAMES A RUN, NEVER A RECIPIENT LIST

Core has the participants in `event_run_participants`, but a module cannot read
core's tables -- so the alternative was a new core surface handing them over. Not
needed: the shard has held the run's ledger since it opened, keyed by the same
serials core stores as `member_key`.

And the grant is RETRYABLE. §G called it un-retryable because a lost
acknowledgement and a grant that never applied were the same event, which is
exactly the argument that made `uo.broadcast` answer `retry: false` in Phase 9.
Protocol 6's idempotency key closes it. `uo.rewards` counts ITEMS rather than
grants: 500 gold to forty people and a candle to forty people are not the same
imposition.

THE ATLAS KEEPS UniqueId AGAIN, AND THE SPAWNER SOURCE SEARCHES

The parser has read `<UniqueId>` and thrown it away since the atlas shipped, on a
line citing a committed artifact -- there is no committed artifact, as
`spawnAtlasSource.js` says in its own header. It is the ONLY name for one
particular spawner that exists off the shard, so a property lease could not have
had a dropdown without it. `PARSER_VERSION` -> 4 so an unchanged tree is re-read.

`uo.options.spawners` is the first searchable source and the first that had to
be: 6,707 spawn points against `MAX_OPTIONS`' 2,000, so a flat list would drop
two thirds of the world and say nothing about which two thirds.

ONE DEFECT IN ALREADY-MERGED CODE, AND IT WOULD HAVE BROKEN EVERYTHING

The protocol pin never left 5. `uo_link_config.protocol` reaches the sidecar as
`X-UOLink-Version` on every REST call and an exact mismatch is a 409, so from
Phase 11a onward every sidecar call on a real deployment would have been refused
-- the whole event plane dead, loudly, for a reason nobody would look here for.
11a took the wire to 6 and 12a to 7; neither moved the pin, in either of the two
places this repo declares it. It survived both because both live walks set the
column by hand while standing the rig up, which is exactly what makes a migration
nobody runs invisible. All three sites go to 7.

The test that guards them is worth understanding before trusting it:
`schemaFragment.test.js` asserts the three declarations agree WITH EACH OTHER --
a real check they once failed -- but all three being equally stale passes it, and
nothing in this repo can anchor it to the wire. Recorded in the model's own
header so the next reader knows.

CHECKS

`npm test`: 620 pass, 0 fail (was 605). `check:imports` and `check:externals`
clean; the client builds and its 42 tests pass. `check:swagger` reports the
fragment stale -- it is ALREADY stale on `edge` (verified by stashing this
branch's changes and re-running) and this phase adds no route, so it is left
alone rather than regenerated inside an unrelated change.

Two bugs the new tests caught in this branch's own code before it left: `counted()`
returns `.count` and the grant read `.value`, so every grant went out with
`amount: undefined` and the non-stackable guard never fired; and `optionalInt`'s
`ok` was ignored, so a bad hue passed silently instead of refusing.

Refs: docs/link/v7.md §11-§14, docs/website/EVENTS_PLAN.md Phase 12b

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit c289586a3d into edge 2026-09-07 16:23:35 +00:00
whitlocktech deleted branch feature/events-p12b-borrowed-and-oneshots 2026-09-07 16:23:36 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-uo#32
No description provided.