The kit was pinned to website 963d734 -- MODULE_API 1.6.0, the Teams cutover --
and the platform is on 1.9.0. Three registrations and two calls arrived in
between, and a reader building against this book would have found no mention of
any of them: a module can now declare what its game can announce, and never who
is told.
Moving `ci/core-ref.json` is the mechanism for exactly this. The pin is now
66bb3b9a (website `main`, the engagement cutover) and `template/module.json`
declares `^1.9.0`.
What chapter 2 gained, under "Telling core something happened":
* a TRIGGER is a payload contract, not a notification stream -- the two share
one id namespace and are constantly confused;
* `ceiling` is required, has no default, and is a CONTAINMENT tree rather than
a size ladder (a `staff` ceiling does not permit `owner`);
* an AUDIENCE resolver returns user ids and nothing else, resolves to NOBODY
on failure, and takes CONSTANT params -- the constraint worth knowing before
you design around it;
* templates re-ensure per seedVersion, rule groups are offered ONCE per group
key, so a rule appended to an existing group reaches fresh installs only;
* `ctx.events.emit` binds the owner and is fire-and-forget; `ctx.inbox.push`
is the direct write, for when there is nothing for an operator to decide.
The template builds all of it: one trigger, one audience over the clan roster it
already had, one seeded body and one seeded rule group, and an emitter in
`boot.js` that fires on the TRANSITION rather than on the poll. Seven new tests,
including the audience that resolves to nobody when its query throws.
Three claims were wrong and are corrected here rather than shipped:
* core validates `subjectKey` against the declared variables and refuses the
module; the draft taught a cooldown keyed on `undefined`, which the check
exists to prevent and a reader will never see.
* `emit` throws OUTSIDE production and only drops-and-logs inside it. Teaching
the second half alone leaves a developer meeting a throw the book says
cannot happen.
* the seeded body itself was malformed -- heading `level: 2` where the block
registry takes 'h2', and no block ids at all.
The third is the one worth keeping: `registerEngagementSeeds` checks that
`blocks` is a non-empty array and stops, so that body would have registered,
seeded, and failed the first time an operator opened it. Found by running the
template's `register()` through core's real registry at the pinned ref -- which
CI does not do, and cannot: the template job checks the version and runs the
template against fakes. A fake accepts what core refuses. The gap is now named
in the chapter, beside the code, and in the pin's own comment, and the rule that
bit has a test that fails on it.
Also: `checkLinks` skipped `.core/`. Bumping this pin means cloning core into
that directory first, and the walk then reported nine broken links in someone
else's README. CI never saw it -- the clone happens in the `template` job and
the check runs in `prose` -- so it was a failure only a person could meet.
Co-Authored-By: Claude <noreply@anthropic.com>
115 lines
5.5 KiB
JavaScript
115 lines
5.5 KiB
JavaScript
// ── Everything this module reaches in core ─────────────────────────────────
|
|
//
|
|
// `ctx` arrives once, as an argument to `register()` (MODULE_API.md §2.3). The
|
|
// code beneath it — models, controllers, utilities — is ordinary Node that
|
|
// requires its dependencies at file scope, the way any Node file does. This file
|
|
// is what lets both of those be true at the same time.
|
|
//
|
|
// **Every export is a lazy accessor, not a stored reference, and that is the
|
|
// whole point.** A model 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` at that moment would hand
|
|
// out `undefined`, permanently, and the failure would surface much later as a
|
|
// TypeError inside a model with no clue pointing here. So each member resolves
|
|
// `ctx` when it is CALLED. Require order stops mattering for everything except
|
|
// `core.init()` itself, which `index.js` runs first.
|
|
//
|
|
// The same rule in the other direction: **never destructure off `ctx` at init
|
|
// time.** 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 the same message. The only ways to
|
|
// reach one before `register()` are a require cycle or a test that forgot to call
|
|
// `init`, and both want naming rather than `undefined`.
|
|
//
|
|
// ── This file is a NARROWING, on purpose ───────────────────────────────────
|
|
//
|
|
// §2.3 lists everything core hands over. What is re-exported below is only what
|
|
// this module actually uses, which is the discipline worth copying: the file is
|
|
// then an honest statement of what your module depends on, and a test double for
|
|
// it (see `test/_fakes.js`) is a complete one. Add a member here when you reach
|
|
// for it — not in advance.
|
|
|
|
let ctx = null
|
|
|
|
function need() {
|
|
if (!ctx) {
|
|
throw new Error('examplegame: 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()`.
|
|
//
|
|
// A file writes `const log = require('../core').logger('world')` at file scope,
|
|
// so the object returned has to exist before `ctx` does. It is a façade whose
|
|
// four methods each resolve the real logger when called. Core namespaces the
|
|
// output with your module id, so these come out as `[examplegame:world]`.
|
|
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 could not resolve these for itself even if it were allowed
|
|
// to — it lives outside core's `server/` (§7.2).
|
|
get express() { return need().express },
|
|
get validator() { return need().validator },
|
|
|
|
// The database. `query(sql, params)` is what every `*.db.js` file uses; raw
|
|
// parameterised SQL, no ORM, the same as core. `pool` is there for the rare
|
|
// case that needs a connection it can hold (a streamed import, say).
|
|
query: (...args) => need().db.query(...args),
|
|
get pool() { return need().db.pool },
|
|
|
|
// Read-only access to who is asking. Minting a session is core's job; a module
|
|
// that needs an identity needs to *read* one.
|
|
auth: { getUserFromRequest: (...args) => need().auth.getUserFromRequest(...args) },
|
|
|
|
// Core's middleware, taken as values rather than wrapped: express stores the
|
|
// function reference at mount time, so a wrapper is what would end up in the
|
|
// stack. Routers are built inside `register()`, so `ctx` is set by then.
|
|
get middleware() { return need().middleware },
|
|
|
|
// Firing a declared event (MODULE_API.md §2.3). Wrapped as a call rather than
|
|
// exposed as `get events()`, so that `require('../core').emit` taken at file
|
|
// scope still resolves `ctx` at call time like everything else here.
|
|
//
|
|
// **It returns nothing, and in production it never throws at the caller.** The
|
|
// emit is the end of this module's involvement: core validates the payload
|
|
// against the declared contract, decides which rules match, resolves who they
|
|
// reach and sends. A module cannot address a person, choose a channel or write
|
|
// a subject line, and this seam is deliberately too narrow to try (§2.7).
|
|
//
|
|
// Outside production a bad payload throws here rather than being logged, which
|
|
// is the point: you meet the mismatch in your own tests instead of in an
|
|
// operator's log six weeks later.
|
|
emit: (triggerId, envelope) => need().events.emit(triggerId, envelope),
|
|
|
|
// Deployment facts. `moduleRoot` is the absolute path to `modules/<id>/` — the
|
|
// only correct way to find a file you shipped, because the working directory is
|
|
// core's and the module's location is the loader's business.
|
|
get moduleRoot() { return need().paths.moduleRoot },
|
|
get moduleId() { return need().moduleId },
|
|
}
|