sharedb 2.0.0 → 2.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.
package/README.md CHANGED
@@ -1,5 +1,3 @@
1
- _This README is for `sharedb@1.x`. For `sharedb@1.x-beta`, see [the 1.x-beta branch](https://github.com/share/sharedb/tree/1.x-beta). To upgrade, see [the upgrade guide](https://github.com/share/sharedb/wiki/Upgrading-to-sharedb@1.0.0-from-1.0.0-beta)._
2
-
3
1
  # ShareDB
4
2
 
5
3
  [![NPM Version](https://img.shields.io/npm/v/sharedb.svg)](https://npmjs.org/package/sharedb)
package/lib/agent.js CHANGED
@@ -736,17 +736,25 @@ Agent.prototype._src = function() {
736
736
 
737
737
  Agent.prototype._broadcastPresence = function(presence, callback) {
738
738
  var agent = this;
739
+ var backend = this.backend;
739
740
  var requests = this.presenceRequests[presence.ch] || (this.presenceRequests[presence.ch] = {});
740
741
  var previousRequest = requests[presence.id];
741
742
  if (!previousRequest || previousRequest.pv < presence.pv) {
742
743
  this.presenceRequests[presence.ch][presence.id] = presence;
743
744
  }
744
- this.backend.transformPresenceToLatestVersion(this, presence, function(error, presence) {
745
+ var context = {
746
+ presence: presence,
747
+ collection: presence.c
748
+ };
749
+ backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
745
750
  if (error) return callback(error);
746
- var channel = agent._getPresenceChannel(presence.ch);
747
- agent.backend.pubsub.publish([channel], presence, function(error) {
751
+ backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
748
752
  if (error) return callback(error);
749
- callback(null, presence);
753
+ var channel = agent._getPresenceChannel(presence.ch);
754
+ agent.backend.pubsub.publish([channel], presence, function(error) {
755
+ if (error) return callback(error);
756
+ callback(null, presence);
757
+ });
750
758
  });
751
759
  });
752
760
  };
@@ -815,7 +823,9 @@ Agent.prototype._handlePresenceData = function(presence) {
815
823
  var agent = this;
816
824
  backend.trigger(backend.MIDDLEWARE_ACTIONS.sendPresence, this, context, function(error) {
817
825
  if (error) {
818
- return agent.send({a: 'p', ch: presence.ch, id: presence.id, error: getReplyErrorObject(error)});
826
+ if (backend.doNotForwardSendPresenceErrorsToClient) backend.errorHandler(error, {agent: agent});
827
+ else agent.send({a: 'p', ch: presence.ch, id: presence.id, error: getReplyErrorObject(error)});
828
+ return;
819
829
  }
820
830
  agent.send(presence);
821
831
  });
package/lib/backend.js CHANGED
@@ -14,6 +14,7 @@ var StreamSocket = require('./stream-socket');
14
14
  var SubmitRequest = require('./submit-request');
15
15
  var ReadSnapshotsRequest = require('./read-snapshots-request');
16
16
  var util = require('./util');
17
+ var logger = require('./logger');
17
18
 
18
19
  var ERROR_CODE = ShareDBError.CODES;
19
20
 
@@ -34,6 +35,14 @@ function Backend(options) {
34
35
  this.suppressPublish = !!options.suppressPublish;
35
36
  this.maxSubmitRetries = options.maxSubmitRetries || null;
36
37
  this.presenceEnabled = !!options.presence;
38
+ this.doNotForwardSendPresenceErrorsToClient = !!options.doNotForwardSendPresenceErrorsToClient;
39
+ if (this.presenceEnabled && !this.doNotForwardSendPresenceErrorsToClient) {
40
+ logger.warn(
41
+ 'Broadcasting "sendPresence" middleware errors to clients is deprecated ' +
42
+ 'and will be removed in a future release. Disable this behaviour with:\n\n' +
43
+ 'new Backend({doNotForwardSendPresenceErrorsToClient: true})\n\n'
44
+ );
45
+ }
37
46
 
38
47
  // Map from event name to a list of middleware
39
48
  this.middleware = {};
@@ -41,6 +50,13 @@ function Backend(options) {
41
50
  // The number of open agents for monitoring and testing memory leaks
42
51
  this.agentsCount = 0;
43
52
  this.remoteAgentsCount = 0;
53
+
54
+ this.errorHandler = typeof options.errorHandler === 'function' ?
55
+ options.errorHandler :
56
+ // eslint-disable-next-line no-unused-vars
57
+ function(error, context) {
58
+ logger.error(error);
59
+ };
44
60
  }
45
61
  module.exports = Backend;
46
62
  emitter.mixin(Backend);
@@ -68,6 +84,8 @@ Backend.prototype.MIDDLEWARE_ACTIONS = {
68
84
  // by design, changing existing reply properties can cause weird bugs, since
69
85
  // the rest of ShareDB would be unaware of those changes.
70
86
  reply: 'reply',
87
+ // The server received presence information
88
+ receivePresence: 'receivePresence',
71
89
  // About to send presence information to a client
72
90
  sendPresence: 'sendPresence',
73
91
  // An operation is about to be submitted to the database
package/lib/client/doc.js CHANGED
@@ -3,6 +3,7 @@ var logger = require('../logger');
3
3
  var ShareDBError = require('../error');
4
4
  var types = require('../types');
5
5
  var util = require('../util');
6
+ var clone = util.clone;
6
7
  var deepEqual = require('fast-deep-equal');
7
8
 
8
9
  var ERROR_CODE = ShareDBError.CODES;
@@ -113,6 +114,10 @@ function Doc(connection, collection, id) {
113
114
  // ops are still received. Should be toggled through the pause() and
114
115
  // resume() methods to correctly flush on resume.
115
116
  this.paused = false;
117
+
118
+ // Internal counter that gets incremented every time doc.data is updated.
119
+ // Used as a cheap way to check if doc.data has changed.
120
+ this._dataStateVersion = 0;
116
121
  }
117
122
  emitter.mixin(Doc);
118
123
 
@@ -155,13 +160,18 @@ Doc.prototype._setType = function(newType) {
155
160
  } else if (newType === null) {
156
161
  this.type = newType;
157
162
  // If we removed the type from the object, also remove its data
158
- this.data = undefined;
163
+ this._setData(undefined);
159
164
  } else {
160
165
  var err = new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Missing type ' + newType);
161
166
  return this.emit('error', err);
162
167
  }
163
168
  };
164
169
 
170
+ Doc.prototype._setData = function(data) {
171
+ this.data = data;
172
+ this._dataStateVersion++;
173
+ };
174
+
165
175
  // Ingest snapshot data. This data must include a version, snapshot and type.
166
176
  // This is used both to ingest data that was exported with a webpage and data
167
177
  // that was received from the server during a fetch.
@@ -217,9 +227,11 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) {
217
227
  this.version = snapshot.v;
218
228
  var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type;
219
229
  this._setType(type);
220
- this.data = (this.type && this.type.deserialize) ?
221
- this.type.deserialize(snapshot.data) :
222
- snapshot.data;
230
+ this._setData(
231
+ (this.type && this.type.deserialize) ?
232
+ this.type.deserialize(snapshot.data) :
233
+ snapshot.data
234
+ );
223
235
  this.emit('load');
224
236
  callback && callback();
225
237
  };
@@ -621,7 +633,7 @@ Doc.prototype._otApply = function(op, source) {
621
633
  }
622
634
  // Apply the individual op component
623
635
  this.emit('before op', componentOp.op, source, op.src);
624
- this.data = this.type.apply(this.data, componentOp.op);
636
+ this._setData(this.type.apply(this.data, componentOp.op));
625
637
  this.emit('op', componentOp.op, source, op.src);
626
638
  }
627
639
  this.emit('op batch', op.op, source);
@@ -634,7 +646,7 @@ Doc.prototype._otApply = function(op, source) {
634
646
  // the snapshot before it gets changed
635
647
  this.emit('before op', op.op, source, op.src);
636
648
  // Apply the operation to the local data, mutating it in place
637
- this.data = this.type.apply(this.data, op.op);
649
+ this._setData(this.type.apply(this.data, op.op));
638
650
  // Emit an 'op' event once the local data includes the changes from the
639
651
  // op. For locally submitted ops, this will be synchronously with
640
652
  // submission and before the server or other clients have received the op.
@@ -647,11 +659,15 @@ Doc.prototype._otApply = function(op, source) {
647
659
 
648
660
  if (op.create) {
649
661
  this._setType(op.create.type);
650
- this.data = (this.type.deserialize) ?
651
- (this.type.createDeserialized) ?
652
- this.type.createDeserialized(op.create.data) :
653
- this.type.deserialize(this.type.create(op.create.data)) :
654
- this.type.create(op.create.data);
662
+ if (this.type.deserialize) {
663
+ if (this.type.createDeserialized) {
664
+ this._setData(this.type.createDeserialized(op.create.data));
665
+ } else {
666
+ this._setData(this.type.deserialize(this.type.create(op.create.data)));
667
+ }
668
+ } else {
669
+ this._setData(this.type.create(op.create.data));
670
+ }
655
671
  this.emit('create', source);
656
672
  return;
657
673
  }
@@ -919,6 +935,15 @@ Doc.prototype.resume = function() {
919
935
  this.flush();
920
936
  };
921
937
 
938
+ // Create a snapshot that can be serialized, deserialized, and passed into `Doc.ingestSnapshot`.
939
+ Doc.prototype.toSnapshot = function() {
940
+ return {
941
+ v: this.version,
942
+ data: clone(this.data),
943
+ type: this.type.uri
944
+ };
945
+ };
946
+
922
947
  // *** Receiving operations
923
948
 
924
949
  // This is called when the server acknowledges an operation from the client.
@@ -948,7 +973,13 @@ Doc.prototype._rollback = function(err) {
948
973
  var op = this.inflightOp;
949
974
 
950
975
  if ('op' in op && op.type.invert) {
951
- op.op = op.type.invert(op.op);
976
+ try {
977
+ op.op = op.type.invert(op.op);
978
+ } catch (error) {
979
+ // If the op doesn't support `.invert()`, we just reload the doc
980
+ // instead of trying to locally revert it.
981
+ return this._hardRollback(err);
982
+ }
952
983
 
953
984
  // Transform the undo operation by any pending ops.
954
985
  for (var i = 0; i < this.pendingOps.length; i++) {
@@ -12,6 +12,7 @@ function LocalDocPresence(presence, presenceId) {
12
12
 
13
13
  this._doc = this.connection.get(this.collection, this.id);
14
14
  this._isSending = false;
15
+ this._docDataVersionByPresenceVersion = {};
15
16
 
16
17
  this._opHandler = this._transformAgainstOp.bind(this);
17
18
  this._createOrDelHandler = this._handleCreateOrDel.bind(this);
@@ -35,6 +36,9 @@ LocalDocPresence.prototype.submit = function(value, callback) {
35
36
  return this._callbackOrEmit(error, callback);
36
37
  };
37
38
 
39
+ // Record the current data state version to check if we need to transform
40
+ // the presence later
41
+ this._docDataVersionByPresenceVersion[this.presenceVersion] = this._doc._dataStateVersion;
38
42
  LocalPresence.prototype.submit.call(this, value, callback);
39
43
  };
40
44
 
@@ -63,6 +67,7 @@ LocalDocPresence.prototype._sendPending = function() {
63
67
  });
64
68
 
65
69
  presence._pendingMessages = [];
70
+ presence._docDataVersionByPresenceVersion = {};
66
71
  });
67
72
  };
68
73
 
@@ -76,9 +81,18 @@ LocalDocPresence.prototype._registerWithDoc = function() {
76
81
 
77
82
  LocalDocPresence.prototype._transformAgainstOp = function(op, source) {
78
83
  var presence = this;
84
+ var docDataVersion = this._doc._dataStateVersion;
85
+
79
86
  this._pendingMessages.forEach(function(message) {
87
+ // Check if the presence needs transforming against the op - this is to check against
88
+ // edge cases where presence is submitted from an 'op' event
89
+ var messageDocDataVersion = presence._docDataVersionByPresenceVersion[message.pv];
90
+ if (messageDocDataVersion >= docDataVersion) return;
80
91
  try {
81
92
  message.p = presence._transformPresence(message.p, op, source);
93
+ // Ensure the presence's data version is kept consistent to deal with "deep" op
94
+ // submissions
95
+ presence._docDataVersionByPresenceVersion[message.pv] = docDataVersion;
82
96
  } catch (error) {
83
97
  var callback = presence._getCallback(message.pv);
84
98
  presence._callbackOrEmit(error, callback);
@@ -103,6 +117,7 @@ LocalDocPresence.prototype._handleCreateOrDel = function() {
103
117
  LocalDocPresence.prototype._handleLoad = function() {
104
118
  this.value = null;
105
119
  this._pendingMessages = [];
120
+ this._docDataVersionByPresenceVersion = {};
106
121
  };
107
122
 
108
123
  LocalDocPresence.prototype._message = function() {
package/lib/db/memory.js CHANGED
@@ -1,6 +1,7 @@
1
1
  var DB = require('./index');
2
2
  var Snapshot = require('../snapshot');
3
3
  var util = require('../util');
4
+ var clone = util.clone;
4
5
 
5
6
  // In-memory ShareDB database
6
7
  //
@@ -172,7 +173,3 @@ MemoryDB.prototype._getVersionSync = function(collection, id) {
172
173
  var collectionOps = this.ops[collection];
173
174
  return (collectionOps && collectionOps[id] && collectionOps[id].length) || 0;
174
175
  };
175
-
176
- function clone(obj) {
177
- return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
178
- }
package/lib/util.js CHANGED
@@ -96,3 +96,8 @@ exports.nextTick = function(callback) {
96
96
  callback.apply(null, args);
97
97
  });
98
98
  };
99
+
100
+ exports.clone = function(obj) {
101
+ return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
102
+ };
103
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "2.0.0",
3
+ "version": "2.2.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -20,6 +20,7 @@
20
20
  "nyc": "^14.1.1",
21
21
  "ot-json0-v2": "ottypes/json0",
22
22
  "ot-json1": "^0.3.0",
23
+ "rich-text": "^4.1.0",
23
24
  "sharedb-legacy": "npm:sharedb@=1.1.0",
24
25
  "sinon": "^7.5.0"
25
26
  },
package/test/backend.js CHANGED
@@ -1,29 +1,51 @@
1
1
  var Backend = require('../lib/backend');
2
2
  var expect = require('chai').expect;
3
3
  var sinon = require('sinon');
4
+ var logger = require('../lib/logger');
4
5
 
5
6
  describe('Backend', function() {
6
7
  var backend;
7
- var agent = {
8
- custom: {
9
- foo: 'bar'
10
- }
11
- };
12
- var fetchOptions = {
13
- snapshotOptions: {
14
- fizz: 'buzz'
15
- }
16
- };
17
-
18
- beforeEach(function() {
19
- backend = new Backend();
20
- });
21
8
 
22
9
  afterEach(function(done) {
23
10
  backend.close(done);
24
11
  });
25
12
 
13
+ describe('options', function() {
14
+ describe('errorHandler', function() {
15
+ it('logs by default', function() {
16
+ backend = new Backend();
17
+ var error = new Error('foo');
18
+ sinon.spy(logger, 'error');
19
+ backend.errorHandler(error);
20
+ expect(logger.error.callCount).to.equal(1);
21
+ });
22
+
23
+ it('overrides with another function', function() {
24
+ var handler = sinon.spy();
25
+ backend = new Backend({errorHandler: handler});
26
+ var error = new Error('foo');
27
+ backend.errorHandler(error);
28
+ expect(handler.callCount).to.equal(1);
29
+ });
30
+ });
31
+ });
32
+
26
33
  describe('a simple document', function() {
34
+ var agent = {
35
+ custom: {
36
+ foo: 'bar'
37
+ }
38
+ };
39
+ var fetchOptions = {
40
+ snapshotOptions: {
41
+ fizz: 'buzz'
42
+ }
43
+ };
44
+
45
+ beforeEach(function() {
46
+ backend = new Backend();
47
+ });
48
+
27
49
  beforeEach(function(done) {
28
50
  var doc = backend.connect().get('books', '1984');
29
51
  doc.create({title: '1984'}, function(error) {
@@ -1,6 +1,9 @@
1
1
  var Backend = require('../../lib/backend');
2
2
  var expect = require('chai').expect;
3
3
  var async = require('async');
4
+ var json0 = require('ot-json0').type;
5
+ var richText = require('rich-text').type;
6
+ var ShareDBError = require('../../lib/error');
4
7
  var errorHandler = require('../util').errorHandler;
5
8
 
6
9
  describe('Doc', function() {
@@ -407,6 +410,43 @@ describe('Doc', function() {
407
410
  ], done);
408
411
  });
409
412
 
413
+ it('rolls the doc back even if the op is not invertible', function(done) {
414
+ var backend = this.backend;
415
+
416
+ async.series([
417
+ function(next) {
418
+ // Register the rich text type, which can't be inverted
419
+ json0.registerSubtype(richText);
420
+
421
+ var validOp = {p: ['richName'], oi: {ops: [{insert: 'Scooby\n'}]}};
422
+ doc.submitOp(validOp, function(error) {
423
+ expect(error).to.not.exist;
424
+ next();
425
+ });
426
+ },
427
+ function(next) {
428
+ // Make the server reject this insertion
429
+ backend.use('submit', function(_context, backendNext) {
430
+ backendNext(new ShareDBError(ShareDBError.CODES.ERR_UNKNOWN_ERROR, 'Custom unknown error'));
431
+ });
432
+ var nonInvertibleOp = {p: ['richName'], t: 'rich-text', o: [{insert: 'e'}]};
433
+
434
+ // The server error should get all the way back to our handler
435
+ doc.submitOp(nonInvertibleOp, function(error) {
436
+ expect(error.message).to.eql('Custom unknown error');
437
+ next();
438
+ });
439
+ },
440
+ doc.whenNothingPending.bind(doc),
441
+ function(next) {
442
+ // The doc should have been reverted successfully
443
+ expect(doc.data).to.eql({name: 'Scooby', richName: {ops: [{insert: 'Scooby\n'}]}});
444
+ next();
445
+ }
446
+ ], done);
447
+ });
448
+
449
+
410
450
  it('rescues an irreversible op collision', function(done) {
411
451
  // This test case attempts to reconstruct the following corner case, with
412
452
  // two independent references to the same document. We submit two simultaneous, but
@@ -475,4 +515,30 @@ describe('Doc', function() {
475
515
  ], done);
476
516
  });
477
517
  });
518
+
519
+ describe('toSnapshot', function() {
520
+ var doc;
521
+ beforeEach(function(done) {
522
+ doc = this.connection.get('dogs', 'scooby');
523
+ doc.create({name: 'Scooby'}, done);
524
+ });
525
+
526
+ it('generates a snapshot', function() {
527
+ expect(doc.toSnapshot()).to.eql({
528
+ v: 1,
529
+ data: {name: 'Scooby'},
530
+ type: 'http://sharejs.org/types/JSONv0'
531
+ });
532
+ });
533
+
534
+ it('clones snapshot data to guard against mutation', function() {
535
+ var snapshot = doc.toSnapshot();
536
+ doc.data.name = 'Shaggy';
537
+ expect(snapshot).to.eql({
538
+ v: 1,
539
+ data: {name: 'Scooby'},
540
+ type: 'http://sharejs.org/types/JSONv0'
541
+ });
542
+ });
543
+ });
478
544
  });
@@ -979,4 +979,50 @@ describe('DocPresence', function() {
979
979
  }
980
980
  ], done);
981
981
  });
982
+
983
+ it('does not transform presence submitted in an op event when the presence was created late', function(done) {
984
+ var localPresence1;
985
+
986
+ doc1.on('op', function() {
987
+ localPresence1.submit({index: 7});
988
+ });
989
+
990
+ localPresence1 = presence1.create('presence-1');
991
+
992
+ async.series([
993
+ presence2.subscribe.bind(presence2),
994
+ function(next) {
995
+ doc1.submitOp({index: 5, value: 'ern'});
996
+ presence2.on('receive', function(id, value) {
997
+ expect(value).to.eql({index: 7});
998
+ next();
999
+ });
1000
+ }
1001
+ ], done);
1002
+ });
1003
+
1004
+ it('does transform late-created presence submitted in an op event by a deep second op', function(done) {
1005
+ var localPresence1;
1006
+
1007
+ var submitted = false;
1008
+ doc1.on('op', function() {
1009
+ if (submitted) return;
1010
+ submitted = true;
1011
+ localPresence1.submit({index: 7});
1012
+ doc1.submitOp({index: 5, value: 'akg'});
1013
+ });
1014
+
1015
+ localPresence1 = presence1.create('presence-1');
1016
+
1017
+ async.series([
1018
+ presence2.subscribe.bind(presence2),
1019
+ function(next) {
1020
+ doc1.submitOp({index: 5, value: 'ern'});
1021
+ presence2.on('receive', function(id, value) {
1022
+ expect(value).to.eql({index: 10});
1023
+ next();
1024
+ });
1025
+ }
1026
+ ], done);
1027
+ });
982
1028
  });
@@ -487,4 +487,102 @@ describe('Presence', function() {
487
487
  }
488
488
  ], done);
489
489
  });
490
+
491
+ describe('middleware', function() {
492
+ describe('receivePresence', function() {
493
+ it('provides the presence in the middleware', function(done) {
494
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context) {
495
+ expect(context.presence.p).to.eql({index: 5});
496
+ done();
497
+ });
498
+
499
+ var localPresence1 = presence1.create('presence-1');
500
+ localPresence1.submit({index: 5}, errorHandler(done));
501
+ });
502
+
503
+ it('can mutate the presence in the middleware', function(done) {
504
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
505
+ context.presence.p.index++;
506
+ next();
507
+ });
508
+
509
+ var localPresence1 = presence1.create('presence-1');
510
+
511
+ async.series([
512
+ presence2.subscribe.bind(presence2),
513
+ function(next) {
514
+ presence2.on('receive', function(id, value) {
515
+ expect(value).to.eql({index: 6});
516
+ next();
517
+ });
518
+ localPresence1.submit({index: 5}, errorHandler(done));
519
+ }
520
+ ], done);
521
+ });
522
+
523
+ it('can cancel a presence broadcast by erroring', function(done) {
524
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
525
+ next(new Error('bad!'));
526
+ });
527
+
528
+ var localPresence1 = presence1.create('presence-1');
529
+
530
+ async.series([
531
+ presence2.subscribe.bind(presence2),
532
+ function(next) {
533
+ presence2.on('receive', function() {
534
+ done(new Error('should not have received presence'));
535
+ });
536
+ localPresence1.submit({index: 5}, function(error) {
537
+ expect(error.message).to.contain('bad!');
538
+ next();
539
+ });
540
+ }
541
+ ], done);
542
+ });
543
+ });
544
+
545
+ describe('sendPresence', function() {
546
+ // TODO: This functionality is deprecated
547
+ it('sends an error to a subscribed client', function(done) {
548
+ var localPresence1 = presence1.create('presence-1');
549
+
550
+ backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
551
+ next(new Error('uh-oh!'));
552
+ });
553
+
554
+ async.series([
555
+ presence2.subscribe.bind(presence2),
556
+ function(next) {
557
+ localPresence1.submit({index: 3}, errorHandler(done));
558
+ presence2.once('error', function(error) {
559
+ expect(error.message).to.equal('uh-oh!');
560
+ next();
561
+ });
562
+ }
563
+ ], done);
564
+ });
565
+
566
+ it('emits errors on the server instead of sending to the client', function(done) {
567
+ var localPresence1 = presence1.create('presence-1');
568
+
569
+ backend.doNotForwardSendPresenceErrorsToClient = true;
570
+ backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
571
+ next(new Error('uh-oh!'));
572
+ });
573
+
574
+ async.series([
575
+ presence2.subscribe.bind(presence2),
576
+ function(next) {
577
+ localPresence1.submit({index: 3}, errorHandler(done));
578
+ presence2.on('error', errorHandler(done));
579
+ backend.errorHandler = function(error) {
580
+ expect(error.message).to.equal('uh-oh!');
581
+ next();
582
+ };
583
+ }
584
+ ], done);
585
+ });
586
+ });
587
+ });
490
588
  });
@@ -1,6 +1,7 @@
1
1
  var expect = require('chai').expect;
2
2
  var projections = require('../lib/projections');
3
3
  var type = require('../lib/types').defaultType.uri;
4
+ var clone = require('../lib/util').clone;
4
5
 
5
6
  describe('projection utility methods', function() {
6
7
  describe('projectSnapshot', function() {
@@ -385,7 +386,3 @@ describe('projection utility methods', function() {
385
386
  });
386
387
  });
387
388
  });
388
-
389
- function clone(obj) {
390
- return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
391
- }