@@ -169,13 +169,33 @@ if (currentVersion < 1) {
console.error('β Migration failed:', err);
throw err;
}
+} else if (currentVersion === 3) {
+ console.log('π Migrating database schema from version 3 to 4...');
+
+ try {
+ db.exec('BEGIN');
+
+ // Add last_activity column to rooms table, default to created_at
+ db.exec('ALTER TABLE rooms ADD COLUMN last_activity INTEGER');
+ db.exec('UPDATE rooms SET last_activity = created_at');
+
+ db.exec('PRAGMA user_version = 4');
+ db.exec('COMMIT');
+
+ console.log('β
Migration from v3 to v4 complete');
+ } catch (err) {
+ db.exec('ROLLBACK');
+ console.error('β Migration failed:', err);
+ throw err;
+ }
} else {
// Plant the schema if soil is fresh (for new databases)
db.exec(`
CREATE TABLE IF NOT EXISTS rooms (
code TEXT PRIMARY KEY NOT NULL,
created_at INTEGER DEFAULT (unixepoch()),
- title TEXT
+ title TEXT,
+ last_activity INTEGER DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS items (
@@ -247,6 +267,7 @@ export function addItems(roomCode: string, items: Array<{id: string, text: strin
for (const item of items) {
stmt.run(item.id, roomCode, item.text);
}
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode);
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
@@ -255,27 +276,43 @@ export function addItems(roomCode: string, items: Array<{id: string, text: strin
}
export function upsertVote(itemId: string, userId: string, voteType: string) {
+ const roomCode = db.prepare('SELECT room_code FROM items WHERE id = ?').get(itemId) as { room_code: string } | undefined;
db.prepare(`
INSERT INTO votes (item_id, user_id, vote_type)
VALUES (?, ?, ?)
ON CONFLICT (item_id, user_id)
DO UPDATE SET vote_type = excluded.vote_type
`).run(itemId, userId, voteType);
+ if (roomCode) {
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode.room_code);
+ }
}
export function deleteVote(itemId: string, userId: string) {
+ const roomCode = db.prepare('SELECT room_code FROM items WHERE id = ?').get(itemId) as { room_code: string } | undefined;
db.prepare('DELETE FROM votes WHERE item_id = ? AND user_id = ?').run(itemId, userId);
+ if (roomCode) {
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode.room_code);
+ }
}
export function updateItemText(itemId: string, text: string) {
+ const roomCode = db.prepare('SELECT room_code FROM items WHERE id = ?').get(itemId) as { room_code: string } | undefined;
db.prepare('UPDATE items SET text = ? WHERE id = ?').run(text, itemId);
+ if (roomCode) {
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode.room_code);
+ }
}
export function deleteItem(itemId: string) {
+ const roomCode = db.prepare('SELECT room_code FROM items WHERE id = ?').get(itemId) as { room_code: string } | undefined;
// Delete votes first (foreign key constraint)
db.prepare('DELETE FROM votes WHERE item_id = ?').run(itemId);
// Delete the item
db.prepare('DELETE FROM items WHERE id = ?').run(itemId);
+ if (roomCode) {
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode.room_code);
+ }
}
export function resetVotes(roomCode: string) {
@@ -285,10 +322,15 @@ export function resetVotes(roomCode: string) {
SELECT id FROM items WHERE room_code = ?
)
`).run(roomCode);
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode);
}
export function updateRoomTitle(roomCode: string, title: string | null) {
- db.prepare('UPDATE rooms SET title = ? WHERE code = ?').run(title, roomCode);
+ db.prepare('UPDATE rooms SET title = ?, last_activity = unixepoch() WHERE code = ?').run(title, roomCode);
+}
+
+export function touchRoomActivity(roomCode: string) {
+ db.prepare('UPDATE rooms SET last_activity = unixepoch() WHERE code = ?').run(roomCode);
}
export function getItemsWithVotes(roomCode: string) {
@@ -305,17 +347,9 @@ export function getItemsWithVotes(roomCode: string) {
export function deleteInactiveRooms(daysInactive: number = 30): number {
const cutoffTimestamp = Math.floor(Date.now() / 1000) - (daysInactive * 24 * 60 * 60);
- // Find rooms where the most recent activity (item or vote) is older than the cutoff
const result = db.prepare(`
DELETE FROM rooms
- WHERE code IN (
- SELECT r.code
- FROM rooms r
- LEFT JOIN items i ON r.code = i.room_code
- LEFT JOIN votes v ON i.id = v.item_id
- GROUP BY r.code
- HAVING COALESCE(MAX(i.created_at), MAX(v.created_at), r.created_at) < ?
- )
+ WHERE last_activity < ?
`).run(cutoffTimestamp);
return result.changes || 0;
@@ -343,6 +343,8 @@ function handleWebSocket(ws: WebSocket, roomCode: string, userId: string) {
const randomIndex = crypto.getRandomValues(new Uint32Array(1))[0] % tiedItems.length;
const chosen = tiedItems[randomIndex];
+ db.touchRoomActivity(roomCode);
+
broadcast(roomCode, {
type: 'tie_broken',
itemId: chosen.id,