Added cold-persistance to Channel Chat Buffer via periodic DB saves based on chat activity.

This commit is contained in:
rainbow napkin 2025-07-23 01:05:39 -04:00
parent 366df357b8
commit c64b315fdf
7 changed files with 124 additions and 5 deletions

View file

@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//local imports //local imports
const connectedUser = require('./connectedUser'); const connectedUser = require('./connectedUser');
const chatBuffer = require('./chatBuffer');
const queue = require('./media/queue'); const queue = require('./media/queue');
const channelModel = require('../../schemas/channel/channelSchema'); const channelModel = require('../../schemas/channel/channelSchema');
const playlistHandler = require('./media/playlistHandler') const playlistHandler = require('./media/playlistHandler')
@ -30,7 +31,7 @@ module.exports = class{
this.queue = new queue(server, chanDB, this); this.queue = new queue(server, chanDB, this);
this.playlistHandler = new playlistHandler(server, chanDB, this); this.playlistHandler = new playlistHandler(server, chanDB, this);
//Define the chat buffer //Define the chat buffer
this.chatBuffer = []; this.chatBuffer = new chatBuffer(server, chanDB, this);
} }
async handleConnection(userDB, chanDB, socket){ async handleConnection(userDB, chanDB, socket){

View file

@ -0,0 +1,109 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const config = require('../../../config.json');
const channelModel = require('../../schemas/channel/channelSchema');
class chatBuffer{
constructor(server, chanDB, channel){
//Grab parent server and chan objects
this.server = server;
this.channel = channel;
//If we have no chanDB.chatBuffer
if(chanDB == null || chanDB.chatBuffer == null){
//Create RAM-based buffer array
this.buffer = [];
//Otherwise
}else{
//Pull buffer from DB
this.buffer = chanDB.chatBuffer;
}
//Create variables to hold timers for deciding when to write RAM buffer to DB
//Goes off 'this.inactivityDelay' seconds after the last chat was sent, assuming it isn't interrupted by new chats
this.inactivityTimer = null;
this.inactivityDelay = 10;
//Goes off 'this.busyDelay' minutes after the first chat message in the current volley of messages. Get's cancelled before being called if this.inactivityTimer goes off.
this.busyTimer = null;
this.busyDelay = 5;
}
push(chat){
//push chat into RAM buffer
this.buffer.push(chat);
//clear existing inactivity timer
clearTimeout(this.inactivityTimer);
//reset inactivity timer
this.inactivityTimer = setTimeout(this.handleInactivity.bind(this), 1000 * this.inactivityDelay);
//If busy timer is unset
if(this.busyTimer == null){
this.busyTimer = setTimeout(this.handleBusyRoom.bind(this), 1000 * 60 * this.busyDelay);
}
}
shift(){
//remove chat from RAM buffer, no need to handle DB timing here, since push should be called when this is called.
this.buffer.shift();
}
//Called after 10 seconds of chat room inactivity
handleInactivity(){
this.saveDB(`${this.inactivityDelay} seconds of inactivity.`);
}
//Called after 5 minutes of solid activity
handleBusyRoom(){
this.saveDB(`${this.busyDelay} minutes of activity.`);
}
async saveDB(reason, chanDB){
//clear existing timers
clearTimeout(this.inactivityTimer);
clearTimeout(this.busyTimer);
this.inactivityTimer = null;
this.busyTimer = null;
//if the server is in screamy boi mode
if(config.verbose){
//This should eventually be replaced by a per-channel logging feature that provides access to chan admins via web front-end
console.log(`Saving chat buffer to channel ${this.channel.name} after ${reason}.`);
}
//If we wheren't handed a channel
if(chanDB == null){
//Now that everything is clean, we can take our time with the DB :P
chanDB = await channelModel.findOne({name:this.channel.name});
}
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while saving chat buffer!`, "chat");
}
//Set chan doc buffer to RAM buffer
chanDB.chatBuffer = this.buffer;
//save chan doc to DB.
await chanDB.save();
}
}
module.exports = chatBuffer;

View file

@ -141,7 +141,7 @@ module.exports = class{
const channel = this.server.activeChannels.get(chan); const channel = this.server.activeChannels.get(chan);
//If chat buffer length is over mandated size //If chat buffer length is over mandated size
if(channel.chatBuffer.length >= this.chatBufferSize){ if(channel.chatBuffer.buffer.length >= this.chatBufferSize){
//Take out oldest chat //Take out oldest chat
channel.chatBuffer.shift(); channel.chatBuffer.shift();
} }

View file

@ -160,7 +160,7 @@ module.exports = class{
const queueLock = this.channel.queue.locked; const queueLock = this.channel.queue.locked;
//Get chat buffer //Get chat buffer
const chatBuffer = this.channel.chatBuffer; const chatBuffer = this.channel.chatBuffer.buffer;
//Send off the metadata to our user's clients //Send off the metadata to our user's clients
this.emit("clientMetadata", {user: userObj, flairList, queue, queueLock, chatBuffer}); this.emit("clientMetadata", {user: userObj, flairList, queue, queueLock, chatBuffer});

View file

@ -17,6 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//NPM Imports //NPM Imports
const {mongoose} = require('mongoose'); const {mongoose} = require('mongoose');
const linkSchema = new mongoose.Schema({
link: mongoose.SchemaTypes.String,
type: mongoose.SchemaTypes.String
});
const chatSchema = new mongoose.Schema({ const chatSchema = new mongoose.Schema({
user: { user: {
type: mongoose.SchemaTypes.String, type: mongoose.SchemaTypes.String,
@ -39,10 +44,9 @@ const chatSchema = new mongoose.Schema({
required: true, required: true,
}, },
links: { links: {
type: [mongoose.SchemaTypes.Number], type: [linkSchema],
required: true, required: true,
}, },
}); });
module.exports = chatSchema; module.exports = chatSchema;

View file

@ -84,6 +84,9 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){
//yell at the browser for fucking up, and tell it what it did wrong. //yell at the browser for fucking up, and tell it what it did wrong.
socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`}); socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`});
}else{ }else{
//Locally handle the exception
module.exports.localExceptionHandler(err);
//yell at the browser for fucking up //yell at the browser for fucking up
socket.emit("kick", {type: "Disconnected", reason: "An unexpected server crash was just prevented. You should probably report this to an admin."}); socket.emit("kick", {type: "Disconnected", reason: "An unexpected server crash was just prevented. You should probably report this to an admin."});
} }

View file

@ -145,6 +145,8 @@ class chatBox{
chatBody.classList.add("chat-panel-buffer","chat-entry-body"); chatBody.classList.add("chat-panel-buffer","chat-entry-body");
chatEntry.appendChild(chatBody); chatEntry.appendChild(chatBody);
console.log(data);
//Append the post-processed chat-body to the chat buffer //Append the post-processed chat-body to the chat buffer
this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data)); this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data));