sharedb 1.9.1 → 2.2.0
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/CHANGELOG.md +6 -0
- package/lib/agent.js +15 -5
- package/lib/backend.js +19 -1
- package/lib/client/doc.js +17 -1
- package/lib/db/memory.js +1 -4
- package/lib/error.js +1 -0
- package/lib/ot.js +15 -4
- package/lib/util.js +5 -0
- package/package.json +4 -3
- package/test/backend.js +36 -14
- package/test/client/doc.js +66 -0
- package/test/client/presence/presence.js +98 -0
- package/test/client/projections.js +8 -9
- package/test/client/query-subscribe.js +17 -8
- package/test/client/snapshot-version-request.js +19 -9
- package/test/projections.js +1 -4
package/CHANGELOG.md
CHANGED
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.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
|
|
@@ -836,7 +854,7 @@ Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp
|
|
|
836
854
|
|
|
837
855
|
Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, callback) {
|
|
838
856
|
var snapshot = startingSnapshot || new Snapshot(id, 0, null, undefined, null);
|
|
839
|
-
var error = ot.applyOps(snapshot, ops, {
|
|
857
|
+
var error = ot.applyOps(snapshot, ops, {_normalizeLegacyJson0Ops: true});
|
|
840
858
|
callback(error, snapshot);
|
|
841
859
|
};
|
|
842
860
|
|
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;
|
|
@@ -919,6 +920,15 @@ Doc.prototype.resume = function() {
|
|
|
919
920
|
this.flush();
|
|
920
921
|
};
|
|
921
922
|
|
|
923
|
+
// Create a snapshot that can be serialized, deserialized, and passed into `Doc.ingestSnapshot`.
|
|
924
|
+
Doc.prototype.toSnapshot = function() {
|
|
925
|
+
return {
|
|
926
|
+
v: this.version,
|
|
927
|
+
data: clone(this.data),
|
|
928
|
+
type: this.type.uri
|
|
929
|
+
};
|
|
930
|
+
};
|
|
931
|
+
|
|
922
932
|
// *** Receiving operations
|
|
923
933
|
|
|
924
934
|
// This is called when the server acknowledges an operation from the client.
|
|
@@ -948,7 +958,13 @@ Doc.prototype._rollback = function(err) {
|
|
|
948
958
|
var op = this.inflightOp;
|
|
949
959
|
|
|
950
960
|
if ('op' in op && op.type.invert) {
|
|
951
|
-
|
|
961
|
+
try {
|
|
962
|
+
op.op = op.type.invert(op.op);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
// If the op doesn't support `.invert()`, we just reload the doc
|
|
965
|
+
// instead of trying to locally revert it.
|
|
966
|
+
return this._hardRollback(err);
|
|
967
|
+
}
|
|
952
968
|
|
|
953
969
|
// Transform the undo operation by any pending ops.
|
|
954
970
|
for (var i = 0; i < this.pendingOps.length; i++) {
|
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/error.js
CHANGED
|
@@ -38,6 +38,7 @@ ShareDBError.CODES = {
|
|
|
38
38
|
ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM: 'ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM',
|
|
39
39
|
ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM: 'ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM',
|
|
40
40
|
ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT: 'ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT',
|
|
41
|
+
ERR_OT_LEGACY_JSON0_OP_CANNOT_BE_NORMALIZED: 'ERR_OT_LEGACY_JSON0_OP_CANNOT_BE_NORMALIZED',
|
|
41
42
|
ERR_OT_OP_BADLY_FORMED: 'ERR_OT_OP_BADLY_FORMED',
|
|
42
43
|
ERR_OT_OP_NOT_APPLIED: 'ERR_OT_OP_NOT_APPLIED',
|
|
43
44
|
ERR_OT_OP_NOT_PROVIDED: 'ERR_OT_OP_NOT_PROVIDED',
|
package/lib/ot.js
CHANGED
|
@@ -164,7 +164,16 @@ exports.applyOps = function(snapshot, ops, options) {
|
|
|
164
164
|
options = options || {};
|
|
165
165
|
for (var index = 0; index < ops.length; index++) {
|
|
166
166
|
var op = ops[index];
|
|
167
|
-
if (options.
|
|
167
|
+
if (options._normalizeLegacyJson0Ops) {
|
|
168
|
+
try {
|
|
169
|
+
normalizeLegacyJson0Ops(snapshot, op);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return new ShareDBError(
|
|
172
|
+
ERROR_CODE.ERR_OT_LEGACY_JSON0_OP_CANNOT_BE_NORMALIZED,
|
|
173
|
+
'Cannot normalize legacy json0 op'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
168
177
|
snapshot.v = op.v;
|
|
169
178
|
var error = exports.apply(snapshot, op);
|
|
170
179
|
if (error) return error;
|
|
@@ -205,15 +214,17 @@ exports.transformPresence = function(presence, op, isOwnOp) {
|
|
|
205
214
|
* json0 had a breaking change in https://github.com/ottypes/json0/pull/40
|
|
206
215
|
* The change added stricter type checking, which breaks fetchSnapshot()
|
|
207
216
|
* when trying to rebuild a snapshot from old, committed ops that didn't
|
|
208
|
-
* have this stricter validation. This method fixes up
|
|
217
|
+
* have this stricter validation. This method fixes up legacy ops to
|
|
209
218
|
* pass the stricter validation
|
|
210
219
|
*/
|
|
211
|
-
function
|
|
220
|
+
function normalizeLegacyJson0Ops(snapshot, json0Op) {
|
|
212
221
|
if (snapshot.type !== types.defaultType.uri) return;
|
|
213
222
|
var components = json0Op.op;
|
|
214
223
|
if (!components) return;
|
|
215
224
|
for (var i = 0; i < components.length; i++) {
|
|
216
|
-
var
|
|
225
|
+
var component = components[i];
|
|
226
|
+
if (typeof component.lm === 'string') component.lm = +component.lm;
|
|
227
|
+
var path = component.p;
|
|
217
228
|
var element = snapshot.data;
|
|
218
229
|
for (var j = 0; j < path.length; j++) {
|
|
219
230
|
var key = path[j];
|
package/lib/util.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sharedb",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "JSON OT database backend",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"chai": "^4.2.0",
|
|
15
15
|
"coveralls": "^3.0.7",
|
|
16
|
-
"eslint": "^
|
|
16
|
+
"eslint": "^7.32.0",
|
|
17
17
|
"eslint-config-google": "^0.14.0",
|
|
18
|
-
"ot-json0-v2": "ottypes/json0",
|
|
19
18
|
"lolex": "^5.1.1",
|
|
20
19
|
"mocha": "^8.2.1",
|
|
21
20
|
"nyc": "^14.1.1",
|
|
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
|
});
|
|
@@ -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
|
});
|
|
@@ -220,17 +220,16 @@ module.exports = function(options) {
|
|
|
220
220
|
if (err) return done(err);
|
|
221
221
|
expect(results).to.have.length(0);
|
|
222
222
|
|
|
223
|
+
query.once('changed', function() {
|
|
224
|
+
expect(query.results).to.have.length(1);
|
|
225
|
+
expect(query.results[0].id).to.equal('fido');
|
|
226
|
+
expect(query.results[0].data).to.eql({age: 3, owner: {name: 'jim'}});
|
|
227
|
+
done();
|
|
228
|
+
});
|
|
229
|
+
|
|
223
230
|
// Submit an op on a field not in the projection, where the op should
|
|
224
231
|
// cause the doc to be included in the query results
|
|
225
|
-
fido.submitOp({p: ['color'], od: 'gold', oi: 'black'}, null,
|
|
226
|
-
if (err) return done(err);
|
|
227
|
-
setTimeout(function() {
|
|
228
|
-
expect(query.results).to.have.length(1);
|
|
229
|
-
expect(query.results[0].id).to.equal('fido');
|
|
230
|
-
expect(query.results[0].data).to.eql({age: 3, owner: {name: 'jim'}});
|
|
231
|
-
done();
|
|
232
|
-
}, 10);
|
|
233
|
-
});
|
|
232
|
+
fido.submitOp({p: ['color'], od: 'gold', oi: 'black'}, null, util.errorHandler(done));
|
|
234
233
|
});
|
|
235
234
|
});
|
|
236
235
|
});
|
|
@@ -216,19 +216,28 @@ module.exports = function(options) {
|
|
|
216
216
|
}
|
|
217
217
|
], function(err) {
|
|
218
218
|
if (err) return done(err);
|
|
219
|
-
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery
|
|
220
|
-
|
|
219
|
+
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery);
|
|
220
|
+
query.on('error', done);
|
|
221
|
+
|
|
222
|
+
query.once('ready', function() {
|
|
221
223
|
connection.close();
|
|
224
|
+
|
|
222
225
|
connection2.get('dogs', 'fido').fetch(function(err) {
|
|
223
226
|
if (err) return done(err);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
async.parallel([
|
|
228
|
+
function(cb) {
|
|
229
|
+
connection2.get('dogs', 'fido').del(cb);
|
|
230
|
+
},
|
|
231
|
+
function(cb) {
|
|
232
|
+
connection2.get('dogs', 'taco').create({age: 2}, cb);
|
|
233
|
+
}
|
|
234
|
+
], function(error) {
|
|
235
|
+
if (error) return done(error);
|
|
227
236
|
backend.connect(connection);
|
|
228
237
|
});
|
|
229
238
|
});
|
|
230
239
|
});
|
|
231
|
-
|
|
240
|
+
|
|
232
241
|
var wait = 2;
|
|
233
242
|
function finish() {
|
|
234
243
|
if (--wait) return;
|
|
@@ -237,12 +246,12 @@ module.exports = function(options) {
|
|
|
237
246
|
expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]);
|
|
238
247
|
done();
|
|
239
248
|
}
|
|
240
|
-
query.
|
|
249
|
+
query.once('insert', function(docs) {
|
|
241
250
|
expect(util.pluck(docs, 'id')).eql(['taco']);
|
|
242
251
|
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
|
|
243
252
|
finish();
|
|
244
253
|
});
|
|
245
|
-
query.
|
|
254
|
+
query.once('remove', function(docs) {
|
|
246
255
|
expect(util.pluck(docs, 'id')).eql(['fido']);
|
|
247
256
|
// We don't assert the value of data, because the del op could be
|
|
248
257
|
// applied by the client before or after the query result is removed.
|
|
@@ -441,16 +441,15 @@ describe('SnapshotVersionRequest', function() {
|
|
|
441
441
|
});
|
|
442
442
|
});
|
|
443
443
|
|
|
444
|
-
describe('invalid
|
|
444
|
+
describe('invalid json0v2 path', function() {
|
|
445
445
|
beforeEach(function(done) {
|
|
446
446
|
var doc = backend.connect().get('series', 'his-dark-materials');
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
doc.submitOp({p: ['0', 'title'], od: 'Golden Compass', oi: 'Northern Lights'},
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
});
|
|
447
|
+
async.series([
|
|
448
|
+
doc.create.bind(doc, [{title: 'Golden Compass'}]),
|
|
449
|
+
doc.submitOp.bind(doc, {p: ['0', 'title'], od: 'Golden Compass', oi: 'Northern Lights'}),
|
|
450
|
+
doc.submitOp.bind(doc, {p: ['1'], li: {title: 'Subtle Knife'}}),
|
|
451
|
+
doc.submitOp.bind(doc, {p: ['1'], lm: '0'})
|
|
452
|
+
], done);
|
|
454
453
|
});
|
|
455
454
|
|
|
456
455
|
describe('json0v1', function() {
|
|
@@ -477,13 +476,24 @@ describe('SnapshotVersionRequest', function() {
|
|
|
477
476
|
types.register(defaultType);
|
|
478
477
|
});
|
|
479
478
|
|
|
480
|
-
it('fetches
|
|
479
|
+
it('fetches a string-indexed list insertion with json0v2', function(done) {
|
|
481
480
|
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
|
|
482
481
|
if (error) return done(error);
|
|
483
482
|
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
|
|
484
483
|
done();
|
|
485
484
|
});
|
|
486
485
|
});
|
|
486
|
+
|
|
487
|
+
it('fetches a list move using a string target', function(done) {
|
|
488
|
+
backend.connect().fetchSnapshot('series', 'his-dark-materials', 4, function(error, snapshot) {
|
|
489
|
+
if (error) return done(error);
|
|
490
|
+
expect(snapshot.data).to.eql([
|
|
491
|
+
{title: 'Subtle Knife'},
|
|
492
|
+
{title: 'Northern Lights'}
|
|
493
|
+
]);
|
|
494
|
+
done();
|
|
495
|
+
});
|
|
496
|
+
});
|
|
487
497
|
});
|
|
488
498
|
});
|
|
489
499
|
});
|
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
|
-
}
|