Tons of fixes to prepare for staged releases.

This commit is contained in:
Max committed 2026-10-28 10:11:40 -07:00
1 parent 9f37d3faf1
commit 5bebe072b2
24 files changed
+1110 -474

No files matched your search

+103 -53
View File
@@ -16,6 +16,9 @@ require('module-alias')(require('path').join(__dirname, '..'))
//Auth helper, used for decoding users web token
let Auth = require('@helpers/Auth')
//Note model, used to authorise socket room joins
let Note = require('@models/Note')
//Helmet adds additional security to express server
const helmet = require('helmet')
@@ -51,6 +54,14 @@ global.SocketIo = io
let noteDiffs = {}
//
// Room names are namespaced. Previously note rooms were keyed on the bare
// note_raw_text.id and user rooms on the bare user.id, sharing one flat
// keyspace -- so user 7 and note_raw_text 7 were the same room.
//
const userRoom = userId => `user:${userId}`
const noteRoom = rawTextId => `note:${rawTextId}`
io.on('connection', function(socket){
//When a user connects, add them to their own room
@@ -59,11 +70,11 @@ io.on('connection', function(socket){
socket.on('user_connect', token => {
Auth.decodeToken(token)
.then(userData => {
socket.join(userData.userId)
socket.join(userRoom(userData.userId))
//Track active logged in user accounts
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
const usersInRoom = io.sockets.adapter.rooms[userRoom(userData.userId)]
io.to(userRoom(userData.userId)).emit('update_active_user_count', usersInRoom.length)
}).catch(error => {
//Don't add user to room if they are not logged in
@@ -74,11 +85,11 @@ io.on('connection', function(socket){
socket.on('get_active_user_count', token => {
Auth.decodeToken(token)
.then(userData => {
socket.join(userData.userId)
socket.join(userRoom(userData.userId))
//Track active logged in user accounts
const usersInRoom = io.sockets.adapter.rooms[userData.userId]
io.to(userData.userId).emit('update_active_user_count', usersInRoom.length)
const usersInRoom = io.sockets.adapter.rooms[userRoom(userData.userId)]
io.to(userRoom(userData.userId)).emit('update_active_user_count', usersInRoom.length)
}).catch(error => {
// console.log(error)
@@ -107,67 +118,106 @@ io.on('connection', function(socket){
})
})
socket.on('join_room', rawTextId => {
// Join user to rawtextid room when they enter
socket.join(rawTextId)
//
// The client asks for a NOTE, not a room. The server authorises the request
// against the session and derives the room itself, so a client can no longer
// name an arbitrary room and be handed its buffered diffs.
//
socket.on('join_room', ({ noteId, token } = {}) => {
//If there are past diffs for this note, send them to the user
if(noteDiffs[rawTextId] != undefined){
//Sort all note diffs by when they were created.
noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
Auth.decodeToken(token)
.then(userData => {
//Emit all sorted diffs to user
socket.emit('past_diffs', noteDiffs[rawTextId])
}
return Note.getRawTextIdForUser(noteId, userData.userId)
.then(rawTextId => {
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
if(usersInRoom){
//Update users in room count
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
}
})
// No claim on this note -> no room, no diffs
if(rawTextId == null){ return }
socket.on('leave_room', roomId => {
socket.leave(roomId)
// console.log('User Left room')
const room = noteRoom(rawTextId)
const usersInRoom = io.sockets.adapter.rooms[roomId]
if(usersInRoom){
// console.log('Users in room', usersInRoom.length)
io.to(roomId).emit('update_user_count', usersInRoom.length)
}
})
// Remember what this socket is allowed into, so leave_room and
// note_diff never have to trust a client-supplied room again
socket.authorizedRooms = socket.authorizedRooms || {}
socket.authorizedRooms[noteId] = { room, rawTextId }
socket.on('note_diff', data => {
socket.join(room)
//Log each diff for note
const noteId = data.id
delete data.id
if(noteDiffs[noteId] == undefined){ noteDiffs[noteId] = [] }
data.time = +new Date
noteDiffs[noteId].push(data)
//If there are past diffs for this note, send them to the user
if(noteDiffs[rawTextId] != undefined){
// Go over each user in this note-room
io.in(noteId).clients((error, clients) => {
if (error) throw error;
//Sort all note diffs by when they were created.
noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time })
//Go through each client in note-room and send them the diff
clients.forEach(socketId => {
// only send off diff if user
if(socketId != socket.id){
io.to(socketId).emit('incoming_diff', data)
//Emit all sorted diffs to user
socket.emit('past_diffs', noteDiffs[rawTextId])
}
const usersInRoom = io.sockets.adapter.rooms[room]
if(usersInRoom){
//Update users in room count
io.to(room).emit('update_user_count', usersInRoom.length)
}
})
});
})
.catch(() => {
//Not logged in, or no access. Join nothing.
})
})
socket.on('truncate_diffs_at_save', checkpoint => {
socket.on('leave_room', ({ noteId } = {}) => {
// Only rooms this socket was actually authorised into
const entry = socket.authorizedRooms && socket.authorizedRooms[noteId]
if(!entry){ return }
socket.leave(entry.room)
delete socket.authorizedRooms[noteId]
const usersInRoom = io.sockets.adapter.rooms[entry.room]
if(usersInRoom){
io.to(entry.room).emit('update_user_count', usersInRoom.length)
}
})
socket.on('note_diff', (data = {}) => {
// Room comes from what this socket was authorised into, never from the
// payload
const entry = socket.authorizedRooms && socket.authorizedRooms[data.noteId]
if(!entry){ return }
const room = entry.room
const rawTextId = entry.rawTextId
//Log each diff for note
if(noteDiffs[rawTextId] == undefined){ noteDiffs[rawTextId] = [] }
// Carry the raw-text id through. This used to `delete data.id`, stripping
// the only identifier before storing and broadcasting -- so a client with
// several notes open on one socket could not tell which note a patch
// belonged to, and applied every one to whichever editor it found first.
const payload = Object.assign({}, data, { rawTextId, time: +new Date })
delete payload.noteId
noteDiffs[rawTextId].push(payload)
// Everyone in the note room except the sender
socket.to(room).emit('incoming_diff', payload)
})
socket.on('truncate_diffs_at_save', (checkpoint = {}) => {
const entry = socket.authorizedRooms && socket.authorizedRooms[checkpoint.noteId]
if(!entry){ return }
checkpoint.rawTextId = entry.rawTextId
// NOTE: the comparison below is `diffSet[i].hash == checkpoint`, comparing
// a hash string against the whole checkpoint object, so it never matches
// and sliceTo stays 0 -- meaning the entire diff set is discarded at every
// save. That happens to be the fail-safe outcome (the saved text becomes
// the new baseline), and the correct slice direction for a real fix is
// ambiguous, so it is deliberately left alone rather than guessed at.
let diffSet = noteDiffs[checkpoint.rawTextId]
if(diffSet && diffSet.length > 0){