fix: harden data integrity, connection stability, and memory management
Wrap all multi-write DB operations in atomic transactions (server/channel creation, message+attachment linking, DM creation, friend acceptance, cascading deletes) to prevent partial-write corruption. Batch N+1 queries in WS ready payload into O(1) bulk fetches with chunked inArray() to respect SQLite's variable limit. Fix chat history regression where background WS messages bypassed channel load by switching the guard from messages.has() to hasMore.has(). Add LRU channel eviction (20 cached, evict to 15) and per-channel message cap (200) to bound client memory growth. Shorten WS heartbeat from 30s to 15s for aggressive proxy/NAT environments. Clear all user-scoped stores on logout to prevent cross-session data leaks. Extract LiveKit internal accessors into shared livekitInternals utility.
This commit is contained in:
@@ -374,24 +374,26 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Create new DM channel
|
||||
// Create new DM channel with both members atomically
|
||||
const dmChannelId = generateSnowflake();
|
||||
const now = Date.now();
|
||||
|
||||
db.insert(schema.dmChannels).values({
|
||||
id: dmChannelId,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
db.transaction((tx) => {
|
||||
tx.insert(schema.dmChannels).values({
|
||||
id: dmChannelId,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
|
||||
db.insert(schema.dmMembers).values({
|
||||
dmChannelId,
|
||||
userId: request.userId,
|
||||
}).run();
|
||||
tx.insert(schema.dmMembers).values({
|
||||
dmChannelId,
|
||||
userId: request.userId,
|
||||
}).run();
|
||||
|
||||
db.insert(schema.dmMembers).values({
|
||||
dmChannelId,
|
||||
userId,
|
||||
}).run();
|
||||
tx.insert(schema.dmMembers).values({
|
||||
dmChannelId,
|
||||
userId,
|
||||
}).run();
|
||||
});
|
||||
|
||||
const currentUserRow = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
||||
const members = [currentUserRow, targetUser]
|
||||
@@ -791,24 +793,26 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
const messageId = generateSnowflake();
|
||||
const now = Date.now();
|
||||
|
||||
db.insert(schema.dmMessages).values({
|
||||
id: messageId,
|
||||
dmChannelId: id,
|
||||
userId: request.userId,
|
||||
replyToId: replyToId || null,
|
||||
content: content?.trim() || null,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
// Insert message and link attachments atomically
|
||||
db.transaction((tx) => {
|
||||
tx.insert(schema.dmMessages).values({
|
||||
id: messageId,
|
||||
dmChannelId: id,
|
||||
userId: request.userId,
|
||||
replyToId: replyToId || null,
|
||||
content: content?.trim() || null,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
|
||||
// Link attachments to this DM message
|
||||
if (attachmentIds && attachmentIds.length > 0) {
|
||||
for (const attId of attachmentIds) {
|
||||
db.update(schema.attachments)
|
||||
.set({ dmMessageId: messageId })
|
||||
.where(eq(schema.attachments.id, attId))
|
||||
.run();
|
||||
if (attachmentIds && attachmentIds.length > 0) {
|
||||
for (const attId of attachmentIds) {
|
||||
tx.update(schema.attachments)
|
||||
.set({ dmMessageId: messageId })
|
||||
.where(eq(schema.attachments.id, attId))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const message = getDmMessageWithUser(messageId);
|
||||
if (!message) {
|
||||
@@ -886,20 +890,20 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(403).send({ error: 'You can only delete your own messages', statusCode: 403 });
|
||||
}
|
||||
|
||||
// Delete attachments linked to this DM message
|
||||
db.delete(schema.attachments)
|
||||
.where(eq(schema.attachments.dmMessageId, id))
|
||||
.run();
|
||||
// Delete attachments, reactions, and message atomically
|
||||
db.transaction((tx) => {
|
||||
tx.delete(schema.attachments)
|
||||
.where(eq(schema.attachments.dmMessageId, id))
|
||||
.run();
|
||||
|
||||
// Delete reactions
|
||||
db.delete(schema.dmReactions)
|
||||
.where(eq(schema.dmReactions.dmMessageId, id))
|
||||
.run();
|
||||
tx.delete(schema.dmReactions)
|
||||
.where(eq(schema.dmReactions.dmMessageId, id))
|
||||
.run();
|
||||
|
||||
// Delete message
|
||||
db.delete(schema.dmMessages)
|
||||
.where(eq(schema.dmMessages.id, id))
|
||||
.run();
|
||||
tx.delete(schema.dmMessages)
|
||||
.where(eq(schema.dmMessages.id, id))
|
||||
.run();
|
||||
});
|
||||
|
||||
// Broadcast to all DM members
|
||||
const dmMembers = db.select()
|
||||
|
||||
@@ -274,24 +274,26 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
||||
const messageId = generateSnowflake();
|
||||
const now = Date.now();
|
||||
|
||||
db.insert(schema.messages).values({
|
||||
id: messageId,
|
||||
channelId: id,
|
||||
userId: request.userId,
|
||||
replyToId: replyToId || null,
|
||||
content: content?.trim() || null,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
// Insert message and link attachments atomically
|
||||
db.transaction((tx) => {
|
||||
tx.insert(schema.messages).values({
|
||||
id: messageId,
|
||||
channelId: id,
|
||||
userId: request.userId,
|
||||
replyToId: replyToId || null,
|
||||
content: content?.trim() || null,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
|
||||
// Link attachments to message
|
||||
if (attachmentIds && attachmentIds.length > 0) {
|
||||
for (const attId of attachmentIds) {
|
||||
db.update(schema.attachments)
|
||||
.set({ messageId })
|
||||
.where(eq(schema.attachments.id, attId))
|
||||
.run();
|
||||
if (attachmentIds && attachmentIds.length > 0) {
|
||||
for (const attId of attachmentIds) {
|
||||
tx.update(schema.attachments)
|
||||
.set({ messageId })
|
||||
.where(eq(schema.attachments.id, attId))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const user = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
||||
if (!user) {
|
||||
@@ -415,9 +417,11 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(403).send({ error: 'You cannot delete this message', statusCode: 403 });
|
||||
}
|
||||
|
||||
// Delete attachments then message
|
||||
db.delete(schema.attachments).where(eq(schema.attachments.messageId, id)).run();
|
||||
db.delete(schema.messages).where(eq(schema.messages.id, id)).run();
|
||||
// Delete attachments and message atomically
|
||||
db.transaction((tx) => {
|
||||
tx.delete(schema.attachments).where(eq(schema.attachments.messageId, id)).run();
|
||||
tx.delete(schema.messages).where(eq(schema.messages.id, id)).run();
|
||||
});
|
||||
|
||||
// Broadcast deletion
|
||||
connectionManager.sendToServer(serverId, {
|
||||
|
||||
@@ -80,33 +80,33 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
const now = Date.now();
|
||||
const inviteCode = generateInviteCode();
|
||||
|
||||
// Create the server
|
||||
db.insert(schema.servers).values({
|
||||
id: serverId,
|
||||
name: trimmedName,
|
||||
icon: icon ?? null,
|
||||
ownerId: request.userId,
|
||||
inviteCode,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
// Create server, owner membership, and default channel atomically
|
||||
db.transaction((tx) => {
|
||||
tx.insert(schema.servers).values({
|
||||
id: serverId,
|
||||
name: trimmedName,
|
||||
icon: icon ?? null,
|
||||
ownerId: request.userId,
|
||||
inviteCode,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
|
||||
// Add owner as member with 'owner' role
|
||||
db.insert(schema.serverMembers).values({
|
||||
serverId,
|
||||
userId: request.userId,
|
||||
role: 'owner',
|
||||
joinedAt: now,
|
||||
}).run();
|
||||
tx.insert(schema.serverMembers).values({
|
||||
serverId,
|
||||
userId: request.userId,
|
||||
role: 'owner',
|
||||
joinedAt: now,
|
||||
}).run();
|
||||
|
||||
// Create default #general text channel
|
||||
db.insert(schema.channels).values({
|
||||
id: channelId,
|
||||
serverId,
|
||||
name: 'general',
|
||||
type: 'text',
|
||||
position: 0,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
tx.insert(schema.channels).values({
|
||||
id: channelId,
|
||||
serverId,
|
||||
name: 'general',
|
||||
type: 'text',
|
||||
position: 0,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
});
|
||||
|
||||
const server = db.select().from(schema.servers).where(eq(schema.servers.id, serverId)).get();
|
||||
if (!server) {
|
||||
@@ -302,10 +302,12 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.code(403).send({ error: 'Only the server owner can delete the server', statusCode: 403 });
|
||||
}
|
||||
|
||||
// Delete all channels (messages cascade), members, then server
|
||||
db.delete(schema.channels).where(eq(schema.channels.serverId, id)).run();
|
||||
db.delete(schema.serverMembers).where(eq(schema.serverMembers.serverId, id)).run();
|
||||
db.delete(schema.servers).where(eq(schema.servers.id, id)).run();
|
||||
// Delete all channels (messages cascade), members, then server atomically
|
||||
db.transaction((tx) => {
|
||||
tx.delete(schema.channels).where(eq(schema.channels.serverId, id)).run();
|
||||
tx.delete(schema.serverMembers).where(eq(schema.serverMembers.serverId, id)).run();
|
||||
tx.delete(schema.servers).where(eq(schema.servers.id, id)).run();
|
||||
});
|
||||
|
||||
return reply.code(200).send({ success: true });
|
||||
});
|
||||
|
||||
@@ -207,15 +207,22 @@ export async function socialRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
|
||||
if (status === 'accepted') {
|
||||
// Add to friends table
|
||||
// Insert friend and update request status atomically
|
||||
const now = Date.now();
|
||||
db.insert(schema.friends).values({
|
||||
userId: friendRequest.fromId,
|
||||
friendId: friendRequest.toId,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
db.transaction((tx) => {
|
||||
tx.insert(schema.friends).values({
|
||||
userId: friendRequest.fromId,
|
||||
friendId: friendRequest.toId,
|
||||
createdAt: now,
|
||||
}).run();
|
||||
|
||||
// Get the accepting user's data for the WS event
|
||||
tx.update(schema.friendRequests)
|
||||
.set({ status })
|
||||
.where(eq(schema.friendRequests.id, id))
|
||||
.run();
|
||||
});
|
||||
|
||||
// WS broadcast AFTER transaction commits
|
||||
const acceptingUser = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
|
||||
if (acceptingUser) {
|
||||
const friend: Friend = {
|
||||
@@ -228,14 +235,14 @@ export async function socialRoutes(app: FastifyInstance): Promise<void> {
|
||||
requestId: id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// For declined, just update the status (single write, no transaction needed)
|
||||
db.update(schema.friendRequests)
|
||||
.set({ status })
|
||||
.where(eq(schema.friendRequests.id, id))
|
||||
.run();
|
||||
}
|
||||
|
||||
// Update request status
|
||||
db.update(schema.friendRequests)
|
||||
.set({ status })
|
||||
.where(eq(schema.friendRequests.id, id))
|
||||
.run();
|
||||
|
||||
return reply.code(200).send({ success: true });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user