sharedb 3.2.0 → 3.2.2

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.
@@ -10,6 +10,7 @@ var ACTIONS = require('../message-actions').ACTIONS;
10
10
  var types = require('../types');
11
11
  var util = require('../util');
12
12
  var logger = require('../logger');
13
+ var DocPresenceEmitter = require('./presence/doc-presence-emitter');
13
14
 
14
15
  var ERROR_CODE = ShareDBError.CODES;
15
16
 
@@ -51,6 +52,7 @@ function Connection(socket) {
51
52
 
52
53
  // Maps from channel -> presence objects
53
54
  this._presences = {};
55
+ this._docPresenceEmitter = new DocPresenceEmitter();
54
56
 
55
57
  // Map from snapshot request ID -> snapshot request
56
58
  this._snapshotRequests = {};
@@ -521,6 +523,7 @@ Connection.prototype.get = function(collection, id) {
521
523
  this.emit('doc', doc);
522
524
  }
523
525
 
526
+ doc._wantsDestroy = false;
524
527
  return doc;
525
528
  };
526
529
 
@@ -531,7 +534,9 @@ Connection.prototype.get = function(collection, id) {
531
534
  * @private
532
535
  */
533
536
  Connection.prototype._destroyDoc = function(doc) {
537
+ if (!doc._wantsDestroy) return;
534
538
  util.digAndRemove(this.collections, doc.collection, doc.id);
539
+ doc.emit('destroy');
535
540
  };
536
541
 
537
542
  Connection.prototype._addDoc = function(doc) {
@@ -773,17 +778,21 @@ Connection.prototype._initialize = function(message) {
773
778
 
774
779
  Connection.prototype.getPresence = function(channel) {
775
780
  var connection = this;
776
- return util.digOrCreate(this._presences, channel, function() {
781
+ var presence = util.digOrCreate(this._presences, channel, function() {
777
782
  return new Presence(connection, channel);
778
783
  });
784
+ presence._wantsDestroy = false;
785
+ return presence;
779
786
  };
780
787
 
781
788
  Connection.prototype.getDocPresence = function(collection, id) {
782
789
  var channel = DocPresence.channel(collection, id);
783
790
  var connection = this;
784
- return util.digOrCreate(this._presences, channel, function() {
791
+ var presence = util.digOrCreate(this._presences, channel, function() {
785
792
  return new DocPresence(connection, collection, id);
786
793
  });
794
+ presence._wantsDestroy = false;
795
+ return presence;
787
796
  };
788
797
 
789
798
  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
  });
@@ -0,0 +1,75 @@
1
+ var util = require('../../util');
2
+ var EventEmitter = require('events').EventEmitter;
3
+
4
+ var EVENTS = [
5
+ 'create',
6
+ 'del',
7
+ 'destroy',
8
+ 'load',
9
+ 'op'
10
+ ];
11
+
12
+ module.exports = DocPresenceEmitter;
13
+
14
+ function DocPresenceEmitter() {
15
+ this._docs = {};
16
+ this._forwarders = {};
17
+ this._emitters = {};
18
+ }
19
+
20
+ DocPresenceEmitter.prototype.addEventListener = function(doc, event, listener) {
21
+ this._registerDoc(doc);
22
+ var emitter = util.dig(this._emitters, doc.collection, doc.id);
23
+ emitter.on(event, listener);
24
+ };
25
+
26
+ DocPresenceEmitter.prototype.removeEventListener = function(doc, event, listener) {
27
+ var emitter = util.dig(this._emitters, doc.collection, doc.id);
28
+ if (!emitter) return;
29
+ emitter.off(event, listener);
30
+ // We'll always have at least one, because of the destroy listener
31
+ if (emitter._eventsCount === 1) this._unregisterDoc(doc);
32
+ };
33
+
34
+ DocPresenceEmitter.prototype._registerDoc = function(doc) {
35
+ var alreadyRegistered = true;
36
+ util.digOrCreate(this._docs, doc.collection, doc.id, function() {
37
+ alreadyRegistered = false;
38
+ return doc;
39
+ });
40
+
41
+ if (alreadyRegistered) return;
42
+
43
+ var emitter = util.digOrCreate(this._emitters, doc.collection, doc.id, function() {
44
+ var e = new EventEmitter();
45
+ // Set a high limit to avoid unnecessary warnings, but still
46
+ // retain some degree of memory leak detection
47
+ e.setMaxListeners(1000);
48
+ return e;
49
+ });
50
+
51
+ var self = this;
52
+ EVENTS.forEach(function(event) {
53
+ var forwarder = util.digOrCreate(self._forwarders, doc.collection, doc.id, event, function() {
54
+ return emitter.emit.bind(emitter, event);
55
+ });
56
+
57
+ doc.on(event, forwarder);
58
+ });
59
+
60
+ this.addEventListener(doc, 'destroy', this._unregisterDoc.bind(this, doc));
61
+ };
62
+
63
+ DocPresenceEmitter.prototype._unregisterDoc = function(doc) {
64
+ var forwarders = util.dig(this._forwarders, doc.collection, doc.id);
65
+ for (var event in forwarders) {
66
+ doc.off(event, forwarders[event]);
67
+ }
68
+
69
+ var emitter = util.dig(this._emitters, doc.collection, doc.id);
70
+ emitter.removeAllListeners();
71
+
72
+ util.digAndRemove(this._forwarders, doc.collection, doc.id);
73
+ util.digAndRemove(this._emitters, doc.collection, doc.id);
74
+ util.digAndRemove(this._docs, doc.collection, doc.id);
75
+ };
@@ -11,6 +11,7 @@ function LocalDocPresence(presence, presenceId) {
11
11
  this.id = this.presence.id;
12
12
 
13
13
  this._doc = this.connection.get(this.collection, this.id);
14
+ this._emitter = this.connection._docPresenceEmitter;
14
15
  this._isSending = false;
15
16
  this._docDataVersionByPresenceVersion = {};
16
17
 
@@ -43,11 +44,11 @@ LocalDocPresence.prototype.submit = function(value, callback) {
43
44
  };
44
45
 
45
46
  LocalDocPresence.prototype.destroy = function(callback) {
46
- this._doc.removeListener('op', this._opHandler);
47
- this._doc.removeListener('create', this._createOrDelHandler);
48
- this._doc.removeListener('del', this._createOrDelHandler);
49
- this._doc.removeListener('load', this._loadHandler);
50
- this._doc.removeListener('destroy', this._destroyHandler);
47
+ this._emitter.removeEventListener(this._doc, 'op', this._opHandler);
48
+ this._emitter.removeEventListener(this._doc, 'create', this._createOrDelHandler);
49
+ this._emitter.removeEventListener(this._doc, 'del', this._createOrDelHandler);
50
+ this._emitter.removeEventListener(this._doc, 'load', this._loadHandler);
51
+ this._emitter.removeEventListener(this._doc, 'destroy', this._destroyHandler);
51
52
 
52
53
  LocalPresence.prototype.destroy.call(this, callback);
53
54
  };
@@ -72,11 +73,11 @@ LocalDocPresence.prototype._sendPending = function() {
72
73
  };
73
74
 
74
75
  LocalDocPresence.prototype._registerWithDoc = function() {
75
- this._doc.on('op', this._opHandler);
76
- this._doc.on('create', this._createOrDelHandler);
77
- this._doc.on('del', this._createOrDelHandler);
78
- this._doc.on('load', this._loadHandler);
79
- this._doc.on('destroy', this._destroyHandler);
76
+ this._emitter.addEventListener(this._doc, 'op', this._opHandler);
77
+ this._emitter.addEventListener(this._doc, 'create', this._createOrDelHandler);
78
+ this._emitter.addEventListener(this._doc, 'del', this._createOrDelHandler);
79
+ this._emitter.addEventListener(this._doc, 'load', this._loadHandler);
80
+ this._emitter.addEventListener(this._doc, 'destroy', this._destroyHandler);
80
81
  };
81
82
 
82
83
  LocalDocPresence.prototype._transformAgainstOp = function(op, source) {
@@ -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
  );
@@ -11,6 +11,7 @@ function RemoteDocPresence(presence, presenceId) {
11
11
  this.presenceVersion = null;
12
12
 
13
13
  this._doc = this.connection.get(this.collection, this.id);
14
+ this._emitter = this.connection._docPresenceEmitter;
14
15
  this._pending = null;
15
16
  this._opCache = null;
16
17
  this._pendingSetPending = false;
@@ -31,19 +32,19 @@ RemoteDocPresence.prototype.receiveUpdate = function(message) {
31
32
  };
32
33
 
33
34
  RemoteDocPresence.prototype.destroy = function(callback) {
34
- this._doc.removeListener('op', this._opHandler);
35
- this._doc.removeListener('create', this._createDelHandler);
36
- this._doc.removeListener('del', this._createDelHandler);
37
- this._doc.removeListener('load', this._loadHandler);
35
+ this._emitter.removeEventListener(this._doc, 'op', this._opHandler);
36
+ this._emitter.removeEventListener(this._doc, 'create', this._createDelHandler);
37
+ this._emitter.removeEventListener(this._doc, 'del', this._createDelHandler);
38
+ this._emitter.removeEventListener(this._doc, 'load', this._loadHandler);
38
39
 
39
40
  RemotePresence.prototype.destroy.call(this, callback);
40
41
  };
41
42
 
42
43
  RemoteDocPresence.prototype._registerWithDoc = function() {
43
- this._doc.on('op', this._opHandler);
44
- this._doc.on('create', this._createDelHandler);
45
- this._doc.on('del', this._createDelHandler);
46
- this._doc.on('load', this._loadHandler);
44
+ this._emitter.addEventListener(this._doc, 'op', this._opHandler);
45
+ this._emitter.addEventListener(this._doc, 'create', this._createDelHandler);
46
+ this._emitter.addEventListener(this._doc, 'del', this._createDelHandler);
47
+ this._emitter.addEventListener(this._doc, 'load', this._loadHandler);
47
48
  };
48
49
 
49
50
  RemoteDocPresence.prototype._setPendingPresence = function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -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();
@@ -0,0 +1,100 @@
1
+ var Backend = require('../../../lib/backend');
2
+ var errorHandler = require('../../util').errorHandler;
3
+ var expect = require('chai').expect;
4
+
5
+ describe('DocPresenceEmitter', function() {
6
+ var backend;
7
+ var connection;
8
+ var doc;
9
+ var emitter;
10
+
11
+ beforeEach(function(done) {
12
+ backend = new Backend();
13
+ connection = backend.connect();
14
+ doc = connection.get('books', 'northern-lights');
15
+ doc.create({title: 'Northern Lights'}, done);
16
+ emitter = connection._docPresenceEmitter;
17
+ });
18
+
19
+ it('listens to an op event', function(done) {
20
+ emitter.addEventListener(doc, 'op', function(op) {
21
+ expect(op).to.eql([{p: ['author'], oi: 'Philip Pullman'}]);
22
+ done();
23
+ });
24
+
25
+ doc.submitOp([{p: ['author'], oi: 'Philip Pullman'}], errorHandler(done));
26
+ });
27
+
28
+ it('stops listening to events', function(done) {
29
+ var listener = function() {
30
+ done(new Error('should not reach'));
31
+ };
32
+
33
+ emitter.addEventListener(doc, 'op', listener);
34
+ emitter.removeEventListener(doc, 'op', listener);
35
+
36
+ doc.submitOp([{p: ['author'], oi: 'Philip Pullman'}], done);
37
+ });
38
+
39
+ it('removes the listener from the doc if there are no more listeners', function() {
40
+ expect(doc._eventsCount).to.equal(0);
41
+ var listener = function() {};
42
+
43
+ emitter.addEventListener(doc, 'op', listener);
44
+
45
+ expect(doc._eventsCount).to.be.greaterThan(0);
46
+ expect(emitter._docs).not.to.be.empty;
47
+ expect(emitter._emitters).not.to.be.empty;
48
+ expect(emitter._forwarders).not.to.be.empty;
49
+
50
+ emitter.removeEventListener(doc, 'op', listener);
51
+
52
+ expect(doc._eventsCount).to.equal(0);
53
+ expect(emitter._docs).to.be.empty;
54
+ expect(emitter._emitters).to.be.empty;
55
+ expect(emitter._forwarders).to.be.empty;
56
+ });
57
+
58
+ it('only registers a single listener on the doc', function() {
59
+ expect(doc._eventsCount).to.equal(0);
60
+ var listener = function() { };
61
+ emitter.addEventListener(doc, 'op', listener);
62
+ var count = doc._eventsCount;
63
+ emitter.addEventListener(doc, 'op', listener);
64
+ expect(doc._eventsCount).to.equal(count);
65
+ });
66
+
67
+ it('only triggers the given event', function(done) {
68
+ emitter.addEventListener(doc, 'op', function(op) {
69
+ expect(op).to.eql([{p: ['author'], oi: 'Philip Pullman'}]);
70
+ done();
71
+ });
72
+
73
+ emitter.addEventListener(doc, 'del', function() {
74
+ done(new Error('should not reach'));
75
+ });
76
+
77
+ doc.submitOp([{p: ['author'], oi: 'Philip Pullman'}], errorHandler(done));
78
+ });
79
+
80
+ it('removes listeners on destroy', function(done) {
81
+ expect(doc._eventsCount).to.equal(0);
82
+ var listener = function() { };
83
+
84
+ emitter.addEventListener(doc, 'op', listener);
85
+
86
+ expect(doc._eventsCount).to.be.greaterThan(0);
87
+ expect(emitter._docs).not.to.be.empty;
88
+ expect(emitter._emitters).not.to.be.empty;
89
+ expect(emitter._forwarders).not.to.be.empty;
90
+
91
+ doc.destroy(function(error) {
92
+ if (error) return done(error);
93
+ expect(doc._eventsCount).to.equal(0);
94
+ expect(emitter._docs).to.be.empty;
95
+ expect(emitter._emitters).to.be.empty;
96
+ expect(emitter._forwarders).to.be.empty;
97
+ done();
98
+ });
99
+ });
100
+ });
@@ -1025,4 +1025,14 @@ describe('DocPresence', function() {
1025
1025
  }
1026
1026
  ], done);
1027
1027
  });
1028
+
1029
+ it('does not trigger EventEmitter memory leak warnings', function() {
1030
+ for (var i = 0; i < 100; i++) {
1031
+ presence1.create();
1032
+ }
1033
+
1034
+ expect(doc1._events.op.warned).not.to.be.ok;
1035
+ var emitter = connection1._docPresenceEmitter._emitters[doc1.collection][doc1.id];
1036
+ expect(emitter._events.op.warned).not.to.be.ok;
1037
+ });
1028
1038
  });
@@ -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).