The entry point becomes real: five mount prefixes, the admin.users.detail extension slot, the shard push catalog, the town-crier announce leg and both lifecycle hooks. module.json declares all of it and the loader checks the declaration against what register() actually registers, in both directions. The URLs are byte-identical to the ones core served before the extraction. That is the whole point of moving the code and not the paths: the shipped Android app calls POST /api/v1/admin/shard/kick and the Discord bot reads /api/v1/public/shard/*, and neither knows a module answers now. Require order is load-bearing and the requires are inside register() because of it. Every ported file reaches core through ./core, whose members resolve ctx when called -- but a router does `const express = core.express` at ITS file scope, which runs the moment it is required. Hoisting these to the top of the file breaks the module with an error about ctx being missing, from a file that never mentions it. boot.js takes the eight UO call sites out of core's server.js. One behavioural change, deliberate: uoLinkSocket.start() and the sidecar health probe used to run AFTER the listener bound and now run before it, because onBoot does. start() returns as soon as the reconnecting client is armed, but the probe is a real HTTP call, so it is fired and NOT awaited -- an unreachable sidecar must not hold the site closed. Reporting that the bridge is down is diagnostics; being up is not a precondition for serving a page. router/rateLimits.js builds the market limiter through ctx.middleware.rateLimit, core's factory. The policy is the module's -- only the module knows what its endpoints cost -- and the plumbing is core's, so there is one express-rate-limit in the process and one place a breach is logged. Co-Authored-By: Claude <noreply@anthropic.com>
122 lines
6.0 KiB
JavaScript
122 lines
6.0 KiB
JavaScript
// ── onBoot / onShutdown ────────────────────────────────────────────────────
|
|
//
|
|
// The eight UO call sites that used to sit in core's `server.js`. `register()`
|
|
// runs with no database (MODULE_API.md §2.2); everything here runs with one.
|
|
//
|
|
// Core dispatches `onBoot` after `ensureSchema` and the schema-fragment replay,
|
|
// and **before the HTTP listener binds** — so the tables these functions touch
|
|
// exist, and nothing is served until the warm-up finishes. That ordering is the
|
|
// contract's promise rather than an accident, and it is why `onBoot` has no
|
|
// timeout: a module that must not serve traffic until a cache is warm only gets
|
|
// that guarantee if the listener is still closed.
|
|
//
|
|
// **One behavioural change, and it is deliberate.** In core, `uoLinkSocket.start()`
|
|
// and the sidecar health probe ran AFTER the listener bound; here they run before
|
|
// it. `start()` returns as soon as the reconnecting client is armed, so that part
|
|
// is free — but the probe is a real HTTP call to the sidecar, and an unreachable
|
|
// sidecar must not hold the site closed. It is therefore fired and NOT awaited,
|
|
// with its own catch. Reporting whether the bridge is up is diagnostics; being up
|
|
// is not a precondition for serving a page, and the site is required to degrade
|
|
// gracefully when the shard is down.
|
|
//
|
|
// Everything here is best-effort by the same rule. A module whose `onBoot`
|
|
// throws is marked `startup_failed` and its routes answer 503 (§4.4), which is
|
|
// the right outcome for a broken module — but "the operator has not configured a
|
|
// ServUO path" is not a broken module, and neither is "the shard is offline".
|
|
|
|
const core = require('./core')
|
|
|
|
const uoLinkSocket = require('./utils/uoLinkSocket')
|
|
const uoLinkClient = require('./utils/uoLinkClient')
|
|
const uoLinkConfig = require('./model/uoLinkConfig/uoLinkConfig.model')
|
|
const shardBroadcast = require('./utils/shardBroadcast')
|
|
const shardAtlas = require('./model/shardAtlas/shardAtlas.model')
|
|
const shardClilocs = require('./model/shardClilocs/shardClilocs.model')
|
|
const shardMarket = require('./model/shardMarket/shardMarket.model')
|
|
|
|
/**
|
|
* Best-effort startup probe of the uo-link sidecar.
|
|
*
|
|
* Logs whether it is reachable and warns loudly on a protocol mismatch —
|
|
* fail-fast visibility rather than silently mis-parsing a newer wire format.
|
|
* Never throws, and is never awaited by `onBoot`.
|
|
*/
|
|
async function checkUoLink() {
|
|
const log = core.logger('boot')
|
|
const config = await uoLinkConfig.getSafe()
|
|
if (!config.enabled) return
|
|
const health = await uoLinkClient.health()
|
|
if (!health.ok) {
|
|
log.warn('uo-link is enabled but the sidecar is unreachable at startup', {
|
|
baseUrl: config.baseUrl,
|
|
error: health.error || `status ${health.status}`,
|
|
})
|
|
return
|
|
}
|
|
if (health.data && health.data.protocol && health.data.protocol !== config.protocol) {
|
|
log.error('uo-link PROTOCOL MISMATCH — pinned vs sidecar', {
|
|
pinned: config.protocol,
|
|
sidecar: health.data.protocol,
|
|
})
|
|
} else {
|
|
log.info('uo-link sidecar reachable', {
|
|
pluginConnected: health.data && health.data.plugin_connected,
|
|
protocol: health.data && health.data.protocol,
|
|
})
|
|
}
|
|
}
|
|
|
|
async function onBoot() {
|
|
const log = core.logger('boot')
|
|
|
|
// Re-derive the spawn atlas from the shard's own ServUO tree. The shard's maps
|
|
// change over its lifetime — facets get added, replaced or renamed — so the
|
|
// atlas is rebuilt on every boot rather than shipped as a snapshot that would
|
|
// silently go stale. Hash-gated, so an unchanged tree costs one read pass and
|
|
// no database write.
|
|
//
|
|
// Best-effort by contract: no configured path, an unreadable mount or a
|
|
// malformed file must never stop the site coming up. A refresh that would
|
|
// REMOVE a facet is staged for admin approval instead of being applied.
|
|
await shardAtlas.refreshOnBoot()
|
|
|
|
// Refresh the cliloc table (UO's id → display-string map) from the file the
|
|
// operator converted out of their own client. Same contract as the atlas:
|
|
// hash-gated so an unchanged file costs one read, and best-effort so a missing
|
|
// or wrong-format file never stops the site coming up — it just means item
|
|
// names render as ids, which is what they did before the table existed.
|
|
const clilocResult = await shardClilocs.refreshOnBoot()
|
|
|
|
// A cliloc import changes what item names RESOLVE to, and the marketplace
|
|
// stores those names denormalized (shard_vendor_items.display_name) so it can
|
|
// index and search them. The shard's market sweep will not re-send an unchanged
|
|
// shop just because the site learned what its items are called, so the backfill
|
|
// has to be pulled rather than waited for. Only after an actual import — the
|
|
// common boot is hash-gated to a no-op and must stay one.
|
|
if (clilocResult && clilocResult.status === 'imported') await shardMarket.refreshDisplayNames()
|
|
|
|
// Start the uo-link WebSocket ingest client. Self-guards: it only actually
|
|
// connects when the admin has enabled the integration and saved a token, so
|
|
// this is a no-op on shards that haven't configured the sidecar.
|
|
try {
|
|
await uoLinkSocket.start()
|
|
} catch (err) {
|
|
log.warn('uo-link socket failed to start (continuing)', { error: err.message })
|
|
}
|
|
|
|
// Deliberately not awaited — see the header. An unreachable sidecar would
|
|
// otherwise hold the listener closed for the length of an HTTP timeout.
|
|
checkUoLink().catch((err) => log.warn('uo-link startup probe failed', { error: err.message }))
|
|
}
|
|
|
|
async function onShutdown() {
|
|
// Core runs this FIRST in its signal handler, while everything it handed over
|
|
// still works — the pool is open, the push dispatcher is up, the SSE fan-out
|
|
// is live. It is the only chance to close cleanly, and it is budgeted, so a
|
|
// hook that will not let go costs five seconds rather than the whole shutdown.
|
|
uoLinkSocket.stop() // close the uo-link WS ingest client
|
|
shardBroadcast.closeAll() // end any open shard live-feed SSE streams
|
|
}
|
|
|
|
module.exports = { onBoot, onShutdown, checkUoLink }
|