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 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++) {
@@ -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
- return util.digOrCreate(this._presences, channel, function() {
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
- return util.digOrCreate(this._presences, channel, function() {
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',
@@ -14,6 +14,7 @@ exports.ACTIONS = {
14
14
  op: 'op',
15
15
  snapshotFetch: 'nf',
16
16
  snapshotFetchByTimestamp: 'nt',
17
+ pingPong: 'pp',
17
18
  presence: 'p',
18
19
  presenceSubscribe: 'ps',
19
20
  presenceUnsubscribe: 'pu',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -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;
@@ -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).