sharedb 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent.js +10 -0
- package/lib/client/connection.js +29 -2
- package/lib/client/doc.js +3 -2
- package/lib/client/presence/presence.js +16 -2
- package/lib/error.js +1 -0
- package/lib/message-actions.js +1 -0
- package/package.json +1 -1
- package/test/client/connection.js +39 -0
- package/test/client/doc.js +21 -1
- package/test/client/presence/presence.js +112 -0
- package/CHANGELOG.md +0 -44
package/lib/agent.js
CHANGED
|
@@ -447,6 +447,8 @@ Agent.prototype._handleMessage = function(request, callback) {
|
|
|
447
447
|
return this._subscribePresence(request.ch, request.seq, callback);
|
|
448
448
|
case ACTIONS.presenceUnsubscribe:
|
|
449
449
|
return this._unsubscribePresence(request.ch, request.seq, callback);
|
|
450
|
+
case ACTIONS.pingPong:
|
|
451
|
+
return this._pingPong(callback);
|
|
450
452
|
default:
|
|
451
453
|
callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Invalid or unknown message'));
|
|
452
454
|
}
|
|
@@ -540,6 +542,14 @@ Agent.prototype._querySubscribe = function(queryId, collection, query, options,
|
|
|
540
542
|
});
|
|
541
543
|
};
|
|
542
544
|
|
|
545
|
+
Agent.prototype._pingPong = function(callback) {
|
|
546
|
+
var error = null;
|
|
547
|
+
var message = {
|
|
548
|
+
a: ACTIONS.pingPong
|
|
549
|
+
};
|
|
550
|
+
callback(error, message);
|
|
551
|
+
};
|
|
552
|
+
|
|
543
553
|
function getResultsData(results) {
|
|
544
554
|
var items = [];
|
|
545
555
|
for (var i = 0; i < results.length; i++) {
|
package/lib/client/connection.js
CHANGED
|
@@ -253,6 +253,8 @@ Connection.prototype.handleMessage = function(message) {
|
|
|
253
253
|
return this._handlePresenceUnsubscribe(err, message);
|
|
254
254
|
case ACTIONS.presenceRequest:
|
|
255
255
|
return this._handlePresenceRequest(err, message);
|
|
256
|
+
case ACTIONS.pingPong:
|
|
257
|
+
return this._handlePingPong(err);
|
|
256
258
|
|
|
257
259
|
default:
|
|
258
260
|
logger.warn('Ignoring unrecognized message', message);
|
|
@@ -476,6 +478,19 @@ Connection.prototype.send = function(message) {
|
|
|
476
478
|
this.socket.send(JSON.stringify(message));
|
|
477
479
|
};
|
|
478
480
|
|
|
481
|
+
Connection.prototype.ping = function() {
|
|
482
|
+
if (!this.canSend) {
|
|
483
|
+
throw new ShareDBError(
|
|
484
|
+
ERROR_CODE.ERR_CANNOT_PING_OFFLINE,
|
|
485
|
+
'Socket must be CONNECTED to ping'
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
var message = {
|
|
490
|
+
a: ACTIONS.pingPong
|
|
491
|
+
};
|
|
492
|
+
this.send(message);
|
|
493
|
+
};
|
|
479
494
|
|
|
480
495
|
/**
|
|
481
496
|
* Closes the socket and emits 'closed'
|
|
@@ -506,6 +521,7 @@ Connection.prototype.get = function(collection, id) {
|
|
|
506
521
|
this.emit('doc', doc);
|
|
507
522
|
}
|
|
508
523
|
|
|
524
|
+
doc._wantsDestroy = false;
|
|
509
525
|
return doc;
|
|
510
526
|
};
|
|
511
527
|
|
|
@@ -516,7 +532,9 @@ Connection.prototype.get = function(collection, id) {
|
|
|
516
532
|
* @private
|
|
517
533
|
*/
|
|
518
534
|
Connection.prototype._destroyDoc = function(doc) {
|
|
535
|
+
if (!doc._wantsDestroy) return;
|
|
519
536
|
util.digAndRemove(this.collections, doc.collection, doc.id);
|
|
537
|
+
doc.emit('destroy');
|
|
520
538
|
};
|
|
521
539
|
|
|
522
540
|
Connection.prototype._addDoc = function(doc) {
|
|
@@ -725,6 +743,11 @@ Connection.prototype._handleHandshake = function(error, message) {
|
|
|
725
743
|
this._initialize(message);
|
|
726
744
|
};
|
|
727
745
|
|
|
746
|
+
Connection.prototype._handlePingPong = function(error) {
|
|
747
|
+
if (error) return this.emit('error', error);
|
|
748
|
+
this.emit('pong');
|
|
749
|
+
};
|
|
750
|
+
|
|
728
751
|
Connection.prototype._initialize = function(message) {
|
|
729
752
|
if (this.state !== 'connecting') return;
|
|
730
753
|
|
|
@@ -753,17 +776,21 @@ Connection.prototype._initialize = function(message) {
|
|
|
753
776
|
|
|
754
777
|
Connection.prototype.getPresence = function(channel) {
|
|
755
778
|
var connection = this;
|
|
756
|
-
|
|
779
|
+
var presence = util.digOrCreate(this._presences, channel, function() {
|
|
757
780
|
return new Presence(connection, channel);
|
|
758
781
|
});
|
|
782
|
+
presence._wantsDestroy = false;
|
|
783
|
+
return presence;
|
|
759
784
|
};
|
|
760
785
|
|
|
761
786
|
Connection.prototype.getDocPresence = function(collection, id) {
|
|
762
787
|
var channel = DocPresence.channel(collection, id);
|
|
763
788
|
var connection = this;
|
|
764
|
-
|
|
789
|
+
var presence = util.digOrCreate(this._presences, channel, function() {
|
|
765
790
|
return new DocPresence(connection, collection, id);
|
|
766
791
|
});
|
|
792
|
+
presence._wantsDestroy = false;
|
|
793
|
+
return presence;
|
|
767
794
|
};
|
|
768
795
|
|
|
769
796
|
Connection.prototype._sendPresenceAction = function(action, seq, presence) {
|
package/lib/client/doc.js
CHANGED
|
@@ -76,6 +76,8 @@ function Doc(connection, collection, id) {
|
|
|
76
76
|
// Whether to re-establish the subscription on reconnect
|
|
77
77
|
this.wantSubscribe = false;
|
|
78
78
|
|
|
79
|
+
this._wantsDestroy = false;
|
|
80
|
+
|
|
79
81
|
// The op that is currently roundtripping to the server, or null.
|
|
80
82
|
//
|
|
81
83
|
// When the connection reconnects, the inflight op is resubmitted.
|
|
@@ -122,6 +124,7 @@ function Doc(connection, collection, id) {
|
|
|
122
124
|
emitter.mixin(Doc);
|
|
123
125
|
|
|
124
126
|
Doc.prototype.destroy = function(callback) {
|
|
127
|
+
this._wantsDestroy = true;
|
|
125
128
|
var doc = this;
|
|
126
129
|
doc.whenNothingPending(function() {
|
|
127
130
|
if (doc.wantSubscribe) {
|
|
@@ -131,12 +134,10 @@ Doc.prototype.destroy = function(callback) {
|
|
|
131
134
|
return doc.emit('error', err);
|
|
132
135
|
}
|
|
133
136
|
doc.connection._destroyDoc(doc);
|
|
134
|
-
doc.emit('destroy');
|
|
135
137
|
if (callback) callback();
|
|
136
138
|
});
|
|
137
139
|
} else {
|
|
138
140
|
doc.connection._destroyDoc(doc);
|
|
139
|
-
doc.emit('destroy');
|
|
140
141
|
if (callback) callback();
|
|
141
142
|
}
|
|
142
143
|
});
|
|
@@ -24,6 +24,7 @@ function Presence(connection, channel) {
|
|
|
24
24
|
|
|
25
25
|
this._remotePresenceInstances = {};
|
|
26
26
|
this._subscriptionCallbacksBySeq = {};
|
|
27
|
+
this._wantsDestroy = false;
|
|
27
28
|
}
|
|
28
29
|
emitter.mixin(Presence);
|
|
29
30
|
|
|
@@ -36,6 +37,9 @@ Presence.prototype.unsubscribe = function(callback) {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
Presence.prototype.create = function(id) {
|
|
40
|
+
if (this._wantsDestroy) {
|
|
41
|
+
throw new Error('Presence is being destroyed');
|
|
42
|
+
}
|
|
39
43
|
id = id || hat();
|
|
40
44
|
var localPresence = this._createLocalPresence(id);
|
|
41
45
|
this.localPresences[id] = localPresence;
|
|
@@ -43,10 +47,15 @@ Presence.prototype.create = function(id) {
|
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
Presence.prototype.destroy = function(callback) {
|
|
50
|
+
this._wantsDestroy = true;
|
|
46
51
|
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);
|
|
47
57
|
this.unsubscribe(function(error) {
|
|
48
58
|
if (error) return presence._callbackOrEmit(error, callback);
|
|
49
|
-
var localIds = Object.keys(presence.localPresences);
|
|
50
59
|
var remoteIds = Object.keys(presence._remotePresenceInstances);
|
|
51
60
|
async.parallel(
|
|
52
61
|
[
|
|
@@ -56,13 +65,18 @@ Presence.prototype.destroy = function(callback) {
|
|
|
56
65
|
}, next);
|
|
57
66
|
},
|
|
58
67
|
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();
|
|
59
73
|
async.each(remoteIds, function(presenceId, next) {
|
|
60
74
|
presence._remotePresenceInstances[presenceId].destroy(next);
|
|
61
75
|
}, next);
|
|
62
76
|
}
|
|
63
77
|
],
|
|
64
78
|
function(error) {
|
|
65
|
-
delete presence.connection._presences[presence.channel];
|
|
79
|
+
if (presence._wantsDestroy) delete presence.connection._presences[presence.channel];
|
|
66
80
|
presence._callbackOrEmit(error, callback);
|
|
67
81
|
}
|
|
68
82
|
);
|
package/lib/error.js
CHANGED
|
@@ -16,6 +16,7 @@ ShareDBError.CODES = {
|
|
|
16
16
|
ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT: 'ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT',
|
|
17
17
|
ERR_APPLY_SNAPSHOT_NOT_PROVIDED: 'ERR_APPLY_SNAPSHOT_NOT_PROVIDED',
|
|
18
18
|
ERR_CLIENT_ID_BADLY_FORMED: 'ERR_CLIENT_ID_BADLY_FORMED',
|
|
19
|
+
ERR_CANNOT_PING_OFFLINE: 'ERR_CANNOT_PING_OFFLINE',
|
|
19
20
|
ERR_CONNECTION_SEQ_INTEGER_OVERFLOW: 'ERR_CONNECTION_SEQ_INTEGER_OVERFLOW',
|
|
20
21
|
ERR_CONNECTION_STATE_TRANSITION_INVALID: 'ERR_CONNECTION_STATE_TRANSITION_INVALID',
|
|
21
22
|
ERR_DATABASE_ADAPTER_NOT_FOUND: 'ERR_DATABASE_ADAPTER_NOT_FOUND',
|
package/lib/message-actions.js
CHANGED
package/package.json
CHANGED
|
@@ -124,6 +124,45 @@ describe('client connection', function() {
|
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
describe('ping/pong', function() {
|
|
128
|
+
it('pings the backend', function(done) {
|
|
129
|
+
var connection = this.backend.connect();
|
|
130
|
+
|
|
131
|
+
connection.on('pong', function() {
|
|
132
|
+
done();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
connection.on('connected', function() {
|
|
136
|
+
connection.ping();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles errors', function(done) {
|
|
141
|
+
this.backend.use('receive', function(request, next) {
|
|
142
|
+
var error = request.data.a === 'pp' && new Error('bad');
|
|
143
|
+
next(error);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
var connection = this.backend.connect();
|
|
147
|
+
|
|
148
|
+
connection.on('error', function(error) {
|
|
149
|
+
expect(error.message).to.equal('bad');
|
|
150
|
+
done();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
connection.on('connected', function() {
|
|
154
|
+
connection.ping();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('throws if pinging offline', function() {
|
|
159
|
+
var connection = this.backend.connect();
|
|
160
|
+
expect(function() {
|
|
161
|
+
connection.ping();
|
|
162
|
+
}).to.throw('Socket must be CONNECTED to ping');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
127
166
|
describe('backend.agentsCount', function() {
|
|
128
167
|
it('updates after connect and connection.close()', function(done) {
|
|
129
168
|
var backend = this.backend;
|
package/test/client/doc.js
CHANGED
|
@@ -43,11 +43,31 @@ describe('Doc', function() {
|
|
|
43
43
|
if (err) return done(err);
|
|
44
44
|
var doc2 = connection.get('dogs', 'fido');
|
|
45
45
|
expect(doc).not.equal(doc2);
|
|
46
|
-
expect(doc).eql(doc2);
|
|
47
46
|
done();
|
|
48
47
|
});
|
|
49
48
|
});
|
|
50
49
|
|
|
50
|
+
it('destroying then getting synchronously does not destroy the new doc', function(done) {
|
|
51
|
+
var connection = this.connection;
|
|
52
|
+
var doc = connection.get('dogs', 'fido');
|
|
53
|
+
var doc2;
|
|
54
|
+
|
|
55
|
+
doc.create({name: 'fido'}, function(error) {
|
|
56
|
+
if (error) return done(error);
|
|
57
|
+
|
|
58
|
+
doc.destroy(function(error) {
|
|
59
|
+
if (error) return done(error);
|
|
60
|
+
var doc3 = connection.get('dogs', 'fido');
|
|
61
|
+
async.parallel([
|
|
62
|
+
doc2.submitOp.bind(doc2, [{p: ['snacks'], oi: true}]),
|
|
63
|
+
doc3.submitOp.bind(doc3, [{p: ['color'], oi: 'gray'}])
|
|
64
|
+
], done);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
doc2 = connection.get('dogs', 'fido');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
51
71
|
it('doc.destroy() works without a callback', function() {
|
|
52
72
|
var doc = this.connection.get('dogs', 'fido');
|
|
53
73
|
doc.destroy();
|
|
@@ -98,6 +98,118 @@ describe('Presence', function() {
|
|
|
98
98
|
], done);
|
|
99
99
|
});
|
|
100
100
|
|
|
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
|
+
|
|
101
213
|
it('requests existing presence from other subscribed clients when subscribing', function(done) {
|
|
102
214
|
var localPresence1 = presence1.create('presence-1');
|
|
103
215
|
async.series([
|
package/CHANGELOG.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
## v2.0
|
|
2
|
-
|
|
3
|
-
### Breaking changes
|
|
4
|
-
|
|
5
|
-
* Drop Node.js v10 support
|
|
6
|
-
|
|
7
|
-
## v1.0-beta
|
|
8
|
-
|
|
9
|
-
### Breaking changes
|
|
10
|
-
|
|
11
|
-
* Add options argument to all public database adapter methods that read
|
|
12
|
-
or write from snapshots or ops.
|
|
13
|
-
|
|
14
|
-
* DB methods that get snapshots or ops no longer return metadata unless
|
|
15
|
-
`{metadata: true}` option is passed.
|
|
16
|
-
|
|
17
|
-
* Replace `source` argument with `options` in doc methods. Use `options.source`
|
|
18
|
-
instead.
|
|
19
|
-
|
|
20
|
-
* Backend streams now write objects intead of strings.
|
|
21
|
-
|
|
22
|
-
* MemoryDB.prototype._querySync now returns `{snapshots: ..., extra: ...}`
|
|
23
|
-
instead of just an array of snapshots.
|
|
24
|
-
|
|
25
|
-
### Non-breaking changes
|
|
26
|
-
|
|
27
|
-
* Add options argument to backend.submit.
|
|
28
|
-
|
|
29
|
-
* Add error codes to all errors.
|
|
30
|
-
|
|
31
|
-
* Add `'updated'` event on queries which fires on all query result changes.
|
|
32
|
-
|
|
33
|
-
* In clients, wrap errors in Error objects to they get passed through event
|
|
34
|
-
emitters.
|
|
35
|
-
|
|
36
|
-
* Sanitize stack traces when sending errors to client, but log them on the
|
|
37
|
-
server.
|
|
38
|
-
|
|
39
|
-
## v0.11.37
|
|
40
|
-
|
|
41
|
-
Beginning of changelog.
|
|
42
|
-
|
|
43
|
-
If you're upgrading from ShareJS 0.7 or earlier,
|
|
44
|
-
take a look at the [ShareJS upgrade guide](docs/upgrading-from-sharejs.md).
|