mirror of
https://github.com/thelounge/thelounge
synced 2024-11-25 13:30:21 +00:00
Merge pull request #1778 from thelounge/xpaw/lazy-init
Heavily improve performance of "init" event
This commit is contained in:
commit
2d0ddfb2e8
13 changed files with 202 additions and 40 deletions
|
@ -12,7 +12,7 @@
|
||||||
<span title="{{topic}}" class="topic">{{{parse topic}}}</span>
|
<span title="{{topic}}" class="topic">{{{parse topic}}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat">
|
<div class="chat">
|
||||||
<div class="show-more {{#equal messages.length 100}}show{{/equal}}">
|
<div class="show-more{{#if messages.length}} show{{/if}}">
|
||||||
<button class="show-more-button" data-id="{{id}}">Show older messages</button>
|
<button class="show-more-button" data-id="{{id}}">Show older messages</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="messages"></div>
|
<div class="messages"></div>
|
||||||
|
|
|
@ -197,7 +197,7 @@ Client.prototype.connect = function(args) {
|
||||||
|
|
||||||
client.networks.push(network);
|
client.networks.push(network);
|
||||||
client.emit("network", {
|
client.emit("network", {
|
||||||
networks: [network],
|
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.lockNetwork) {
|
if (config.lockNetwork) {
|
||||||
|
|
|
@ -136,11 +136,39 @@ Chan.prototype.removeUser = function(user) {
|
||||||
this.users.delete(user.nick.toLowerCase());
|
this.users.delete(user.nick.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
Chan.prototype.toJSON = function() {
|
/**
|
||||||
var clone = _.clone(this);
|
* Get a clean clone of this channel that will be sent to the client.
|
||||||
clone.users = []; // Do not send user list, the client will explicitly request it when needed
|
* This function performs manual cloning of channel object for
|
||||||
clone.messages = clone.messages.slice(-100);
|
* better control of performance and memory usage.
|
||||||
return clone;
|
*
|
||||||
|
* @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) {
|
function writeUserLog(client, msg) {
|
||||||
|
|
|
@ -7,6 +7,17 @@ module.exports = Network;
|
||||||
|
|
||||||
let id = 1;
|
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) {
|
function Network(attr) {
|
||||||
_.defaults(this, attr, {
|
_.defaults(this, attr, {
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -63,14 +74,27 @@ Network.prototype.setNick = function(nick) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Network.prototype.toJSON = function() {
|
/**
|
||||||
return _.omit(this, [
|
* Get a clean clone of this network that will be sent to the client.
|
||||||
"awayMessage",
|
* This function performs manual cloning of network object for
|
||||||
"chanCache",
|
* better control of performance and memory usage.
|
||||||
"highlightRegex",
|
*
|
||||||
"irc",
|
* Both of the parameters that are accepted by this function are passed into channels' getFilteredClone call.
|
||||||
"password",
|
*
|
||||||
]);
|
* @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() {
|
Network.prototype.export = function() {
|
||||||
|
|
|
@ -47,6 +47,6 @@ exports.input = function(network, chan, cmd, args) {
|
||||||
network.channels.push(newChan);
|
network.channels.push(newChan);
|
||||||
this.emit("join", {
|
this.emit("join", {
|
||||||
network: network.id,
|
network: network.id,
|
||||||
chan: newChan,
|
chan: newChan.getFilteredClone(true),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ module.exports = function(irc, network) {
|
||||||
network.channels.push(chan);
|
network.channels.push(chan);
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.id,
|
network: network.id,
|
||||||
chan: chan,
|
chan: chan.getFilteredClone(true),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ module.exports = function(irc, network) {
|
||||||
client.save();
|
client.save();
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.id,
|
network: network.id,
|
||||||
chan: chan,
|
chan: chan.getFilteredClone(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request channels' modes
|
// Request channels' modes
|
||||||
|
|
|
@ -49,7 +49,7 @@ module.exports = function(irc, network) {
|
||||||
network.channels.push(chan);
|
network.channels.push(chan);
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.id,
|
network: network.id,
|
||||||
chan: chan,
|
chan: chan.getFilteredClone(true),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ module.exports = function(irc, network) {
|
||||||
network.channels.push(chan);
|
network.channels.push(chan);
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
network: network.id,
|
network: network.id,
|
||||||
chan: chan,
|
chan: chan.getFilteredClone(true),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ module.exports = function(irc, network) {
|
||||||
client.emit("join", {
|
client.emit("join", {
|
||||||
shouldOpen: true,
|
shouldOpen: true,
|
||||||
network: network.id,
|
network: network.id,
|
||||||
chan: chan,
|
chan: chan.getFilteredClone(true),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -420,24 +420,11 @@ function initializeClient(socket, client, token, lastMessage) {
|
||||||
socket.join(client.id);
|
socket.join(client.id);
|
||||||
|
|
||||||
const sendInitEvent = (tokenToSend) => {
|
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", {
|
socket.emit("init", {
|
||||||
applicationServerKey: manager.webPush.vapidKeys.publicKey,
|
applicationServerKey: manager.webPush.vapidKeys.publicKey,
|
||||||
pushSubscription: client.config.sessions[token],
|
pushSubscription: client.config.sessions[token],
|
||||||
active: client.lastActiveChannel,
|
active: client.lastActiveChannel,
|
||||||
networks: networks,
|
networks: client.networks.map((network) => network.getFilteredClone(client.lastActiveChannel, lastMessage)),
|
||||||
token: tokenToSend,
|
token: tokenToSend,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
|
const Chan = require("../../src/models/chan");
|
||||||
var Chan = require("../../src/models/chan");
|
const Msg = require("../../src/models/msg");
|
||||||
var Msg = require("../../src/models/msg");
|
const User = require("../../src/models/user");
|
||||||
var Network = require("../../src/models/network");
|
const Network = require("../../src/models/network");
|
||||||
|
|
||||||
describe("Network", function() {
|
describe("Network", function() {
|
||||||
describe("#export()", function() {
|
describe("#export()", function() {
|
||||||
|
@ -91,4 +91,38 @@ describe("Network", function() {
|
||||||
expect(network.channels[1].messages[2].text).to.equal("message after network creation");
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue