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>
135 lines
6.2 KiB
JavaScript
135 lines
6.2 KiB
JavaScript
// ── Everything this module reaches in core ─────────────────────────────────
|
|
//
|
|
// `ctx` arrives once, as an argument to `register()` (MODULE_API.md §2.3). The
|
|
// code below it — models, utils, controllers — is ordinary Node that requires
|
|
// its dependencies at file scope, the way it did when it lived in core. This
|
|
// file is what lets both be true.
|
|
//
|
|
// **The shape is a lazy accessor, not a stored reference, and that is the whole
|
|
// point.** A ported file writes
|
|
//
|
|
// const { query } = require('../../core')
|
|
//
|
|
// at require time, which is before `register()` has been called and therefore
|
|
// before any `ctx` exists. Handing out `ctx.db.query` there would hand out
|
|
// `undefined`, permanently, and the failure would surface much later as a
|
|
// TypeError inside a model. So every export here is a stable function that
|
|
// resolves `ctx` when it is CALLED. Require order stops mattering, and the port
|
|
// stays a one-line import change per file rather than a signature change per
|
|
// function.
|
|
//
|
|
// The other half of the same rule: nothing here may be destructured off `ctx`
|
|
// at init time either, for the same reason in the other direction — core is
|
|
// free to hand over a getter (`ctx.site.baseUrl` is one), and a value captured
|
|
// once is a value that cannot change.
|
|
//
|
|
// If `ctx` is missing, every accessor throws with the same message. That is
|
|
// deliberate: the only way to reach one before `register()` is a require cycle
|
|
// or a test that forgot to call `init`, and both want naming, not `undefined`.
|
|
|
|
let ctx = null
|
|
|
|
function need() {
|
|
if (!ctx) {
|
|
throw new Error('module-uo: core accessed before register() — see server/core.js')
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
/** Called once, first thing in `register()`. */
|
|
function init(value) {
|
|
ctx = value
|
|
}
|
|
|
|
/** Test seam. Nothing in the module calls this; there is no de-registration. */
|
|
function _reset() {
|
|
ctx = null
|
|
}
|
|
|
|
// A logger that can be taken at require time and used after `register()`.
|
|
//
|
|
// Ported files write `const log = require('../core').logger('shard-ingest')` at
|
|
// file scope — the same shape as core's `require('./logger')('…')` — so the
|
|
// object returned has to exist before `ctx` does. It is a façade whose four
|
|
// methods each resolve the real logger on call. Core namespaces it with the
|
|
// module id, so these come out as `[uo:shard-ingest]`.
|
|
function logger(namespace) {
|
|
const call = (level) => (message, meta) => need().log(namespace)[level](message, meta)
|
|
return { error: call('error'), warn: call('warn'), info: call('info'), debug: call('debug') }
|
|
}
|
|
|
|
module.exports = {
|
|
init,
|
|
_reset,
|
|
logger,
|
|
|
|
// Shared server dependencies. Core owns exactly one express, as it owns
|
|
// exactly one React on the client, and for the same reason: a second copy in
|
|
// the process is a second Router prototype and a second set of instanceof
|
|
// checks. A module lives outside core's `server/`, so it could not resolve
|
|
// these for itself even if it were allowed to (§7.2).
|
|
get express() { return need().express },
|
|
get validator() { return need().validator },
|
|
|
|
// Database. `query` is the one every `*.db.js` uses; `pool` is for the
|
|
// streamed atlas import, which needs a connection it can hold.
|
|
query: (...args) => need().db.query(...args),
|
|
get pool() { return need().db.pool },
|
|
|
|
// Core state a module may read or append to, each narrowed to what is
|
|
// actually used (§2.3).
|
|
settings: {
|
|
get: (...args) => need().settings.get(...args),
|
|
set: (...args) => need().settings.set(...args),
|
|
getInstanceName: (...args) => need().settings.getInstanceName(...args),
|
|
},
|
|
activity: { log: (...args) => need().activity.log(...args) },
|
|
users: { getById: (...args) => need().users.getById(...args) },
|
|
posts: {
|
|
listAll: (...args) => need().posts.listAll(...args),
|
|
getById: (...args) => need().posts.getById(...args),
|
|
linkAnnounceJob: (...args) => need().posts.linkAnnounceJob(...args),
|
|
markAnnounced: (...args) => need().posts.markAnnounced(...args),
|
|
},
|
|
auth: { getUserFromRequest: (...args) => need().auth.getUserFromRequest(...args) },
|
|
push: { publish: (...args) => need().push.publish(...args) },
|
|
|
|
// The engagement seam (MODULE_API 1.7.0, ENGAGEMENT.md §5.1). `emit` says an
|
|
// event this module DECLARED has happened; the engine decides whether anyone is
|
|
// told, on which channel, subject to which rule and preference. `inbox.push`
|
|
// writes an in-app item with no rule at all, for the cases that are not events.
|
|
//
|
|
// Both are fire-and-forget and return undefined by contract — a module calls
|
|
// them from inside a game-event handler and there is nothing it could correctly
|
|
// do with a storage failure of core's. `inbox.push` additionally does not report
|
|
// "the user has this switched off", because a module that could see that would
|
|
// be a module that could enumerate people's preferences one write at a time.
|
|
events: {
|
|
emit: (...args) => need().events.emit(...args),
|
|
// MODULE_API 1.10.0 (EVENTS.md F, Phase 8). "Ask every action of mine which
|
|
// of its ledgered resources the game still has." Core cannot know when to
|
|
// ask -- it has no concept of the game being up -- so the module says when,
|
|
// and `shardIngest` says it on a changed `bootId`. Fire-and-forget like
|
|
// `emit`, and for the same reason: core owns what happens next and there is
|
|
// nothing a game-event handler could correctly do with the answer.
|
|
reconcile: (...args) => need().events.reconcile(...args),
|
|
},
|
|
inbox: { push: (...args) => need().inbox.push(...args) },
|
|
secretBox: {
|
|
encrypt: (...args) => need().secretBox.encrypt(...args),
|
|
decrypt: (...args) => need().secretBox.decrypt(...args),
|
|
},
|
|
get uploads() { return need().uploads },
|
|
|
|
// Middleware. Taken as values rather than wrapped, because express stores the
|
|
// function reference at mount time — a wrapper would be what ends up in the
|
|
// stack, and `requireRole('admin')` returns a middleware rather than being
|
|
// one. Routers are built inside `register()`, so `ctx` is set by then.
|
|
get middleware() { return need().middleware },
|
|
|
|
// Deployment facts.
|
|
get baseUrl() { return need().site.baseUrl },
|
|
get moduleRoot() { return need().paths.moduleRoot },
|
|
get moduleId() { return need().moduleId },
|
|
}
|