chore(quality): resolve SonarQube code smells across website
Clears the 124 CODE_SMELL findings from the SonarQube scan (server, client, and bot). All changes are behaviour-preserving refactors — no route, protocol, schema, or config changes — verified against the full server (381) and client (43) test suites plus a clean client build. By rule: - S3776 (20, cognitive complexity): extract helpers/handlers so each function drops under the threshold — shard model upsert builders, page/wiki update, block validation, notification stream mapping (dispatch table), SSO mobile login, shard ingest deps, uo-link socket backfill/connect, the bot slash- command dispatchers + discord manager, and the Shard/UserDetail/HeroEditor/ CharacterStats React components. - S4624 (34, nested template literals): pull inner templates into locals / a withQs() helper; rewrite shardEvents.describe() as a formatter table. - S3358 (35, nested ternaries): lift to if/else vars, lookup maps, small components, or guarded JSX expressions. - S6479 (12, array-index React keys): key by stable content instead of index (two in-editor lists left as-is; index matches their by-index edit model). - S6353 (6): [0-9]/[^0-9] -> \d/\D. S125 (5): reword state-shape comments that parsed as code. S3800/S3782 (botScore): JSDoc-type PATH_WEIGHTS tuples. - S6481 (2): memoize Auth/Site context values (and SiteContext brand). - S4144: dedupe HeroEditor upload handler into useImageUpload(). - S1126 (2), S6035, S5869 (redundant A-Z under /i), S5843 (town-name regex -> prefix list): assorted one-liners. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,39 +43,40 @@ function buildUrl(wsUrl, token) {
|
||||
return token ? `${wsUrl}${sep}token=${encodeURIComponent(token)}` : wsUrl
|
||||
}
|
||||
|
||||
// One guarded board snapshot: fetch, verify `data[key]` is an array, hand it to
|
||||
// `apply`, and (when given) log `label` with the row count. Isolated so a
|
||||
// failed/absent board never aborts the rest of backfill — and so backfill()
|
||||
// stays a flat sequence rather than nine repetitions of the same guard.
|
||||
async function snapshot(fetchFn, key, apply, label) {
|
||||
const res = await fetchFn()
|
||||
if (!res.ok || !res.data || !Array.isArray(res.data[key])) return
|
||||
await apply(res.data[key])
|
||||
if (label) log.info(label, { count: res.data[key].length })
|
||||
}
|
||||
|
||||
// Replay events through the dispatcher oldest-first (history/economy arrive
|
||||
// newest-first) so latest-wins state settles correctly.
|
||||
async function ingestReversed(events) {
|
||||
for (const ev of [...events].reverse()) await shardIngest.ingest(ev, { fromBackfill: true })
|
||||
}
|
||||
async function ingestEach(events) {
|
||||
for (const ev of events) await shardIngest.ingest(ev, { fromBackfill: true })
|
||||
}
|
||||
|
||||
// Pull recent events from the sidecar's own store and replay them through the
|
||||
// dispatcher (fromBackfill = no SSE re-broadcast). dedupe_key + INSERT IGNORE
|
||||
// make this idempotent, so overlap with what we already stored is harmless.
|
||||
async function backfill() {
|
||||
try {
|
||||
const hist = await uoLinkClient.getHistory({ limit: BACKFILL_LIMIT })
|
||||
if (hist.ok && hist.data && Array.isArray(hist.data.events)) {
|
||||
// History is newest-first; replay oldest-first so latest-wins state (e.g.
|
||||
// house.decay stage) settles correctly.
|
||||
const events = [...hist.data.events].reverse()
|
||||
for (const ev of events) await shardIngest.ingest(ev, { fromBackfill: true })
|
||||
log.info('backfilled events from /history', { count: events.length })
|
||||
}
|
||||
const eco = await uoLinkClient.getEconomy(200)
|
||||
if (eco.ok && eco.data && Array.isArray(eco.data.series)) {
|
||||
const series = [...eco.data.series].reverse()
|
||||
for (const ev of series) await shardIngest.ingest(ev, { fromBackfill: true })
|
||||
}
|
||||
await snapshot(() => uoLinkClient.getHistory({ limit: BACKFILL_LIMIT }), 'events', ingestReversed, 'backfilled events from /history')
|
||||
await snapshot(() => uoLinkClient.getEconomy(200), 'series', ingestReversed)
|
||||
|
||||
// Champ board + help-page queue have no replay stream — snapshot the
|
||||
// authoritative current state directly (the sidecar guide's advice for both),
|
||||
// reconciling our tables to it so a stale row from before a disconnect can't
|
||||
// linger. Live champ.*/page.* deltas keep them fresh thereafter.
|
||||
const champs = await uoLinkClient.getChamps()
|
||||
if (champs.ok && champs.data && Array.isArray(champs.data.spawns)) {
|
||||
await shardState.replaceChamps(champs.data.spawns)
|
||||
log.info('snapshotted champ board from /champs', { count: champs.data.spawns.length })
|
||||
}
|
||||
const pages = await uoLinkClient.getPages()
|
||||
if (pages.ok && pages.data && Array.isArray(pages.data.pages)) {
|
||||
await shardState.replacePages(pages.data.pages)
|
||||
log.info('snapshotted help-page queue from /pages', { count: pages.data.pages.length })
|
||||
}
|
||||
await snapshot(() => uoLinkClient.getChamps(), 'spawns', (s) => shardState.replaceChamps(s), 'snapshotted champ board from /champs')
|
||||
await snapshot(() => uoLinkClient.getPages(), 'pages', (p) => shardState.replacePages(p), 'snapshotted help-page queue from /pages')
|
||||
|
||||
// ── Protocol 2.0 boards ──────────────────────────────────────────────
|
||||
// Same as champs/pages: snapshot the authoritative current state and
|
||||
@@ -83,21 +84,10 @@ async function backfill() {
|
||||
// failed/absent board (e.g. no City Loyalty → empty /governors) never wipes
|
||||
// another. Governors are NOT cleared before upsert (cities are fixed and the
|
||||
// term-capture is idempotent, so a reconnect can't spawn spurious terms).
|
||||
const guilds = await uoLinkClient.getGuilds()
|
||||
if (guilds.ok && guilds.data && Array.isArray(guilds.data.guilds)) {
|
||||
await shardState.replaceGuilds(guilds.data.guilds)
|
||||
log.info('snapshotted guild board from /guilds', { count: guilds.data.guilds.length })
|
||||
}
|
||||
const governors = await uoLinkClient.getGovernors()
|
||||
if (governors.ok && governors.data && Array.isArray(governors.data.cities)) {
|
||||
await shardState.replaceGovernors(governors.data.cities)
|
||||
log.info('snapshotted governor board from /governors', { count: governors.data.cities.length })
|
||||
}
|
||||
const houses = await uoLinkClient.getHouses()
|
||||
if (houses.ok && houses.data && Array.isArray(houses.data.houses)) {
|
||||
for (const ev of houses.data.houses) await shardIngest.ingest(ev, { fromBackfill: true })
|
||||
log.info('snapshotted house registry from /houses', { count: houses.data.houses.length })
|
||||
}
|
||||
await snapshot(() => uoLinkClient.getGuilds(), 'guilds', (g) => shardState.replaceGuilds(g), 'snapshotted guild board from /guilds')
|
||||
await snapshot(() => uoLinkClient.getGovernors(), 'cities', (c) => shardState.replaceGovernors(c), 'snapshotted governor board from /governors')
|
||||
await snapshot(() => uoLinkClient.getHouses(), 'houses', ingestEach, 'snapshotted house registry from /houses')
|
||||
|
||||
const presence = await uoLinkClient.getPresence()
|
||||
if (presence.ok && presence.data && typeof presence.data.count === 'number') {
|
||||
await shardState.setPresence(presence.data)
|
||||
@@ -150,66 +140,69 @@ async function connect() {
|
||||
return
|
||||
}
|
||||
|
||||
ws.on('open', async () => {
|
||||
log.info('uo-link WS connected')
|
||||
state.connected = true
|
||||
state.lastConnectedAt = Date.now()
|
||||
backoff = BACKOFF_MIN_MS
|
||||
await uoLinkConfig.recordStatus({ status: 'connected', statusDetail: null, pluginConnected: true }).catch(() => {})
|
||||
await backfill()
|
||||
})
|
||||
|
||||
ws.on('message', async (raw) => {
|
||||
let event
|
||||
try {
|
||||
event = JSON.parse(raw.toString())
|
||||
} catch {
|
||||
log.warn('dropping non-JSON WS frame')
|
||||
return
|
||||
}
|
||||
|
||||
if (event.kind === 'ws.hello') {
|
||||
helloSeen = true
|
||||
if (event.protocol && event.protocol !== state.protocol) {
|
||||
log.error('uo-link protocol mismatch on ws.hello — closing', {
|
||||
expected: state.protocol,
|
||||
got: event.protocol,
|
||||
})
|
||||
await uoLinkConfig
|
||||
.recordStatus({ status: 'error', statusDetail: `protocol mismatch: expected ${state.protocol}, got ${event.protocol}` })
|
||||
.catch(() => {})
|
||||
running = false
|
||||
try {
|
||||
ws.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (event.kind === 'pong') return // sidecar heartbeat — ignore
|
||||
|
||||
state.lastEventAt = Number.isFinite(event.t) ? event.t : Date.now()
|
||||
await shardIngest.ingest(event)
|
||||
})
|
||||
|
||||
ws.on('close', async () => {
|
||||
state.connected = false
|
||||
if (running) state.reconnects += 1
|
||||
log.warn('uo-link WS closed')
|
||||
await uoLinkConfig
|
||||
.recordStatus({ status: running ? 'reconnecting' : 'disconnected', pluginConnected: false })
|
||||
.catch(() => {})
|
||||
ws = null
|
||||
scheduleReconnect()
|
||||
})
|
||||
|
||||
ws.on('open', handleOpen)
|
||||
ws.on('message', handleMessage)
|
||||
ws.on('close', handleClose)
|
||||
ws.on('error', (err) => {
|
||||
log.warn('uo-link WS error', { message: err.message })
|
||||
// 'close' fires after 'error'; reconnect is scheduled there.
|
||||
})
|
||||
}
|
||||
|
||||
// WS lifecycle handlers, split out of connect() so it stays a flat setup path.
|
||||
async function handleOpen() {
|
||||
log.info('uo-link WS connected')
|
||||
state.connected = true
|
||||
state.lastConnectedAt = Date.now()
|
||||
backoff = BACKOFF_MIN_MS
|
||||
await uoLinkConfig.recordStatus({ status: 'connected', statusDetail: null, pluginConnected: true }).catch(() => {})
|
||||
await backfill()
|
||||
}
|
||||
|
||||
// A ws.hello frame: mark it seen and, on a protocol mismatch, record the error
|
||||
// and close (we won't run against an incompatible sidecar).
|
||||
async function handleHello(event) {
|
||||
helloSeen = true
|
||||
if (!event.protocol || event.protocol === state.protocol) return
|
||||
log.error('uo-link protocol mismatch on ws.hello — closing', { expected: state.protocol, got: event.protocol })
|
||||
await uoLinkConfig
|
||||
.recordStatus({ status: 'error', statusDetail: `protocol mismatch: expected ${state.protocol}, got ${event.protocol}` })
|
||||
.catch(() => {})
|
||||
running = false
|
||||
try {
|
||||
ws.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(raw) {
|
||||
let event
|
||||
try {
|
||||
event = JSON.parse(raw.toString())
|
||||
} catch {
|
||||
log.warn('dropping non-JSON WS frame')
|
||||
return
|
||||
}
|
||||
|
||||
if (event.kind === 'ws.hello') return handleHello(event)
|
||||
if (event.kind === 'pong') return // sidecar heartbeat — ignore
|
||||
|
||||
state.lastEventAt = Number.isFinite(event.t) ? event.t : Date.now()
|
||||
await shardIngest.ingest(event)
|
||||
}
|
||||
|
||||
async function handleClose() {
|
||||
state.connected = false
|
||||
if (running) state.reconnects += 1
|
||||
log.warn('uo-link WS closed')
|
||||
await uoLinkConfig
|
||||
.recordStatus({ status: running ? 'reconnecting' : 'disconnected', pluginConnected: false })
|
||||
.catch(() => {})
|
||||
ws = null
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
// Begin (or restart) the WS client. Idempotent — a running client is stopped
|
||||
// first so a config save can re-point it at a new URL/token.
|
||||
async function start() {
|
||||
|
||||
Reference in New Issue
Block a user