try to fix this spaghetti of a code

This commit is contained in:
Frederik Spirgi
2026-09-09 10:59:30 +02:00
parent 729de5dcc4
commit c43845356a
5 changed files with 23 additions and 9 deletions
+7 -2
View File
@@ -1,4 +1,4 @@
const crypto = require('node:crypto'); const crypto = require('node:crypto');
class AppError extends Error { class AppError extends Error {
constructor(code, message, status = 400) { super(message); this.code = code; this.status = status; } constructor(code, message, status = 400) { super(message); this.code = code; this.status = status; }
} }
@@ -16,4 +16,9 @@ class KeyedQueue {
async drain() { await Promise.all([...this.tails.values()]); } async drain() { await Promise.all([...this.tails.values()]); }
} }
const hash = value => crypto.createHash('sha256').update(value).digest('hex'); const hash = value => crypto.createHash('sha256').update(value).digest('hex');
module.exports = {AppError, KeyedQueue, hash}; function canonical(value) {
if (Array.isArray(value)) return value.map(canonical);
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
return value;
}
module.exports = {AppError, KeyedQueue, hash, canonical};
+2 -2
View File
@@ -1,6 +1,6 @@
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const {EventEmitter} = require('node:events'); const {EventEmitter} = require('node:events');
const {KeyedQueue, AppError, hash} = require('./core'); const {KeyedQueue, AppError, hash, canonical} = require('./core');
const engine = require('./engine'); const engine = require('./engine');
function createLobbyService(repository, config) { function createLobbyService(repository, config) {
@@ -38,7 +38,7 @@ function createLobbyService(repository, config) {
return queue.run([...(membershipMutation ? ['membership'] : []), ...(username ? [`account:${username}`] : []), return queue.run([...(membershipMutation ? ['membership'] : []), ...(username ? [`account:${username}`] : []),
...(message.lobbyId ? [`lobby:${message.lobbyId}`] : [])], async () => { ...(message.lobbyId ? [`lobby:${message.lobbyId}`] : [])], async () => {
const id = `${username || 'system'}:${message.requestId}`; const id = `${username || 'system'}:${message.requestId}`;
const fingerprint = hash(JSON.stringify(message)); const fingerprint = hash(JSON.stringify(canonical(message)));
const prior = await repository.command(id); const prior = await repository.command(id);
if (prior) { if (prior) {
if (prior.fingerprint !== fingerprint) throw new AppError('IDEMPOTENCY_CONFLICT', 'Request ID was reused', 409); if (prior.fingerprint !== fingerprint) throw new AppError('IDEMPOTENCY_CONFLICT', 'Request ID was reused', 409);
+5 -2
View File
@@ -47,7 +47,7 @@ async function createRepository(filename, {beforeCommit = async () => {}} = {})
`); `);
if (old) { if (old) {
// Fails and rolls back rather than silently rounding or losing invalid balances. // Fails and rolls back rather than silently rounding or losing invalid balances.
await tx.exec('INSERT INTO users(username, password_hash, money) SELECT username, password_hash, money FROM legacy_users; DROP TABLE legacy_users;'); await tx.exec('INSERT INTO users(username, password_hash, money) SELECT username, password_hash, money FROM legacy_users; INSERT INTO ledger(command_id,username,delta,reason) SELECT 'schema-v1:' || username, username, money, 'Opening balance migration' FROM users; DROP TABLE legacy_users;');
} }
await tx.exec('PRAGMA user_version = 1'); await tx.exec('PRAGMA user_version = 1');
}); });
@@ -58,7 +58,10 @@ async function createRepository(filename, {beforeCommit = async () => {}} = {})
user: username => read(tx => tx.get('SELECT username, password_hash AS passwordHash, session_hash AS sessionHash, money FROM users WHERE username = ?', username)), user: username => read(tx => tx.get('SELECT username, password_hash AS passwordHash, session_hash AS sessionHash, money FROM users WHERE username = ?', username)),
users: () => read(tx => tx.all('SELECT username, money FROM users ORDER BY money DESC, username ASC')), users: () => read(tx => tx.all('SELECT username, money FROM users ORDER BY money DESC, username ASC')),
async register(username, passwordHash, sessionHash) { async register(username, passwordHash, sessionHash) {
try { await transaction(tx => tx.run('INSERT INTO users(username,password_hash,session_hash) VALUES(?,?,?)', username, passwordHash, sessionHash)); } try { await transaction(async tx => {
await tx.run('INSERT INTO users(username,password_hash,session_hash) VALUES(?,?,?)', username, passwordHash, sessionHash);
await tx.run('INSERT INTO ledger(command_id,username,delta,reason) VALUES(?,?,200,?)', `register:${username}`, username, 'Starting chips');
}); }
catch (error) { if (error.code === 'SQLITE_CONSTRAINT') throw new AppError('USERNAME_EXISTS', 'Username already exists', 409); throw error; } catch (error) { if (error.code === 'SQLITE_CONSTRAINT') throw new AppError('USERNAME_EXISTS', 'Username already exists', 409); throw error; }
}, },
setSession: (username, sessionHash) => transaction(tx => tx.run('UPDATE users SET session_hash = ? WHERE username = ?', sessionHash, username)), setSession: (username, sessionHash) => transaction(tx => tx.run('UPDATE users SET session_hash = ? WHERE username = ?', sessionHash, username)),
+4 -3
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
window.apiRequest = async function (url, options = {}) { window.apiRequest = async function (url, options = {}) {
const response = await fetch(url, {credentials: 'same-origin', ...options}); const response = await fetch(url, {credentials: 'same-origin', signal: AbortSignal.timeout(5000), ...options});
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (response.status === 401) { if (response.status === 401) {
localStorage.removeItem('activeLobbyId'); localStorage.removeItem('activeLobbyId');
@@ -45,15 +45,16 @@ window.RealtimeClient = class extends EventTarget {
} }
this.dispatchEvent(new CustomEvent('message', {detail: message})); this.dispatchEvent(new CustomEvent('message', {detail: message}));
}); });
socket.addEventListener('error', () => this.status('Connection interrupted. Reconnecting…')); socket.addEventListener('error', () => this.status('Connection interrupted. Reconnecting…'));
socket.addEventListener('close', async event => { socket.addEventListener('close', async event => {
clearTimeout(timeout); this.ready = false; clearTimeout(timeout); this.ready = false;
if (this.stopped) return; if (this.stopped) return;
this.status('Connection interrupted. Reconnecting…'); this.status('Connection interrupted. Reconnecting…');
if (event.code === 1008 && /Session/.test(event.reason)) { if (event.code === 1008 && /Session/.test(event.reason)) {
this.close(); localStorage.removeItem('activeLobbyId'); location.assign('/login'); return; this.close(); localStorage.removeItem('activeLobbyId'); location.assign('/login'); return;
} }
try { await apiRequest('/verify', {method: 'POST'}); } catch { /* apiRequest redirects expired sessions; network outages are retried. */ } try { await apiRequest('/verify', {method: 'POST'}); } catch { /* apiRequest redirects expired sessions; network outages are retried. */ }
if (this.stopped) return;
if (++this.attempts > 5) { this.close(); this.status('Could not reconnect. Reload to try again.'); return; } if (++this.attempts > 5) { this.close(); this.status('Could not reconnect. Reload to try again.'); return; }
this.reconnectTimer = setTimeout(() => this.open(), Math.min(500 * 2 ** (this.attempts - 1), 5000)); this.reconnectTimer = setTimeout(() => this.open(), Math.min(500 * 2 ** (this.attempts - 1), 5000));
}); });
+5
View File
@@ -248,6 +248,11 @@ document.addEventListener("DOMContentLoaded", () => {
} }
if (message.type === "auth:error" || message.type === "poker:error" || message.type === "error") { if (message.type === "auth:error" || message.type === "poker:error" || message.type === "error") {
if (["NOT_SEATED", "LOBBY_NOT_FOUND"].includes(message.code)) {
localStorage.removeItem("activeLobbyId");
location.assign("/");
return;
}
showError(message.message || "Poker connection failed"); showError(message.message || "Poker connection failed");
} }
}); });