Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions chat-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { db } from "./db.ts";
import type { ChatMessage } from "./chat.ts";

// ── Schema ──────────────────────────────────────────────────────────────────

db.exec(`
CREATE TABLE IF NOT EXISTS chat_messages (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
display_name TEXT NOT NULL,
content TEXT NOT NULL,
timestamp INTEGER NOT NULL,
badges TEXT,
is_subscriber INTEGER DEFAULT 0,
is_mod INTEGER DEFAULT 0,
round_num INTEGER
);
CREATE INDEX IF NOT EXISTS idx_chat_round ON chat_messages(round_num);
CREATE INDEX IF NOT EXISTS idx_chat_timestamp ON chat_messages(timestamp);
`);

// ── Persistence ─────────────────────────────────────────────────────────────

const insertStmt = db.prepare(`
INSERT OR IGNORE INTO chat_messages
(id, username, display_name, content, timestamp, badges, is_subscriber, is_mod, round_num)
VALUES ($id, $username, $displayName, $content, $timestamp, $badges, $isSubscriber, $isMod, $roundNum)
`);

const insertBatch = db.transaction((messages: ChatMessage[]) => {
for (const msg of messages) {
insertStmt.run({
$id: msg.id,
$username: msg.username,
$displayName: msg.displayName,
$content: msg.content,
$timestamp: msg.timestamp,
$badges: JSON.stringify(msg.badges),
$isSubscriber: msg.isSubscriber ? 1 : 0,
$isMod: msg.isMod ? 1 : 0,
$roundNum: msg.roundNum,
});
}
});

const pendingMessages: ChatMessage[] = [];
let flushTimer: ReturnType<typeof setInterval> | null = null;

const FLUSH_INTERVAL_MS = 5_000;

function flush() {
if (pendingMessages.length === 0) return;
const batch = pendingMessages.splice(0);
insertBatch(batch);
}
Comment on lines +51 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High chat-store.ts:51

Messages are removed from pendingMessages before insertBatch succeeds. If the insert throws, those messages are lost. Consider restoring the batch on error (e.g., catch { pendingMessages.unshift(...batch); throw e; }) or only splicing after success.

-function flush() {
-  if (pendingMessages.length === 0) return;
-  const batch = pendingMessages.splice(0);
-  insertBatch(batch);
+function flush() {
+  if (pendingMessages.length === 0) return;
+  const batch = pendingMessages.splice(0);
+  try {
+    insertBatch(batch);
+  } catch (e) {
+    pendingMessages.unshift(...batch);
+    throw e;
+  }
 }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file chat-store.ts around lines 51-55:

Messages are removed from `pendingMessages` before `insertBatch` succeeds. If the insert throws, those messages are lost. Consider restoring the batch on error (e.g., `catch { pendingMessages.unshift(...batch); throw e; }`) or only splicing after success.

Evidence trail:
 chat - store . ts lines 51 - 55 ( commit 52 e 246 68 dd 79 af 099 1 e 8 fb 0 de 16 e 1 fe 96 08 dd 0 a 0 ): ` pending Messages . splice ( 0 )` on line 53 removes messages before ` insert Batch ( batch )` on line 54 is called , with no error handling to restore messages if insert fails .


export function queueMessage(message: ChatMessage) {
pendingMessages.push(message);
if (pendingMessages.length >= 50) {
flush();
}
}

export function startPersistence() {
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
}
Comment on lines +70 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium chat-store.ts:64

Calling startPersistence() multiple times leaks intervals since the previous timer reference is overwritten. Consider clearing any existing timer first, similar to how stopPersistence does.

Suggested change
export function startPersistence() {
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
}
export function startPersistence() {
if (flushTimer) clearInterval(flushTimer);
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file chat-store.ts around lines 64-66:

Calling `startPersistence()` multiple times leaks intervals since the previous timer reference is overwritten. Consider clearing any existing timer first, similar to how `stopPersistence` does.

Evidence trail:
chat-store.ts lines 47, 64-66, 68-74 at commit 52e24668dd79af0991e8fb0de16e1fe9608dd0a0. Line 65 shows `flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);` with no guard. Lines 68-74 show stopPersistence does clear existing timer with `if (flushTimer) { clearInterval(flushTimer); ... }`.


export function stopPersistence() {
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
flush();
}

// ── Queries ─────────────────────────────────────────────────────────────────

type ChatRow = {
id: string;
username: string;
display_name: string;
content: string;
timestamp: number;
badges: string;
is_subscriber: number;
is_mod: number;
round_num: number | null;
};

function rowToMessage(row: ChatRow): ChatMessage {
return {
id: row.id,
username: row.username,
displayName: row.display_name,
content: row.content,
timestamp: row.timestamp,
badges: JSON.parse(row.badges || "{}"),
isSubscriber: row.is_subscriber === 1,
isMod: row.is_mod === 1,
roundNum: row.round_num,
};
}

export function getRecentChat(limit = 50): ChatMessage[] {
const rows = db
.query(
"SELECT * FROM chat_messages ORDER BY timestamp DESC LIMIT $limit",
)
.all({ $limit: limit }) as ChatRow[];
return rows.reverse().map(rowToMessage);
}
Comment on lines +111 to +118
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

getRecentChat reads only flushed data — recent messages may be missing.

This function queries SQLite, but up to 50 messages (or 5 seconds worth) may still be sitting in pendingMessages and won't appear in results. The /api/chat/recent endpoint in server.ts (Line 566) calls this function, so API consumers could see stale data. Consider flushing before querying, or merging in-memory pending messages into the result set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@chat-store.ts` around lines 104 - 111, getRecentChat currently only reads
persisted rows and can miss messages still in memory (pendingMessages); update
getRecentChat to include pendingMessages before or while querying: either call
the existing flushPendingMessages (or the function that persists
pendingMessages) and await it prior to running the SELECT, or fetch DB rows and
merge in-memory pendingMessages into the result set (use timestamp to sort,
dedupe by id if present, and enforce the requested limit) so the returned
ChatMessage[] reflects both persisted and pending messages; refer to
getRecentChat, pendingMessages, and any flush/persist function name in the
module to locate and implement the change.


export function getChatForRound(roundNum: number): ChatMessage[] {
const rows = db
.query(
"SELECT * FROM chat_messages WHERE round_num = $roundNum ORDER BY timestamp ASC",
)
.all({ $roundNum: roundNum }) as ChatRow[];
return rows.map(rowToMessage);
}

export function getChatCount(): number {
const result = db
.query("SELECT COUNT(*) as count FROM chat_messages")
.get() as { count: number };
return result.count;
}
Loading