feat(events): an expired ledger status and ctx.events.expired (MODULE_API 1.11.0)
A game that ends a resource at its own deadline — a Rust zone erased when its
time is up — could reach core only through ctx.events.reconcile(), which files
it `orphaned`: amber, "it vanished", and still claimable for a revert. The new
call files it as the plan working (Rust PLAN_FIXES F14, D170, D183):
- event_run_resources.status gains `expired`, terminal like `reverted`: the
sweep never takes it back, live_marker releases the target, and it joins
neither HELD nor UNRESOLVED. The ENUM ALTER re-runs as a no-op on every boot
(checked on MariaDB 11.8 with the stored generated column depending on it).
- ctx.events.expired({ kind, ref }) marks the calling module's own pending,
confirmed or orphaned rows for that target expired and logs
`resource.expired`; a revert in flight is left to finish. The owner is bound
by the loader, like reconcile. A finished run whose last unresolved row this
was goes to cleanup `complete`, even from `incomplete`.
- The run console shows it green, "ended by the game on time".
Additions only, so minor. Module-uo (coreApi ^1.10.0) calls none of it and
reads no ledger status; the contract test now asserts the range still holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -495,6 +495,7 @@ test('ctx exposes exactly the documented surface, and is frozen', () => {
|
||||
fs.writeFileSync(${JSON.stringify(seen)}, JSON.stringify({
|
||||
keys: Object.keys(ctx).sort(),
|
||||
middleware: Object.keys(ctx.middleware).sort(),
|
||||
events: Object.keys(ctx.events).sort(),
|
||||
moduleId: ctx.moduleId,
|
||||
mutable,
|
||||
}))
|
||||
@@ -530,10 +531,42 @@ test('ctx exposes exactly the documented surface, and is frozen', () => {
|
||||
assert.deepEqual(probe.middleware, [
|
||||
'accountChangeLimiter', 'noindex', 'rateLimit', 'requireAuth', 'requireRole', 'siteMode', 'validate',
|
||||
])
|
||||
// API 1.10.0 gave `events` its `reconcile`, and 1.11.0 its `expired` (Rust
|
||||
// PLAN_FIXES D183): the game ended a ledgered resource at its own deadline.
|
||||
assert.deepEqual(probe.events, ['emit', 'expired', 'reconcile'])
|
||||
assert.equal(probe.moduleId, 'probe')
|
||||
assert.equal(probe.mutable, false, 'ctx members must be frozen')
|
||||
})
|
||||
|
||||
test("ctx.events.expired closes the calling module's rows, whatever it passes", async () => {
|
||||
// The owner is bound by the loader, never read from the arguments — the same
|
||||
// rule `reconcile` keeps. Without it a module could mark another module's
|
||||
// resources expired and take them off the teardown list.
|
||||
const cleanup = require('../src/events/cleanup')
|
||||
const original = cleanup.expireResource
|
||||
const calls = []
|
||||
cleanup.expireResource = async (owner, resource) => {
|
||||
calls.push({ owner, resource })
|
||||
return { expired: 1 }
|
||||
}
|
||||
try {
|
||||
writeModule('zoner', {
|
||||
server: `module.exports = (ctx) => {
|
||||
const result = ctx.events.expired({ kind: 'zone', ref: '3:rg-1', owner: 'someone-else', owner_module: 'x' })
|
||||
if (result !== undefined) throw new Error('expired must return undefined')
|
||||
}`,
|
||||
})
|
||||
assert.equal(stateOf(freshLoader(tmpRoot), 'zoner').state, 'registered')
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].owner, 'zoner')
|
||||
assert.equal(calls[0].resource.kind, 'zone')
|
||||
assert.equal(calls[0].resource.ref, '3:rg-1')
|
||||
} finally {
|
||||
cleanup.expireResource = original
|
||||
}
|
||||
})
|
||||
|
||||
// ── Lifecycle hooks ────────────────────────────────────────────────────────
|
||||
|
||||
test('a lifecycle hook must be a function, and may be registered once', () => {
|
||||
|
||||
Reference in New Issue
Block a user