Skip to content

Commit b975ab1

Browse files
committed
🐛 Fix Presence.destroy() race condition
There's currently a race condition that can throw uncaught errors when destroying both a `LocalPresence` *and* its parent `Presence` object. If the `LocalPresence` destroys first, then - since the `Presence` [cached][1] its ID before the `Presence` unsubscribe - by the time the `Presence` tries to destroy its children, the `LocalPresence` is already gone, which throws when [trying to call `destroy()`][2]. This change adds a test for this race condition, and adds a fix, which just checks if the `LocalPresence` still exists on the parent before trying to destroy it. [1]: https://github.com/share/sharedb/blob/91cae28a0065faf68749243ef36aadf54bf0645d/lib/client/presence/presence.js#L56 [2]: https://github.com/share/sharedb/blob/91cae28a0065faf68749243ef36aadf54bf0645d/lib/client/presence/presence.js#L64
1 parent 91cae28 commit b975ab1

File tree

2 files changed

+29
-1
lines changed

2 files changed

+29
-1
lines changed

lib/client/presence/presence.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ Presence.prototype.destroy = function(callback) {
6161
[
6262
function(next) {
6363
async.each(localIds, function(presenceId, next) {
64-
presence.localPresences[presenceId].destroy(next);
64+
var localPresence = presence.localPresences[presenceId];
65+
if (!localPresence) return next();
66+
localPresence.destroy(next);
6567
}, next);
6668
},
6769
function(next) {

test/client/presence/presence.js

+26
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,32 @@ describe('Presence', function() {
146146
], errorHandler(done));
147147
});
148148

149+
it('does not throw if LocalPresence destroys before Presence', function(done) {
150+
var localPresence1 = presence1.create('presence-1');
151+
152+
async.series([
153+
presence1.subscribe.bind(presence1),
154+
function(next) {
155+
// Middleware that ensures the presence update replies before the
156+
// unsubscribe, so the local presence is destroyed first
157+
var replies = [];
158+
backend.use('reply', function(message, cb) {
159+
if (!replies) return cb();
160+
if (message.reply.a !== 'p') return replies.push(cb);
161+
var _replies = replies;
162+
replies = null;
163+
cb();
164+
_replies.forEach(function(reply) {
165+
reply();
166+
});
167+
});
168+
169+
presence1.destroy(next);
170+
localPresence1.destroy(errorHandler(done));
171+
}
172+
], done);
173+
});
174+
149175
it('throws if trying to create local presence when wanting destroy', function(done) {
150176
presence2.destroy(errorHandler(done));
151177
expect(function() {

0 commit comments

Comments
 (0)