Files
ark-stronghold-protocol/server/index.js
T
sganggs 1b38e30819 feedback5 R1: startServer and main become wiring — PORT / HOST / TRUST_PROXY and the option picks go to http/config.js, the session stack and the /ws upgrade to http/websocket.js, the request listener and /healthz to http/routes.js (new), the security headers and the clientError 400 to http/common.js, the banner, signals and entry check to http/boot.js
Same order of construction, listeners and headers as before; a scripted fingerprint (export list, 51 HTTP requests'
status / header order / body hash, raw upgrade and malformed-request replies) and the process run (banner, port in
use, SIGINT) match the pre-split server exactly. test/version.test.js reads the banner and /healthz from their new
files; module headers describe each piece.
2026-10-05 16:08:41 +08:00

111 lines
6.1 KiB
JavaScript

// server/index.js — process entry & boot (DESIGN §1, §2). Plain node:http + ws, no framework: startServer() below
// wires the modules under server/http/, in this order —
//
// http/config.js ROOT, the served directories, the environment (PORT 3000, HOST 0.0.0.0, TRUST_PROXY auto, DEBUG),
// which startServer() options go to net.js / lobby.js, the console logger
// http/websocket.js session wiring (SessionRegistry → Lobby → Network) and the WebSocket at /ws (maxPayload 64 KB;
// refused at upgrade with 404 / 429 per network / 503)
// http/static.js the static mounts (/ → public/, /data/, /shared/, /sim/ `.js` only), the /data.js browser stand-in
// http/media.js /media/bgm/act1 → public/assets/audio/bgm/act1.mp3 (audio addressed without its extension)
// http/files.js one file → response: MIME, gzip + memory cache, ETag / Last-Modified / 304, Cache-Control, ranges
// http/buildTag.js the build tag of the served browser runtime (/healthz `build`, public/js/ui/buildGuard.js)
// http/routes.js the request listener: security headers, 414 / 400 / 405, GET /healthz → JSON status, else static
// http/common.js what every answer shares: security headers, URL split, error page, JSON replies, bare 400
// http/boot.js banner (Local / LAN / tunnel URLs), port-in-use hint, graceful shutdown on SIGINT / SIGTERM
//
// Per-network limits for internet clients (see net.js clientAddress; local/LAN peers are exempt): open sockets
// (maxConnectionsPerAddr, refused at upgrade with 429), rooms and running matches (lobby.js).
//
// Programmatic use (tests): `const srv = await startServer({ port: 0, quiet: true }); … await srv.close();`
// The server only auto-listens when this file is the process entry point.
import http from 'node:http';
import { getData, loadData } from './data.js';
import { ROOT, listenAddress, serveDirs, makeLogger, parseTrustProxy } from './http/config.js';
import { WS_MAX_PAYLOAD, createSessionStack, attachWebSocket } from './http/websocket.js';
import { DATA_SHIM_JS, createStaticHandler } from './http/static.js';
import { MIME, COMPRESSIBLE, acceptsGzip, parseRange } from './http/files.js';
import { BUILD_INPUTS, computeBuildTag, buildTag, resetBuildTag } from './http/buildTag.js';
import { createRequestHandler } from './http/routes.js';
import { answerClientError } from './http/common.js';
import { lanUrls, isProcessEntry, runMain } from './http/boot.js';
// The public API of this module (tests and tools import it from here); the code lives in ./http/.
export {
ROOT, WS_MAX_PAYLOAD, DATA_SHIM_JS, MIME, COMPRESSIBLE, BUILD_INPUTS, computeBuildTag, buildTag, resetBuildTag,
acceptsGzip, parseRange, createStaticHandler, lanUrls, parseTrustProxy,
};
/**
* Build and start the HTTP + WebSocket server.
* @param {{
* port?: number, host?: string, quiet?: boolean, log?: object,
* publicDir?: string, dataDir?: string, sharedDir?: string,
* MatchClass?: Function, seedFn?: () => number,
* lobbyGraceMs?: number, reconnectWindowMs?: number, heartbeatMs?: number, helloTimeoutMs?: number,
* ratePerSec?: number, rateBurst?: number, maxConnections?: number, maxRooms?: number,
* maxConnectionsPerAddr?: number, maxRoomsPerAddr?: number, maxMatchesPerAddr?: number, resyncMinGapMs?: number,
* heavyPerSec?: number, heavyBurst?: number, trustProxy?: 'auto' | boolean, soloReconnectWindowMs?: number,
* }} [opts]
* @returns {Promise<{ port: number, host: string, url: string, server: http.Server, wss: import('ws').WebSocketServer,
* lobby: import('./lobby.js').Lobby, network: import('./net.js').Network,
* registry: import('./net.js').SessionRegistry, close: () => Promise<void> }>}
*/
export async function startServer(opts = {}) {
const { port, host } = listenAddress(opts);
const log = opts.log || makeLogger(!!opts.quiet);
const { publicDir, dataDir, sharedDir } = serveDirs(opts);
// The process-wide singleton serves the default data dir; a custom dir (tests) gets its own copy.
const data = opts.dataDir ? loadData(dataDir, { log }) : getData({ dir: dataDir, log });
const { registry, lobby, network } = createSessionStack(opts, { data, log });
const serveStatic = createStaticHandler({ publicDir, dataDir, sharedDir, log });
const startedAt = Date.now();
// The tag is per process (see buildTag): read the browser runtime once, here, not on every /healthz.
resetBuildTag();
buildTag();
const server = http.createServer(createRequestHandler({ serveStatic, health: { startedAt, network, registry, lobby }, log }));
server.on('clientError', answerClientError);
const wss = attachWebSocket(server, { network, log });
try {
await new Promise((resolve, reject) => {
const onError = (e) => { server.off('listening', onListening); reject(e); };
const onListening = () => { server.off('error', onError); resolve(); };
server.once('error', onError);
server.once('listening', onListening);
server.listen(port, host);
});
} catch (e) {
network.close(); // stop heartbeat/sweep timers of the half-built server
throw e;
}
server.on('error', (e) => log.error('[http] server error', e));
const addr = server.address();
const actualPort = typeof addr === 'object' && addr ? addr.port : port;
const url = `http://${host === '0.0.0.0' || host === '::' ? 'localhost' : host}:${actualPort}`;
let closing = null;
async function close() {
if (closing) return closing;
closing = (async () => {
try { lobby.shutdown('shutdown'); } catch (e) { log.error('[shutdown] lobby', e); }
network.close();
await new Promise((resolve) => {
server.close(() => resolve());
server.closeIdleConnections?.();
setTimeout(() => { server.closeAllConnections?.(); }, 500).unref();
});
try { wss.close(); } catch { /* ignore */ }
})();
return closing;
}
return { port: actualPort, host, url, server, wss, lobby, network, registry, close };
}
// `node server/index.js` / npm start: listen, print the banner, stop on SIGINT / SIGTERM (http/boot.js).
if (isProcessEntry(import.meta.url)) runMain(startServer);