]> git.cworth.org Git - lmno-server/commitdiff
lmno: Save session state to disk so clients can reconnect after server restart
authorCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 23:41:22 +0000 (19:41 -0400)
committerCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 23:41:22 +0000 (19:41 -0400)
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.

lmno.js
session-store.js [new file with mode: 0644]

diff --git a/lmno.js b/lmno.js
index 5f3f097c8fdaec12d05c2bf9762ce170ffafb9ac..cb94610baa7432ae98d7690fb9d4deeef0626ca4 100644 (file)
--- 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 (file)
index 0000000..85f0903
--- /dev/null
@@ -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;