+
diff --git a/src/client.js b/src/client.js
index 65db1235..fe602bce 100644
--- a/src/client.js
+++ b/src/client.js
@@ -197,7 +197,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 63b45203..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(-100);
- 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/plugins/inputs/query.js b/src/plugins/inputs/query.js
index 6d9d3a26..501b8bc1 100644
--- a/src/plugins/inputs/query.js
+++ b/src/plugins/inputs/query.js
@@ -47,6 +47,6 @@ exports.input = function(network, chan, cmd, args) {
network.channels.push(newChan);
this.emit("join", {
network: network.id,
- chan: newChan,
+ chan: newChan.getFilteredClone(true),
});
};
diff --git a/src/plugins/irc-events/banlist.js b/src/plugins/irc-events/banlist.js
index 9a7a119c..adac4cc0 100644
--- a/src/plugins/irc-events/banlist.js
+++ b/src/plugins/irc-events/banlist.js
@@ -29,7 +29,7 @@ module.exports = function(irc, network) {
network.channels.push(chan);
client.emit("join", {
network: network.id,
- chan: chan,
+ chan: chan.getFilteredClone(true),
});
}
diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js
index a781d059..6e53d42b 100644
--- a/src/plugins/irc-events/join.js
+++ b/src/plugins/irc-events/join.js
@@ -18,7 +18,7 @@ module.exports = function(irc, network) {
client.save();
client.emit("join", {
network: network.id,
- chan: chan,
+ chan: chan.getFilteredClone(true),
});
// Request channels' modes
diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js
index 3122d1b8..0ddf2059 100644
--- a/src/plugins/irc-events/list.js
+++ b/src/plugins/irc-events/list.js
@@ -49,7 +49,7 @@ module.exports = function(irc, network) {
network.channels.push(chan);
client.emit("join", {
network: network.id,
- chan: chan,
+ chan: chan.getFilteredClone(true),
});
}
diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js
index 5ddcc0f3..894adaa0 100644
--- a/src/plugins/irc-events/message.js
+++ b/src/plugins/irc-events/message.js
@@ -68,7 +68,7 @@ module.exports = function(irc, network) {
network.channels.push(chan);
client.emit("join", {
network: network.id,
- chan: chan,
+ chan: chan.getFilteredClone(true),
});
}
}
diff --git a/src/plugins/irc-events/whois.js b/src/plugins/irc-events/whois.js
index 6747c2ed..d07836c0 100644
--- a/src/plugins/irc-events/whois.js
+++ b/src/plugins/irc-events/whois.js
@@ -17,7 +17,7 @@ module.exports = function(irc, network) {
client.emit("join", {
shouldOpen: true,
network: network.id,
- chan: chan,
+ chan: chan.getFilteredClone(true),
});
}
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,
});
};
diff --git a/test/models/chan.js b/test/models/chan.js
index 77dc1752..d9c2c182 100644
--- a/test/models/chan.js
+++ b/test/models/chan.js
@@ -145,4 +145,93 @@ describe("Chan", function() {
]);
});
});
+
+ describe("#getFilteredClone(lastActiveChannel, lastMessage)", function() {
+ it("should send empty user list", function() {
+ const chan = new Chan();
+ chan.setUser(new User({nick: "test"}));
+
+ expect(chan.getFilteredClone().users).to.be.empty;
+ });
+
+ it("should keep necessary properties", function() {
+ const chan = new Chan();
+
+ expect(chan.getFilteredClone()).to.be.an("object").that.has.all.keys(
+ "firstUnread",
+ "highlight",
+ "id",
+ "key",
+ "messages",
+ "name",
+ "topic",
+ "type",
+ "unread",
+ "users"
+ );
+ });
+
+ it("should send only last message for non active channel", function() {
+ const chan = new Chan({
+ id: 1337,
+ messages: [
+ new Msg({id: 10}),
+ new Msg({id: 11}),
+ new Msg({id: 12}),
+ new Msg({id: 13}),
+ ],
+ });
+
+ expect(chan.id).to.equal(1337);
+
+ const messages = chan.getFilteredClone(999).messages;
+
+ expect(messages).to.have.lengthOf(1);
+ expect(messages[0].id).to.equal(13);
+ });
+
+ it("should send more messages for active channel", function() {
+ const chan = new Chan({
+ id: 1337,
+ messages: [
+ new Msg({id: 10}),
+ new Msg({id: 11}),
+ new Msg({id: 12}),
+ new Msg({id: 13}),
+ ],
+ });
+
+ expect(chan.id).to.equal(1337);
+
+ const messages = chan.getFilteredClone(1337).messages;
+
+ expect(messages).to.have.lengthOf(4);
+ expect(messages[0].id).to.equal(10);
+ expect(messages[3].id).to.equal(13);
+
+ expect(chan.getFilteredClone(true).messages).to.have.lengthOf(4);
+ });
+
+ it("should only send new messages", function() {
+ const chan = new Chan({
+ id: 1337,
+ messages: [
+ new Msg({id: 10}),
+ new Msg({id: 11}),
+ new Msg({id: 12}),
+ new Msg({id: 13}),
+ new Msg({id: 14}),
+ new Msg({id: 15}),
+ ],
+ });
+
+ expect(chan.id).to.equal(1337);
+
+ const messages = chan.getFilteredClone(1337, 12).messages;
+
+ expect(messages).to.have.lengthOf(3);
+ expect(messages[0].id).to.equal(13);
+ expect(messages[2].id).to.equal(15);
+ });
+ });
});
diff --git a/test/models/network.js b/test/models/network.js
index a8a97895..97354295 100644
--- a/test/models/network.js
+++ b/test/models/network.js
@@ -1,10 +1,10 @@
"use strict";
-var expect = require("chai").expect;
-
-var Chan = require("../../src/models/chan");
-var Msg = require("../../src/models/msg");
-var Network = require("../../src/models/network");
+const expect = require("chai").expect;
+const Chan = require("../../src/models/chan");
+const Msg = require("../../src/models/msg");
+const User = require("../../src/models/user");
+const Network = require("../../src/models/network");
describe("Network", function() {
describe("#export()", function() {
@@ -91,4 +91,38 @@ describe("Network", function() {
expect(network.channels[1].messages[2].text).to.equal("message after network creation");
});
});
+
+ describe("#getFilteredClone(lastActiveChannel, lastMessage)", function() {
+ it("should filter channels", function() {
+ const chan = new Chan();
+ chan.setUser(new User({nick: "test"}));
+
+ const network = new Network({
+ channels: [
+ chan,
+ ],
+ });
+
+ expect(network.channels[0].users).to.be.empty;
+ });
+
+ it("should keep necessary properties", function() {
+ const network = new Network();
+
+ expect(network.getFilteredClone()).to.be.an("object").that.has.all.keys(
+ "channels",
+ "commands",
+ "host",
+ "hostname",
+ "id",
+ "ip",
+ "name",
+ "port",
+ "realname",
+ "serverOptions",
+ "tls",
+ "username"
+ );
+ });
+ });
});