docs(website): record slice 1, and MODULE_API 1.1.0
The whole server half is out: 40 files, ~9,674 lines, 27 of 68 tables. The acceptance criterion held exactly -- core's manifest goes 228 to 158 public routes and the 70 that left reappear byte-identical once the module loads, with routes.guards identical across all 228. The contract grew to 1.1.0: ctx.activity.log, ctx.users.getById, ctx.site.baseUrl, ctx.middleware.rateLimit + accountChangeLimiter, and a fourth registry, registerPostHook. Each is documented with why it could not be vendored, because that reasoning is the useful part -- an admin action a module performs belongs in core's ONE audit log, a second rate-limit store is a limit enforced by two counters, and core's CMS was calling a UO file directly. §2.7.1 gains the slice record: the core.js port mechanism and its consequence (require order is load-bearing), the vendoring line (pure leaf helpers may be copied, security controls may not), the two core defects the extraction exposed, the one deliberate behaviour change, and the one test that looked like it should move and should not. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -26,9 +26,20 @@ here extends the contract first, in this file, before the module is written agai
|
||||
Core exports a single integer-major semver string from `server/src/modules/version.js`:
|
||||
|
||||
```js
|
||||
const MODULE_API_VERSION = '1.0.0'
|
||||
const MODULE_API_VERSION = '1.1.0'
|
||||
```
|
||||
|
||||
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.1.0 — Phase 3 slice 1.** `ctx` gained `activity.log`, `users.getById`, `site.baseUrl`, and
|
||||
`middleware.rateLimit` + `middleware.accountChangeLimiter`; `api` gained `registerPostHook`.
|
||||
Additions only. Each exists because module-uo's extraction needed it and none could be vendored — an
|
||||
admin action a module performs belongs in core's one audit log, the extension slot needs the user its
|
||||
prefix names, §2.7 forbids a module reading core's `APP_BASE_URL`, a second rate-limit store is a
|
||||
limit enforced by two counters, and core's CMS was calling a UO file directly.
|
||||
|
||||
Every `module.json` declares a `coreApi` semver **range**. The loader checks it at boot, before it
|
||||
requires a line of module code, and a mismatch fails that module loudly into `startup_failed`
|
||||
(§4.4) with the two versions in the reason. It never silently proceeds.
|
||||
@@ -128,6 +139,11 @@ module-uo does not need is on the list.
|
||||
| `ctx.uploads` | `{ upload, UPLOAD_DIR, MIME_EXT }` | `admin/imageUpload.js` | atlas art import |
|
||||
| `ctx.posts` | `{ listAll, getById, linkAnnounceJob, markAnnounced }` | `model/posts` | `newsGump.js:108`, `announceWorker.js:58` |
|
||||
| `ctx.paths.moduleRoot` | absolute path to `modules/<id>/` | loader | atlas art, cliloc files |
|
||||
| `ctx.activity.log` | `({ req, action, detail }) => Promise<void>` | `model/activity` | every admin UO controller (1.1.0) |
|
||||
| `ctx.users.getById` | `(id) => Promise<user\|null>` | `model/users` | `usersShard.controller` (1.1.0) |
|
||||
| `ctx.site.baseUrl` | getter, string with no trailing slash | `APP_BASE_URL` | `shardAnnounce` (1.1.0) |
|
||||
| `ctx.middleware.rateLimit` | `(options) => middleware` | `middleware/rateLimit` | the market search (1.1.0) |
|
||||
| `ctx.middleware.accountChangeLimiter` | middleware | `middleware/rateLimit` | `player/shard.router` (1.1.0) |
|
||||
| `ctx.moduleId` | the id from `module.json` | loader | log tags, table checks |
|
||||
|
||||
Three narrowings from `MODULE_SYSTEM.md` §2.1, all deliberate:
|
||||
@@ -159,6 +175,7 @@ api.registerRoutes({ public: {...}, admin: {...}, player: {...} })
|
||||
api.registerExtension(slot, router)
|
||||
api.registerNotificationStreams(streams)
|
||||
api.registerAnnounceLeg({ leg, label, dispatch, classify })
|
||||
api.registerPostHook({ onSaved, onDeleted })
|
||||
api.onBoot(async (ctx) => {})
|
||||
api.onShutdown(async () => {})
|
||||
```
|
||||
@@ -240,6 +257,24 @@ next_attempt_at)` and `leg` is a stored value. The parent `status` rollup is ove
|
||||
legs — done when every leg delivered, failed when every leg gave up, partial in between; and `done`
|
||||
when a job has no legs at all, since nothing is left to deliver.
|
||||
|
||||
**`registerPostHook({ onSaved, onDeleted })`** — added in API 1.1.0. Core's CMS is the only writer of
|
||||
posts, and a module may need to mirror one somewhere core knows nothing about. `onSaved` receives
|
||||
`{ post, transition }` — the same transition `registerAnnounceLeg` fires on — and `onDeleted`
|
||||
receives `{ post, id }`. Both are optional; a registration with neither is refused, since it is a
|
||||
subscription that can never fire. One hook per registrant.
|
||||
|
||||
Every hook is awaited and none may throw past core: a subscriber's failure is logged and costs
|
||||
neither another subscriber nor the save itself. A sidecar hiccup breaking a post edit would be a
|
||||
worse bug than a stale mirror.
|
||||
|
||||
**It is deliberately not part of `registerAnnounceLeg`**, which fires on the same transition. A leg
|
||||
is a one-shot *delivery* with retry and classification; a post hook maintains idempotent *state*, has
|
||||
to run on delete as well as save, and refreshes silently on an edit. Overloading the leg would have
|
||||
meant a `dispatch` that must not be retried and a `classify` that means nothing.
|
||||
|
||||
Before it existed, core's post controller required `utils/newsGump` directly — core's publish path
|
||||
naming a UO file, and the last thing binding core to the module.
|
||||
|
||||
**`onBoot(fn)` / `onShutdown(fn)`** — §2.5.
|
||||
|
||||
### 2.5 Lifecycle
|
||||
|
||||
Reference in New Issue
Block a user