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 +0 -2
- package/lib/agent.js +15 -5
- package/lib/backend.js +18 -0
- package/lib/client/doc.js +43 -12
- package/lib/client/presence/local-doc-presence.js +15 -0
- package/lib/db/memory.js +1 -4
- package/lib/util.js +5 -0
- package/package.json +2 -1
- package/test/backend.js +36 -14
- package/test/client/doc.js +66 -0
- package/test/client/presence/doc-presence.js +46 -0
- package/test/client/presence/presence.js +98 -0
- package/test/projections.js +1 -4
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
|
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
221
|
-
this.type.deserialize
|
|
222
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
651
|
-
(this.type.createDeserialized)
|
|
652
|
-
this.type.createDeserialized(op.create.data)
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sharedb",
|
|
3
|
-
"version": "2.
|
|
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) {
|
package/test/client/doc.js
CHANGED
|
@@ -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
|
});
|
package/test/projections.js
CHANGED
|
@@ -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
|
-
}
|