Skip to content

Commit c22c4ff

Browse files
authored
Merge pull request #579 from share/destroying-presence
🐛 Allow getting `Presence` while it's being destroyed
2 parents c9e773d + c3f7030 commit c22c4ff

File tree

3 files changed

+134
-4
lines changed

3 files changed

+134
-4
lines changed

lib/client/connection.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -776,17 +776,21 @@ Connection.prototype._initialize = function(message) {
776776

777777
Connection.prototype.getPresence = function(channel) {
778778
var connection = this;
779-
return util.digOrCreate(this._presences, channel, function() {
779+
var presence = util.digOrCreate(this._presences, channel, function() {
780780
return new Presence(connection, channel);
781781
});
782+
presence._wantsDestroy = false;
783+
return presence;
782784
};
783785

784786
Connection.prototype.getDocPresence = function(collection, id) {
785787
var channel = DocPresence.channel(collection, id);
786788
var connection = this;
787-
return util.digOrCreate(this._presences, channel, function() {
789+
var presence = util.digOrCreate(this._presences, channel, function() {
788790
return new DocPresence(connection, collection, id);
789791
});
792+
presence._wantsDestroy = false;
793+
return presence;
790794
};
791795

792796
Connection.prototype._sendPresenceAction = function(action, seq, presence) {

lib/client/presence/presence.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function Presence(connection, channel) {
2424

2525
this._remotePresenceInstances = {};
2626
this._subscriptionCallbacksBySeq = {};
27+
this._wantsDestroy = false;
2728
}
2829
emitter.mixin(Presence);
2930

@@ -36,17 +37,25 @@ Presence.prototype.unsubscribe = function(callback) {
3637
};
3738

3839
Presence.prototype.create = function(id) {
40+
if (this._wantsDestroy) {
41+
throw new Error('Presence is being destroyed');
42+
}
3943
id = id || hat();
4044
var localPresence = this._createLocalPresence(id);
4145
this.localPresences[id] = localPresence;
4246
return localPresence;
4347
};
4448

4549
Presence.prototype.destroy = function(callback) {
50+
this._wantsDestroy = true;
4651
var presence = this;
52+
// Store these at the time of destruction: any LocalPresence on this
53+
// instance at this time will be destroyed, but if the destroy is
54+
// cancelled, any future LocalPresence objects will be kept.
55+
// See: https://github.com/share/sharedb/pull/579
56+
var localIds = Object.keys(presence.localPresences);
4757
this.unsubscribe(function(error) {
4858
if (error) return presence._callbackOrEmit(error, callback);
49-
var localIds = Object.keys(presence.localPresences);
5059
var remoteIds = Object.keys(presence._remotePresenceInstances);
5160
async.parallel(
5261
[
@@ -56,13 +65,18 @@ Presence.prototype.destroy = function(callback) {
5665
}, next);
5766
},
5867
function(next) {
68+
// We don't bother stashing the RemotePresence instances because
69+
// they're not really bound to our local state: if we want to
70+
// destroy, we destroy them all, but if we cancel the destroy,
71+
// we'll want to keep them all
72+
if (!presence._wantsDestroy) return next();
5973
async.each(remoteIds, function(presenceId, next) {
6074
presence._remotePresenceInstances[presenceId].destroy(next);
6175
}, next);
6276
}
6377
],
6478
function(error) {
65-
delete presence.connection._presences[presence.channel];
79+
if (presence._wantsDestroy) delete presence.connection._presences[presence.channel];
6680
presence._callbackOrEmit(error, callback);
6781
}
6882
);

test/client/presence/presence.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,118 @@ describe('Presence', function() {
9898
], done);
9999
});
100100

101+
it('gets presence during a destroy', function(done) {
102+
var localPresence1 = presence1.create('presence-1');
103+
var presence2a;
104+
105+
async.series([
106+
presence2.subscribe.bind(presence2),
107+
function(next) {
108+
presence2.destroy(errorHandler(done));
109+
next();
110+
},
111+
function(next) {
112+
presence2a = connection2.getPresence('test-channel');
113+
presence2a.subscribe(function(error) {
114+
next(error);
115+
});
116+
},
117+
function(next) {
118+
localPresence1.submit({index: 5}, errorHandler(done));
119+
presence2a.once('receive', function() {
120+
next();
121+
});
122+
}
123+
], done);
124+
});
125+
126+
it('destroys old local presence but keeps new local presence when getting during destroy', function(done) {
127+
presence2.create('presence-2');
128+
var presence2a;
129+
130+
async.series([
131+
presence2.subscribe.bind(presence2),
132+
function(next) {
133+
presence2.destroy(function() {
134+
expect(presence2).to.equal(presence2a);
135+
expect(Object.keys(presence2.localPresences)).to.eql(['presence-2a']);
136+
done();
137+
});
138+
next();
139+
},
140+
function(next) {
141+
presence2a = connection2.getPresence('test-channel');
142+
presence2a.create('presence-2a');
143+
presence2a.subscribe(function(error) {
144+
next(error);
145+
});
146+
}
147+
], errorHandler(done));
148+
});
149+
150+
it('destroys old local presence but keeps new local presence when getting during destroy', function(done) {
151+
presence2.create('presence-2');
152+
var presence2a;
153+
154+
async.series([
155+
presence2.subscribe.bind(presence2),
156+
function(next) {
157+
presence2.destroy(function() {
158+
expect(presence2).to.equal(presence2a);
159+
expect(Object.keys(presence2.localPresences)).to.eql(['presence-2a']);
160+
done();
161+
});
162+
next();
163+
},
164+
function(next) {
165+
presence2a = connection2.getPresence('test-channel');
166+
presence2a.create('presence-2a');
167+
presence2a.subscribe(function(error) {
168+
next(error);
169+
});
170+
}
171+
], errorHandler(done));
172+
});
173+
174+
it('throws if trying to create local presence when wanting destroy', function(done) {
175+
presence2.destroy(errorHandler(done));
176+
expect(function() {
177+
presence2.create('presence-2');
178+
}).to.throw('Presence is being destroyed');
179+
done();
180+
});
181+
182+
it('gets presence after destroy unsubscribe', function(done) {
183+
var localPresence2 = presence2.create('presence-2');
184+
var presence2a;
185+
186+
var flushLocalPresence2Destroy;
187+
sinon.stub(localPresence2, 'destroy').callsFake(function(callback) {
188+
flushLocalPresence2Destroy = callback;
189+
});
190+
191+
async.series([
192+
presence2.subscribe.bind(presence2),
193+
function(next) {
194+
presence2.destroy(function() {
195+
expect(connection2.getPresence('test-channel')).to.equal(presence2a);
196+
done();
197+
});
198+
next();
199+
},
200+
// Wait for the destroy unsubscribe callback to start, where we check
201+
// _wantsDestroy for the first time
202+
presence2.unsubscribe.bind(presence2),
203+
function(next) {
204+
presence2a = connection2.getPresence('test-channel');
205+
presence2a.subscribe(function(error) {
206+
next(error);
207+
});
208+
flushLocalPresence2Destroy();
209+
}
210+
], errorHandler(done));
211+
});
212+
101213
it('requests existing presence from other subscribed clients when subscribing', function(done) {
102214
var localPresence1 = presence1.create('presence-1');
103215
async.series([

0 commit comments

Comments
 (0)