Spaces:
Sleeping
Sleeping
Sasha commited on
Commit ·
80fc693
1
Parent(s): 613007f
Optimize message ingestion with logChatMessagesBatch: bulk upsert to database.
Browse files- server/db.js +128 -0
- server/server.js +6 -6
server/db.js
CHANGED
|
@@ -565,6 +565,134 @@ export async function logChatMessage(msg) {
|
|
| 565 |
}
|
| 566 |
}
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
/**
|
| 569 |
* Log voice words (bulk insert)
|
| 570 |
*/
|
|
|
|
| 565 |
}
|
| 566 |
}
|
| 567 |
|
| 568 |
+
/**
|
| 569 |
+
* Log chat messages in batch (performance optimized)
|
| 570 |
+
*/
|
| 571 |
+
export async function logChatMessagesBatch(messages) {
|
| 572 |
+
if (!messages || messages.length === 0) return;
|
| 573 |
+
|
| 574 |
+
// 1. Fetch all streams to search in-memory instead of making DB queries for each message
|
| 575 |
+
const streams = await getStreamsList();
|
| 576 |
+
const activeStream = await getActiveStream();
|
| 577 |
+
|
| 578 |
+
const getStreamIdForTime = (timestampIso) => {
|
| 579 |
+
if (!timestampIso) return activeStream ? activeStream.id : null;
|
| 580 |
+
const time = new Date(timestampIso);
|
| 581 |
+
const matchedStream = streams.find(s => {
|
| 582 |
+
const start = new Date(s.start_time);
|
| 583 |
+
const isAfterStart = start <= time;
|
| 584 |
+
const isBeforeEnd = !s.end_time || new Date(s.end_time) >= time;
|
| 585 |
+
return isAfterStart && isBeforeEnd;
|
| 586 |
+
});
|
| 587 |
+
return matchedStream ? matchedStream.id : (activeStream ? activeStream.id : null);
|
| 588 |
+
};
|
| 589 |
+
|
| 590 |
+
if (dbMode === 'sqlite') {
|
| 591 |
+
const insertMsg = sqliteDb.prepare(`
|
| 592 |
+
INSERT INTO messages (id, stream_id, username, display_name, message, timestamp, is_streamer, is_mod, is_sub)
|
| 593 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 594 |
+
`);
|
| 595 |
+
|
| 596 |
+
const insertViewer = sqliteDb.prepare(`
|
| 597 |
+
INSERT INTO stream_viewers (stream_id, username, display_name, has_chatted, is_mod, is_sub, first_seen)
|
| 598 |
+
VALUES (?, ?, ?, 1, ?, ?, ?)
|
| 599 |
+
ON CONFLICT(stream_id, username) DO UPDATE SET
|
| 600 |
+
has_chatted = 1,
|
| 601 |
+
is_mod = excluded.is_mod,
|
| 602 |
+
is_sub = excluded.is_sub
|
| 603 |
+
`);
|
| 604 |
+
|
| 605 |
+
const runTransaction = sqliteDb.transaction((msgs) => {
|
| 606 |
+
for (const msg of msgs) {
|
| 607 |
+
const streamId = getStreamIdForTime(msg.timestamp);
|
| 608 |
+
try {
|
| 609 |
+
insertMsg.run(
|
| 610 |
+
msg.id,
|
| 611 |
+
streamId,
|
| 612 |
+
msg.username.toLowerCase(),
|
| 613 |
+
msg.displayName || msg.username,
|
| 614 |
+
msg.message,
|
| 615 |
+
msg.timestamp || new Date().toISOString(),
|
| 616 |
+
msg.isStreamer ? 1 : 0,
|
| 617 |
+
msg.isMod ? 1 : 0,
|
| 618 |
+
msg.isSub ? 1 : 0
|
| 619 |
+
);
|
| 620 |
+
} catch (err) {
|
| 621 |
+
if (!err.message.includes('UNIQUE constraint failed')) {
|
| 622 |
+
console.error('[Database] SQLite Batch Error saving message:', err.message);
|
| 623 |
+
}
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
try {
|
| 627 |
+
insertViewer.run(
|
| 628 |
+
streamId,
|
| 629 |
+
msg.username.toLowerCase(),
|
| 630 |
+
msg.displayName || msg.username,
|
| 631 |
+
msg.isMod ? 1 : 0,
|
| 632 |
+
msg.isSub ? 1 : 0,
|
| 633 |
+
msg.timestamp || new Date().toISOString()
|
| 634 |
+
);
|
| 635 |
+
} catch (err) {
|
| 636 |
+
console.error('[Database] SQLite Batch Error updating viewer:', err.message);
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
});
|
| 640 |
+
|
| 641 |
+
runTransaction(messages);
|
| 642 |
+
} else {
|
| 643 |
+
// Supabase Mode: Build batch lists
|
| 644 |
+
const messagesToInsert = [];
|
| 645 |
+
const viewersToUpsert = new Map(); // Use Map to unique-fy viewers by stream_id + username
|
| 646 |
+
|
| 647 |
+
for (const msg of messages) {
|
| 648 |
+
const streamId = getStreamIdForTime(msg.timestamp);
|
| 649 |
+
|
| 650 |
+
messagesToInsert.push({
|
| 651 |
+
id: msg.id,
|
| 652 |
+
stream_id: streamId,
|
| 653 |
+
username: msg.username.toLowerCase(),
|
| 654 |
+
display_name: msg.displayName || msg.username,
|
| 655 |
+
message: msg.message,
|
| 656 |
+
timestamp: msg.timestamp || new Date().toISOString(),
|
| 657 |
+
is_streamer: msg.isStreamer || false,
|
| 658 |
+
is_mod: msg.isMod || false,
|
| 659 |
+
is_sub: msg.isSub || false
|
| 660 |
+
});
|
| 661 |
+
|
| 662 |
+
const viewerKey = `${streamId || 'null'}-${msg.username.toLowerCase()}`;
|
| 663 |
+
viewersToUpsert.set(viewerKey, {
|
| 664 |
+
stream_id: streamId,
|
| 665 |
+
username: msg.username.toLowerCase(),
|
| 666 |
+
display_name: msg.displayName || msg.username,
|
| 667 |
+
has_chatted: true,
|
| 668 |
+
is_mod: msg.isMod || false,
|
| 669 |
+
is_sub: msg.isSub || false,
|
| 670 |
+
first_seen: msg.timestamp || new Date().toISOString()
|
| 671 |
+
});
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
// 1. Bulk upsert messages
|
| 675 |
+
const { error: msgError } = await supabase
|
| 676 |
+
.from('messages')
|
| 677 |
+
.upsert(messagesToInsert, { onConflict: 'id' });
|
| 678 |
+
|
| 679 |
+
if (msgError) {
|
| 680 |
+
console.error('[Supabase] Bulk message upsert error:', msgError.message);
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// 2. Bulk upsert viewers
|
| 684 |
+
const uniqueViewers = Array.from(viewersToUpsert.values());
|
| 685 |
+
const { error: viewerError } = await supabase
|
| 686 |
+
.from('stream_viewers')
|
| 687 |
+
.upsert(uniqueViewers, { onConflict: 'stream_id, username' });
|
| 688 |
+
|
| 689 |
+
if (viewerError) {
|
| 690 |
+
console.error('[Supabase] Bulk viewers upsert error:', viewerError.message);
|
| 691 |
+
}
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
|
| 696 |
/**
|
| 697 |
* Log voice words (bulk insert)
|
| 698 |
*/
|
server/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'path';
|
|
| 7 |
import fs from 'fs';
|
| 8 |
import {
|
| 9 |
logChatMessage,
|
|
|
|
| 10 |
logVoiceWords,
|
| 11 |
logModAction,
|
| 12 |
logViewerJoin,
|
|
@@ -175,12 +176,11 @@ app.post('/api/log/messages', authenticateWorker, async (req, res) => {
|
|
| 175 |
return res.status(400).json({ error: 'Invalid input. Expected array of messages.' });
|
| 176 |
}
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
}
|
| 184 |
}
|
| 185 |
|
| 186 |
res.json({ success: true, count: messages.length });
|
|
|
|
| 7 |
import fs from 'fs';
|
| 8 |
import {
|
| 9 |
logChatMessage,
|
| 10 |
+
logChatMessagesBatch,
|
| 11 |
logVoiceWords,
|
| 12 |
logModAction,
|
| 13 |
logViewerJoin,
|
|
|
|
| 176 |
return res.status(400).json({ error: 'Invalid input. Expected array of messages.' });
|
| 177 |
}
|
| 178 |
|
| 179 |
+
try {
|
| 180 |
+
await logChatMessagesBatch(messages);
|
| 181 |
+
} catch (err) {
|
| 182 |
+
console.error('Error logging batch messages:', err);
|
| 183 |
+
return res.status(500).json({ error: 'Failed to log messages' });
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
res.json({ success: true, count: messages.length });
|