Fix race condition with NICK and QUIT when multiple dispatch users are in the same channel

This commit is contained in:
Ken-Håvard Lieng 2017-04-11 03:49:52 +02:00
parent 3393b1b706
commit 18651c1a10
8 changed files with 145 additions and 138 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,14 @@
export default function createCommandMiddleware(type, handlers) { export default function createCommandMiddleware(type, handlers) {
return store => next => action => { return ({ dispatch, getState }) => next => action => {
if (action.type === type) { if (action.type === type) {
const words = action.command.slice(1).split(' '); const words = action.command.slice(1).split(' ');
const command = words[0]; const command = words[0];
const params = words.slice(1); const params = words.slice(1);
if (Object.prototype.hasOwnProperty.call(handlers, command)) { if (command in handlers) {
handlers[command]({ handlers[command]({
dispatch: store.dispatch, dispatch,
getState: store.getState, getState,
server: action.server, server: action.server,
channel: action.channel channel: action.channel
}, ...params); }, ...params);

View File

@ -104,15 +104,15 @@ export default createReducer(Map(), {
}, },
[actions.SOCKET_NICK](state, action) { [actions.SOCKET_NICK](state, action) {
const { server, channels } = action; const { server } = action;
return state.withMutations(s => { return state.withMutations(s => {
channels.forEach(channel => { s.get(server).forEach((v, channel) => {
s.updateIn([server, channel, 'users'], users => { s.updateIn([server, channel, 'users'], users =>
const i = users.findIndex(user => user.nick === action.old); users.update(
return users.update(i, user => users.findIndex(user => user.nick === action.old),
updateRenderName(user.set('nick', action.new)) user => updateRenderName(user.set('nick', action.new))
).sort(compareUsers); ).sort(compareUsers)
}); );
}); });
}); });
}, },

View File

@ -7,20 +7,29 @@ function withReason(message, reason) {
return message + (reason ? ` (${reason})` : ''); return message + (reason ? ` (${reason})` : '');
} }
export default function handleSocket(socket, { dispatch, getState }) { function findChannels(state, server, user) {
socket.onAny((event, data) => { const channels = [];
const type = `SOCKET_${event.toUpperCase()}`;
if (Array.isArray(data)) { state.channels.get(server).forEach((channel, channelName) => {
dispatch({ type, data }); if (channel.get('users').find(u => u.nick === user)) {
} else { channels.push(channelName);
dispatch({ type, ...data });
} }
}); });
socket.on('message', message => dispatch(addMessage(message))); return channels;
socket.on('pm', message => dispatch(addMessage(message))); }
socket.on('join', data => { export default function handleSocket(socket, { dispatch, getState }) {
const handlers = {
message(message) {
dispatch(addMessage(message));
},
pm(message) {
dispatch(addMessage(message));
},
join(data) {
const state = getState(); const state = getState();
const { server, channel } = state.tab.selected; const { server, channel } = state.tab.selected;
if (server && channel) { if (server && channel) {
@ -33,39 +42,39 @@ export default function handleSocket(socket, { dispatch, getState }) {
dispatch(select(server, joinedChannel)); dispatch(select(server, joinedChannel));
} }
} }
});
socket.on('servers', data => { dispatch(inform(`${data.user} joined the channel`, data.server, data.channels[0]));
},
servers(data) {
if (!data) { if (!data) {
dispatch(routeActions.replace('/connect')); dispatch(routeActions.replace('/connect'));
} }
}); },
socket.on('join', ({ user, server, channels }) => part({ user, server, channel, reason }) {
dispatch(inform(`${user} joined the channel`, server, channels[0])) dispatch(inform(withReason(`${user} left the channel`, reason), server, channel));
); },
socket.on('part', ({ user, server, channel, reason }) => quit({ user, server, reason }) {
dispatch(inform(withReason(`${user} left the channel`, reason), server, channel)) const channels = findChannels(getState(), server, user);
); dispatch(broadcast(withReason(`${user} quit`, reason), server, channels));
},
socket.on('quit', ({ user, server, reason, channels }) => nick(data) {
dispatch(broadcast(withReason(`${user} quit`, reason), server, channels)) const channels = findChannels(getState(), data.server, data.old);
); dispatch(broadcast(`${data.old} changed nick to ${data.new}`, data.server, channels));
},
socket.on('nick', data => motd({ content, server }) {
dispatch(broadcast(`${data.old} changed nick to ${data.new}`, data.server, data.channels))
);
socket.on('motd', ({ content, server }) =>
dispatch(addMessages(content.map(line => ({ dispatch(addMessages(content.map(line => ({
server, server,
to: server, to: server,
message: line message: line
})))) }))));
); },
socket.on('whois', data => { whois(data) {
const tab = getState().tab.selected; const tab = getState().tab.selected;
dispatch(inform([ dispatch(inform([
@ -76,7 +85,23 @@ export default function handleSocket(socket, { dispatch, getState }) {
`Server: ${data.server}`, `Server: ${data.server}`,
`Channels: ${data.channels}` `Channels: ${data.channels}`
], tab.server, tab.channel)); ], tab.server, tab.channel));
}); },
socket.on('print', ({ server, message }) => dispatch(inform(message, server))); print({ server, message }) {
dispatch(inform(message, server));
}
};
socket.onMessage((type, data) => {
if (type in handlers) {
handlers[type](data);
}
type = `SOCKET_${type.toUpperCase()}`;
if (Array.isArray(data)) {
dispatch({ type, data });
} else {
dispatch({ type, ...data });
}
});
} }

View File

@ -1,10 +1,7 @@
import EventEmitter2 from 'eventemitter2';
import Backoff from 'backo'; import Backoff from 'backo';
export default class Socket extends EventEmitter2 { export default class Socket {
constructor(host) { constructor(host) {
super();
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
this.url = `${protocol}://${host}/ws`; this.url = `${protocol}://${host}/ws`;
@ -15,6 +12,7 @@ export default class Socket extends EventEmitter2 {
max: 5000, max: 5000,
jitter: 0.25 jitter: 0.25
}); });
this.handlers = [];
this.connect(); this.connect();
} }
@ -30,7 +28,6 @@ export default class Socket extends EventEmitter2 {
this.ws.onopen = () => { this.ws.onopen = () => {
clearTimeout(this.timeoutConnect); clearTimeout(this.timeoutConnect);
this.backoff.reset(); this.backoff.reset();
this.emit('connect');
this.setTimeoutPing(); this.setTimeoutPing();
}; };
@ -38,7 +35,6 @@ export default class Socket extends EventEmitter2 {
clearTimeout(this.timeoutConnect); clearTimeout(this.timeoutConnect);
clearTimeout(this.timeoutPing); clearTimeout(this.timeoutPing);
if (!this.closing) { if (!this.closing) {
this.emit('disconnect');
this.retry(); this.retry();
} }
this.closing = false; this.closing = false;
@ -76,10 +72,19 @@ export default class Socket extends EventEmitter2 {
setTimeoutPing() { setTimeoutPing() {
clearTimeout(this.timeoutPing); clearTimeout(this.timeoutPing);
this.timeoutPing = setTimeout(() => { this.timeoutPing = setTimeout(() => {
this.emit('disconnect');
this.closing = true; this.closing = true;
this.ws.close(); this.ws.close();
this.connect(); this.connect();
}, this.pingTimeout); }, this.pingTimeout);
} }
onMessage(handler) {
this.handlers.push(handler);
}
emit(type, data) {
for (let i = 0; i < this.handlers.length; i++) {
this.handlers[i](type, data);
}
}
} }

View File

@ -60,7 +60,6 @@ func (i *ircHandler) nick(msg *irc.Message) {
Server: i.client.Host, Server: i.client.Host,
Old: msg.Nick, Old: msg.Nick,
New: msg.LastParam(), New: msg.LastParam(),
Channels: channelStore.FindUserChannels(msg.Nick, i.client.Host),
}) })
channelStore.RenameUser(msg.Nick, msg.LastParam(), i.client.Host) channelStore.RenameUser(msg.Nick, msg.LastParam(), i.client.Host)
@ -141,7 +140,6 @@ func (i *ircHandler) quit(msg *irc.Message) {
Server: i.client.Host, Server: i.client.Host,
User: msg.Nick, User: msg.Nick,
Reason: msg.LastParam(), Reason: msg.LastParam(),
Channels: channelStore.FindUserChannels(msg.Nick, i.client.Host),
}) })
channelStore.RemoveUserAll(msg.Nick, i.client.Host) channelStore.RemoveUserAll(msg.Nick, i.client.Host)

View File

@ -30,7 +30,6 @@ type Nick struct {
Server string `json:"server"` Server string `json:"server"`
Old string `json:"old"` Old string `json:"old"`
New string `json:"new"` New string `json:"new"`
Channels []string `json:"channels"`
} }
type Join struct { type Join struct {
@ -59,7 +58,6 @@ type Quit struct {
Server string `json:"server"` Server string `json:"server"`
User string `json:"user"` User string `json:"user"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
Channels []string `json:"channels"`
} }
type Chat struct { type Chat struct {

View File

@ -101,25 +101,6 @@ func (c *ChannelStore) SetMode(server, channel, user, add, remove string) {
c.userLock.Unlock() c.userLock.Unlock()
} }
func (c *ChannelStore) FindUserChannels(user, server string) []string {
var channels []string
c.userLock.Lock()
for channel, users := range c.users[server] {
for _, nick := range users {
if strings.TrimLeft(nick, "@+") == user {
channels = append(channels, channel)
break
}
}
}
c.userLock.Unlock()
return channels
}
func (c *ChannelStore) GetTopic(server, channel string) string { func (c *ChannelStore) GetTopic(server, channel string) string {
c.topicLock.Lock() c.topicLock.Lock()
defer c.topicLock.Unlock() defer c.topicLock.Unlock()