module-uo's half of ENGAGEMENT.md Phase 11: every trigger DECLARATION, the
wire-kind mapping that fires them, and the three registered audiences. No rule
and no template is seeded here -- that is 11b -- so nothing this adds sends
anybody anything until an operator writes a rule.
server/config/shardTriggers.js declares the 24, grouped by the audience kind
each family exercises, and every variable carries the `example` the template
editor previews and test-sends with. Ceilings: 10 `owner`, 2 `members`, 7
`authenticated`, 2 `staff`, 3 `admin` (the value core adds in the same window).
`uo.cheat.detected` at `staff` is the declaration the lattice exists for.
server/utils/shardEngagement.js maps the wire to those ids, hung off
shardIngest.ingest beside the SSE broadcast and the push tickle, and reads like
shardPush.js on purpose -- owner resolution is why neither can be a pure mapper.
Three things live here because a rule cannot express them:
* Transitions. champ.update and city.update are full-state upserts, so without
a per-process tracker a sidecar reconnect reads as twenty spawns starting.
A FIRST sighting is never a transition.
* Thresholds. conditions.js compares a declared variable against a LITERAL, so
"within 24 hours of dismissal" is not expressible; and vendor.listing is a
sweep frame re-emitted on any price change, so per-frame would flood. The
crossing is tracked here and `hoursRemaining` is declared so an operator can
still narrow with `is at most`.
* The members audience. "The members of THIS guild" differs every firing, so
it travels on the envelope as recipientUserIds (Phase 6 decision 2).
**The fan-out runs BEFORE the state write, and that ordering is load-bearing.**
account.unlinked drops the shard_account_links row that names the one person who
needs to be told; house.remove drops the house whose stored ownerAcct is the only
place a collapsed house's owner appears; guild.leave/remove need the roster and
board mirrors to name who left. Resolving afterwards finds nobody, every time.
Four rows of 8.6 deliberately do not ship, each with its reason recorded in
docs (docs#194): uo.market.item_listed (a saved search, no per-user query store),
uo.guild.joined (core's team.member.joined already fires for it -- a UO guild IS
a Team and this module is the provider), uo.link.requested (no addressable
recipient by construction, ~5-minute TTL), and uo.points.rank_changed's personal
half (top[] names a serial, links are keyed by account).
coreApi -> ^1.8.0: the module now calls registerEventTriggers and declares
`ceiling: 'admin'`, so a 1.7.0 core would refuse the ceiling and a 1.6.0 one
would not have the method at all.
39 new tests; 509/509 pass. check:imports, check:bundle and check:swagger clean.
Co-Authored-By: Claude <noreply@anthropic.com>
126 lines
5.7 KiB
JavaScript
126 lines
5.7 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) },
|
|
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 },
|
|
}
|