From 552f3da67e67b2fe78a12d69d4de685373575f59 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 29 Nov 2017 21:54:09 +0200 Subject: [PATCH] Implement manual network/channel cloning for better performance --- src/client.js | 2 +- src/models/chan.js | 38 +++++++++++++++++++++++++++++++++----- src/models/network.js | 40 ++++++++++++++++++++++++++++++++-------- src/server.js | 15 +-------------- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/client.js b/src/client.js index 94a8cc59..3a1da35e 100644 --- a/src/client.js +++ b/src/client.js @@ -196,7 +196,7 @@ Client.prototype.connect = function(args) { client.networks.push(network); client.emit("network", { - networks: [network], + networks: [network.getFilteredClone(this.lastActiveChannel, -1)], }); if (config.lockNetwork) { diff --git a/src/models/chan.js b/src/models/chan.js index 764933bf..dbcdcd98 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -136,11 +136,39 @@ Chan.prototype.removeUser = function(user) { this.users.delete(user.nick.toLowerCase()); }; -Chan.prototype.toJSON = function() { - var clone = _.clone(this); - clone.users = []; // Do not send user list, the client will explicitly request it when needed - clone.messages = clone.messages.slice(-1); - return clone; +/** + * Get a clean clone of this channel that will be sent to the client. + * This function performs manual cloning of channel object for + * better control of performance and memory usage. + * + * @param {(int|bool)} lastActiveChannel - Last known active user channel id (needed to control how many messages are sent) + * If true, channel is assumed active. + * @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates. + */ +Chan.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) { + return Object.keys(this).reduce((newChannel, prop) => { + if (prop === "users") { + // Do not send users, client requests updated user list whenever needed + newChannel[prop] = []; + } else if (prop === "messages") { + // If channel is active, send up to 100 last messages, for all others send just 1 + // Client will automatically load more messages whenever needed based on last seen messages + const messagesToSend = lastActiveChannel === true || this.id === lastActiveChannel ? -100 : -1; + + // If client is reconnecting, only send new messages that client has not seen yet + if (lastMessage > -1) { + newChannel[prop] = this[prop] + .filter((m) => m.id > lastMessage) + .slice(messagesToSend); + } else { + newChannel[prop] = this[prop].slice(messagesToSend); + } + } else { + newChannel[prop] = this[prop]; + } + + return newChannel; + }, {}); }; function writeUserLog(client, msg) { diff --git a/src/models/network.js b/src/models/network.js index 7ad33776..61da4fed 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -7,6 +7,17 @@ module.exports = Network; let id = 1; +/** + * @type {Object} List of keys which should not be sent to the client. + */ +const filteredFromClient = { + awayMessage: true, + chanCache: true, + highlightRegex: true, + irc: true, + password: true, +}; + function Network(attr) { _.defaults(this, attr, { name: "", @@ -63,14 +74,27 @@ Network.prototype.setNick = function(nick) { ); }; -Network.prototype.toJSON = function() { - return _.omit(this, [ - "awayMessage", - "chanCache", - "highlightRegex", - "irc", - "password", - ]); +/** + * Get a clean clone of this network that will be sent to the client. + * This function performs manual cloning of network object for + * better control of performance and memory usage. + * + * Both of the parameters that are accepted by this function are passed into channels' getFilteredClone call. + * + * @see {@link Chan#getFilteredClone} + */ +Network.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) { + return Object.keys(this).reduce((newNetwork, prop) => { + if (prop === "channels") { + // Channels objects perform their own cloning + newNetwork[prop] = this[prop].map((channel) => channel.getFilteredClone(lastActiveChannel, lastMessage)); + } else if (!filteredFromClient[prop]) { + // Some properties that are not useful for the client are skipped + newNetwork[prop] = this[prop]; + } + + return newNetwork; + }, {}); }; Network.prototype.export = function() { diff --git a/src/server.js b/src/server.js index 418fbc92..fc1089b1 100644 --- a/src/server.js +++ b/src/server.js @@ -420,24 +420,11 @@ function initializeClient(socket, client, token, lastMessage) { socket.join(client.id); const sendInitEvent = (tokenToSend) => { - let networks = client.networks; - - if (lastMessage > -1) { - // We need a deep cloned object because we are going to remove unneeded messages - networks = _.cloneDeep(networks); - - networks.forEach((network) => { - network.channels.forEach((channel) => { - channel.messages = channel.messages.filter((m) => m.id > lastMessage); - }); - }); - } - socket.emit("init", { applicationServerKey: manager.webPush.vapidKeys.publicKey, pushSubscription: client.config.sessions[token], active: client.lastActiveChannel, - networks: networks, + networks: client.networks.map((network) => network.getFilteredClone(client.lastActiveChannel, lastMessage)), token: tokenToSend, }); };