sharedb 3.2.1 → 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 = {};
@@ -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) {
@@ -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.1",
3
+ "version": "3.2.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -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
  });