From: Carl Worth Date: Sun, 8 Mar 2026 23:41:22 +0000 (-0400) Subject: lmno: Save session state to disk so clients can reconnect after server restart X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=824c8e36f36d5b7dbfce9b6fa1918386b7a92a36;p=lmno-server lmno: Save session state to disk so clients can reconnect after server restart In recent commits we added save/restore of all game state, but our express session objects were only stored in memory. So, after a server restart, a client reconnecting was getting an entirely new session rather than being connect to an existing session (and associated with the current player state in the game object). To fix this, this commit adds a new FileStore in session-store.js which saves sessions to a JSON file on disk when sessions are changed, (basically just user login or nickname change), and loads them at server start. --- diff --git a/lmno.js b/lmno.js index 5f3f097..cb94610 100644 --- a/lmno.js +++ b/lmno.js @@ -7,9 +7,12 @@ const fs = require("fs"); const path = require("path"); const nunjucks = require("nunjucks"); +const FileStore = require("./session-store"); + const DATA_DIR = path.join(__dirname, "data"); const STATE_FILE = path.join(DATA_DIR, "lmno-state.json"); const STATS_FILE = path.join(DATA_DIR, "lmno-stats.jsonl"); +const SESSIONS_FILE = path.join(DATA_DIR, "lmno-sessions.json"); const RETIREMENT_AGE_MS = 24 * 60 * 60 * 1000; try { @@ -53,10 +56,16 @@ app.set('trust proxy', true); app.use(cors()); app.use(body_parser.urlencoded({ extended: false })); app.use(body_parser.json()); +/* Ensure data directory exists before creating session store. */ +if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR); +} + app.use(session({ secret: lmno_config.session_secret, resave: false, - saveUninitialized: false + saveUninitialized: false, + store: new FileStore(SESSIONS_FILE) })); const njx = nunjucks.configure("templates", { diff --git a/session-store.js b/session-store.js new file mode 100644 index 0000000..85f0903 --- /dev/null +++ b/session-store.js @@ -0,0 +1,55 @@ +const session = require("express-session"); +const fs = require("fs"); + +/* A simple file-backed session store for express-session. + * + * Sessions are kept in memory for fast access and written to a JSON + * file on modification (which is infrequent — just nickname and login + * changes, not every request, since express-session is configured with + * resave: false). Atomic writes (tmp file + rename) prevent corruption. + */ +class FileStore extends session.Store { + constructor(filepath) { + super(); + this.filepath = filepath; + this.sessions = {}; + this._load(); + } + + _load() { + try { + const data = fs.readFileSync(this.filepath, 'utf8'); + this.sessions = JSON.parse(data); + const count = Object.keys(this.sessions).length; + console.log(`Restored ${count} session(s) from ${this.filepath}`); + } catch (e) { + /* File doesn't exist yet — start with an empty store. */ + this.sessions = {}; + } + } + + _save() { + const tmp = this.filepath + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(this.sessions)); + fs.renameSync(tmp, this.filepath); + } + + get(sid, callback) { + const sess = this.sessions[sid]; + callback(null, sess ? JSON.parse(sess) : null); + } + + set(sid, sess, callback) { + this.sessions[sid] = JSON.stringify(sess); + this._save(); + callback(null); + } + + destroy(sid, callback) { + delete this.sessions[sid]; + this._save(); + callback(null); + } +} + +module.exports = FileStore;