feat(events): UO wave 1 — the verbs that need no protocol change (Phase 9)
module-uo registers its first event actions: `uo.broadcast`, `uo.towncrier.post` and `uo.news.post`, plus the `uo.broadcasts` budget dimension and the three spawn-atlas option sources. The write plane they use has existed since protocol 2.1; what is new is the declaration that lets the event engine drive it unattended. Three things the tree corrected about the plan: - The plan's `on_failure: 'skip'` for `uo.broadcast` is already the default for `risk: 'notify'`, and `on_failure` is what happens AFTER the retries. The lever a module actually has is the failure envelope, so the action answers `retry: false` to everything — and every action declares `budgetMs: 15000`, because core's 10s default deadline fires before `uoLinkClient`'s 12s timeout and `classify()` answers `retry` for a timeout without asking the module. Without the budget the retry refusal is unreachable. - `reconcile()` needs no protocol work. A shard restart wipes both the crier lines and an event's news article, so `perform()` stamps the shard `bootId` into the resource payload and `reconcile()` reports in force exactly the rows whose stamp still matches — correct for the module's own trigger and for core's boot sweep alike. `shardIngest` fires `ctx.events.reconcile()` on a changed `bootId`, after `recordStatus` so the comparison reads the new boot. - Event articles post under `evt-<idempotencyKey>`, because `newsGump.js` uses the bare website post id and re-pushes that set on every reconnect. `ci/core-ref.json` moves to a website `edge` sha for the length of this workstream: `registerEventActions` exists only from MODULE_API 1.10.0, so under the old `main` pin the module does not load at all. Verified locally — the frozen-manifest rig passes against the new pin. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ const shardStateModel = require('../model/shardState/shardState.model')
|
||||
const shardLinksModel = require('../model/shardLinks/shardLinks.model')
|
||||
const shardMarketModel = require('../model/shardMarket/shardMarket.model')
|
||||
const uoLinkConfigModel = require('../model/uoLinkConfig/uoLinkConfig.model')
|
||||
const { settings: settingsModel } = require('../core')
|
||||
const { settings: settingsModel, events: coreEvents } = require('../core')
|
||||
const broadcaster = require('./shardBroadcast')
|
||||
const shardPush = require('./shardPush')
|
||||
const shardEngagement = require('./shardEngagement')
|
||||
@@ -103,11 +103,12 @@ async function resolveShardName(shard, deps) {
|
||||
|
||||
// Apply the state-change side effect for a kind (if any). Returns a promise.
|
||||
async function applyStateChange(event, deps) {
|
||||
const { shardState, uoLinkConfig, log } = deps
|
||||
const { shardState, uoLinkConfig, eventsReconcile, log } = deps
|
||||
switch (event.kind) {
|
||||
case 'server.hello': {
|
||||
const incoming = event.bootId || null
|
||||
if (incoming && state.bootId && incoming !== state.bootId) {
|
||||
const restarted = Boolean(incoming && state.bootId && incoming !== state.bootId)
|
||||
if (restarted) {
|
||||
log.warn('shard restarted (bootId changed) — clearing online roster', {
|
||||
from: state.bootId,
|
||||
to: incoming,
|
||||
@@ -116,6 +117,23 @@ async function applyStateChange(event, deps) {
|
||||
}
|
||||
if (incoming) state.bootId = incoming
|
||||
await uoLinkConfig.recordStatus({ pluginConnected: true, bootId: incoming, lastEventAt: event.t })
|
||||
if (restarted) {
|
||||
// EVENTS.md F: core has no concept of the game being up, so the module
|
||||
// says when a ledger of live shard resources has become a claim about a
|
||||
// world that no longer exists. This is that moment, and a changed
|
||||
// `bootId` is the only thing that distinguishes it from a sidecar
|
||||
// reconnect — which changes nothing in the game and must not orphan a row.
|
||||
//
|
||||
// **After `recordStatus`, and that ordering is load-bearing.** Every
|
||||
// action's `reconcile()` decides what is still in force by comparing its
|
||||
// stamp against the CURRENT boot id, which it reads back out of this
|
||||
// row. Asking first would have every resource compared against the boot
|
||||
// that has just ended, and every one of them would look live.
|
||||
//
|
||||
// Fire-and-forget by the contract: core logs what it orphaned, and there
|
||||
// is nothing an ingest handler could correctly do with the answer.
|
||||
eventsReconcile()
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'server.shutdown':
|
||||
@@ -292,6 +310,11 @@ function resolveDeps(deps) {
|
||||
broadcast: deps.broadcast || broadcaster.broadcast,
|
||||
pushDispatch: deps.pushDispatch || shardPush.fromShardEvent,
|
||||
engagement: deps.engagement || shardEngagement.fromShardEvent,
|
||||
// MODULE_API 1.10.0 (EVENTS.md F, Phase 8). Injectable for the same reason
|
||||
// every member above is: a test that asserted a shard restart triggers a
|
||||
// reconcile must be able to see the call without a live event engine behind
|
||||
// it.
|
||||
eventsReconcile: deps.eventsReconcile || (() => coreEvents.reconcile()),
|
||||
log: deps.log || defaultLog,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user