sharedb-mongo 1.0.0-beta.6 → 1.0.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/.eslintrc.js +49 -0
- package/.github/workflows/test.yml +58 -0
- package/LICENSE +21 -0
- package/README.md +205 -59
- package/index.js +367 -129
- package/mongodb.js +3 -0
- package/op-link-validator.js +78 -0
- package/package.json +25 -11
- package/src/middleware/actions.js +9 -0
- package/src/middleware/middlewareHandler.js +66 -0
- package/test/mocha.opts +1 -0
- package/test/setup.js +10 -0
- package/test/test_get_ops.js +135 -0
- package/test/test_get_ops_without_strict_linking.js +189 -0
- package/test/test_mongo.js +121 -45
- package/test/test_mongo_middleware.js +469 -0
- package/test/test_op_link_validator.js +159 -0
- package/test/test_skip_poll.js +8 -5
- package/.npmignore +0 -4
- package/.travis.yml +0 -27
package/mongodb.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is a class for determining an op with a unique version number
|
|
3
|
+
* when presented with an **ordered** series of ops.
|
|
4
|
+
*
|
|
5
|
+
* For example, consider the following chain of op versions:
|
|
6
|
+
* 1 -> 1 -> 2 -> 2 -> 3 -> 4
|
|
7
|
+
* If we want to find the first unique version, we must consider a
|
|
8
|
+
* window of three versions. For example, if we consider the first
|
|
9
|
+
* three versions:
|
|
10
|
+
* 1 -> 1 -> 2
|
|
11
|
+
* Then we know that 1 is not unique. We don't know if 2 is unique
|
|
12
|
+
* yet, because we don't know what comes next. Therefore we push
|
|
13
|
+
* one more version and check again:
|
|
14
|
+
* 1 -> 2 -> 2
|
|
15
|
+
* Again we now see that 2 is not unique, so we keep pushing ops
|
|
16
|
+
* until we reach the final window:
|
|
17
|
+
* 2 -> 3 -> 4
|
|
18
|
+
* From here, **assuming the ops are well ordered** we can safely
|
|
19
|
+
* see that v3 is unique. We cannot make the same assumption of
|
|
20
|
+
* v4, because we don't know what comes next.
|
|
21
|
+
*
|
|
22
|
+
* Note that we also assume that the chain starts with **all**
|
|
23
|
+
* of the copies of an op version. That is that if we are provided
|
|
24
|
+
* 1 -> 2
|
|
25
|
+
* Then v1 is unique (because there are no other v1s).
|
|
26
|
+
*
|
|
27
|
+
* Similarly, if a null op is pushed into the class, it is assumed
|
|
28
|
+
* to be the end of the chain, and hence a unique version can be
|
|
29
|
+
* inferred, eg with this chain:
|
|
30
|
+
* 5 -> 6 -> null
|
|
31
|
+
* We say that 6 is unique, because we've reached the end of the
|
|
32
|
+
* list
|
|
33
|
+
*/
|
|
34
|
+
function OpLinkValidator() {
|
|
35
|
+
this.currentOp = undefined;
|
|
36
|
+
this.previousOp = undefined;
|
|
37
|
+
this.oneBeforePreviousOp = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
OpLinkValidator.prototype.push = function(op) {
|
|
41
|
+
this.oneBeforePreviousOp = this.previousOp;
|
|
42
|
+
this.previousOp = this.currentOp;
|
|
43
|
+
this.currentOp = op;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
OpLinkValidator.prototype.opWithUniqueVersion = function() {
|
|
47
|
+
return this._previousVersionWasUnique() ? this.previousOp : null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
OpLinkValidator.prototype.isAtEndOfList = function() {
|
|
51
|
+
// We ascribe a special meaning to a current op of null
|
|
52
|
+
// being that we're at the end of the list, because this
|
|
53
|
+
// is the value that the Mongo cursor will return when
|
|
54
|
+
// the cursor is exhausted
|
|
55
|
+
return this.currentOp === null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
OpLinkValidator.prototype._previousVersionWasUnique = function() {
|
|
59
|
+
var previousVersion = this._previousVersion();
|
|
60
|
+
|
|
61
|
+
return typeof previousVersion === 'number'
|
|
62
|
+
&& previousVersion !== this._currentVersion()
|
|
63
|
+
&& previousVersion !== this._oneBeforePreviousVersion();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
OpLinkValidator.prototype._currentVersion = function() {
|
|
67
|
+
return this.currentOp && this.currentOp.v;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
OpLinkValidator.prototype._previousVersion = function() {
|
|
71
|
+
return this.previousOp && this.previousOp.v;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
OpLinkValidator.prototype._oneBeforePreviousVersion = function() {
|
|
75
|
+
return this.oneBeforePreviousOp && this.oneBeforePreviousOp.v;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
module.exports = OpLinkValidator;
|
package/package.json
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sharedb-mongo",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "MongoDB database adapter for ShareDB",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"async": "^
|
|
8
|
-
"mongodb": "^2.1.2",
|
|
9
|
-
"sharedb": "^1.0.0
|
|
7
|
+
"async": "^2.6.3",
|
|
8
|
+
"mongodb": "^2.1.2 || ^3.0.0 || ^4.0.0",
|
|
9
|
+
"sharedb": "^1.9.1 || ^2.0.0"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
12
|
+
"chai": "^4.2.0",
|
|
13
|
+
"coveralls": "^3.0.7",
|
|
14
|
+
"eslint": "^5.16.0",
|
|
15
|
+
"eslint-config-google": "^0.13.0",
|
|
16
|
+
"mocha": "^6.2.2",
|
|
17
|
+
"mongodb2": "npm:mongodb@^2.1.2",
|
|
18
|
+
"mongodb3": "npm:mongodb@^3.0.0",
|
|
19
|
+
"mongodb4": "npm:mongodb@^4.0.0",
|
|
20
|
+
"nyc": "^14.1.1",
|
|
21
|
+
"ot-json1": "^1.0.1",
|
|
22
|
+
"sharedb-mingo-memory": "^1.1.1",
|
|
23
|
+
"sinon": "^6.1.5",
|
|
24
|
+
"sinon-chai": "^3.7.0"
|
|
17
25
|
},
|
|
18
26
|
"scripts": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
27
|
+
"lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'",
|
|
28
|
+
"lint:fix": "npm run lint -- --fix",
|
|
29
|
+
"test": "mocha",
|
|
30
|
+
"test:mongodb2": "_SHAREDB_MONGODB_DRIVER=mongodb2 npm test",
|
|
31
|
+
"test:mongodb3": "_SHAREDB_MONGODB_DRIVER=mongodb3 npm test",
|
|
32
|
+
"test:mongodb4": "_SHAREDB_MONGODB_DRIVER=mongodb4 npm test",
|
|
33
|
+
"test:all": "npm run test:mongodb2 && npm run test:mongodb3 && npm run test:mongodb4",
|
|
34
|
+
"test-cover": "nyc --temp-dir=coverage -r text -r lcov npm run test:all"
|
|
21
35
|
},
|
|
22
36
|
"repository": "git://github.com/share/sharedb-mongo.git",
|
|
23
37
|
"author": "Nate Smith and Joseph Gentle",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
// Triggers before the call to write a new document is made
|
|
3
|
+
beforeCreate: 'beforeCreate',
|
|
4
|
+
// Triggers before the call to replace a document is made
|
|
5
|
+
beforeOverwrite: 'beforeOverwrite',
|
|
6
|
+
// Triggers directly before the call to issue a query for snapshots
|
|
7
|
+
// Applies for both a single lookup by ID and bulk lookups by a list of IDs
|
|
8
|
+
beforeSnapshotLookup: 'beforeSnapshotLookup'
|
|
9
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
var MIDDLEWARE_ACTIONS = require('./actions');
|
|
2
|
+
|
|
3
|
+
function MiddlewareHandler() {
|
|
4
|
+
this._middleware = {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add middleware to an action or array of actions
|
|
9
|
+
*
|
|
10
|
+
* @param action The action to use from MIDDLEWARE_ACTIONS (e.g. 'beforeOverwrite')
|
|
11
|
+
* @param fn The function to call when this middleware is triggered
|
|
12
|
+
* The fn receives a request object with information on the triggered action (e.g. the snapshot to write)
|
|
13
|
+
* and a next function to call once the middleware is complete
|
|
14
|
+
*
|
|
15
|
+
* NOTE: It is recommended not to add async or long running tasks to the sharedb-mongo middleware as it will
|
|
16
|
+
* be called very frequently during sensitive operations. It may have a significant performance impact.
|
|
17
|
+
*/
|
|
18
|
+
MiddlewareHandler.prototype.use = function(action, fn) {
|
|
19
|
+
if (Array.isArray(action)) {
|
|
20
|
+
for (var i = 0; i < action.length; i++) {
|
|
21
|
+
this.use(action[i], fn);
|
|
22
|
+
}
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
if (!action) throw new Error('Expected action to be defined');
|
|
26
|
+
if (!fn) throw new Error('Expected fn to be defined');
|
|
27
|
+
if (!Object.values(MIDDLEWARE_ACTIONS).includes(action)) {
|
|
28
|
+
throw new Error('Unrecognized action name ' + action);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var fns = this._middleware[action] || (this._middleware[action] = []);
|
|
32
|
+
fns.push(fn);
|
|
33
|
+
return this;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Passes request through the middleware stack
|
|
38
|
+
*
|
|
39
|
+
* Middleware may modify the request object. After all middleware have been
|
|
40
|
+
* invoked we call `callback` with `null` and the modified request. If one of
|
|
41
|
+
* the middleware resturns an error the callback is called with that error.
|
|
42
|
+
*
|
|
43
|
+
* @param action The action to trigger from MIDDLEWARE_ACTIONS (e.g. 'beforeOverwrite')
|
|
44
|
+
* @param request Request details such as the snapshot to write, depends on the triggered action
|
|
45
|
+
* @param callback Function to call once the middleware has been processed.
|
|
46
|
+
*/
|
|
47
|
+
MiddlewareHandler.prototype.trigger = function(action, request, callback) {
|
|
48
|
+
request.action = action;
|
|
49
|
+
|
|
50
|
+
var fns = this._middleware[action];
|
|
51
|
+
if (!fns) return callback();
|
|
52
|
+
|
|
53
|
+
// Copying the triggers we'll fire so they don't get edited while we iterate.
|
|
54
|
+
fns = fns.slice();
|
|
55
|
+
var next = function(err) {
|
|
56
|
+
if (err) return callback(err);
|
|
57
|
+
var fn = fns.shift();
|
|
58
|
+
if (!fn) return callback();
|
|
59
|
+
fn(request, next);
|
|
60
|
+
};
|
|
61
|
+
next();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
MiddlewareHandler.Actions = MIDDLEWARE_ACTIONS;
|
|
65
|
+
|
|
66
|
+
module.exports = MiddlewareHandler;
|
package/test/mocha.opts
CHANGED
package/test/setup.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
var expect = require('chai').expect;
|
|
2
|
+
var ShareDbMongo = require('..');
|
|
3
|
+
var sinon = require('sinon');
|
|
4
|
+
|
|
5
|
+
var mongoUrl = process.env.TEST_MONGO_URL || 'mongodb://localhost:27017/test';
|
|
6
|
+
|
|
7
|
+
function create(options, callback) {
|
|
8
|
+
var opts = Object.assign({
|
|
9
|
+
mongoOptions: {},
|
|
10
|
+
getOpsWithoutStrictLinking: true
|
|
11
|
+
}, options);
|
|
12
|
+
var db = new ShareDbMongo(mongoUrl, opts);
|
|
13
|
+
db.getDbs(function(err, mongo) {
|
|
14
|
+
if (err) return callback(err);
|
|
15
|
+
mongo.dropDatabase(function(err) {
|
|
16
|
+
if (err) return callback(err);
|
|
17
|
+
callback(null, db, mongo);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// loop thru strict linking options
|
|
23
|
+
[true, false].forEach(function(strictLinkingOption) {
|
|
24
|
+
describe('getOps with strict linking ' + strictLinkingOption, function() {
|
|
25
|
+
beforeEach(function(done) {
|
|
26
|
+
var self = this;
|
|
27
|
+
create(
|
|
28
|
+
{getOpsWithoutStrictLinking: strictLinkingOption},
|
|
29
|
+
function(err, db, mongo) {
|
|
30
|
+
if (err) return done(err);
|
|
31
|
+
self.db = db;
|
|
32
|
+
self.mongo = mongo;
|
|
33
|
+
done();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(function(done) {
|
|
38
|
+
this.db.close(done);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('a chain of ops', function() {
|
|
42
|
+
var db;
|
|
43
|
+
var mongo;
|
|
44
|
+
var id;
|
|
45
|
+
var collection;
|
|
46
|
+
|
|
47
|
+
beforeEach(function(done) {
|
|
48
|
+
db = this.db;
|
|
49
|
+
mongo = this.mongo;
|
|
50
|
+
id = 'document1';
|
|
51
|
+
collection = 'testcollection';
|
|
52
|
+
|
|
53
|
+
sinon.spy(db, '_getOps');
|
|
54
|
+
sinon.spy(db, '_getSnapshotOpLink');
|
|
55
|
+
|
|
56
|
+
var ops = [
|
|
57
|
+
{v: 0, create: {}},
|
|
58
|
+
{v: 1, p: ['foo'], oi: 'bar'},
|
|
59
|
+
{v: 2, p: ['foo'], oi: 'baz'},
|
|
60
|
+
{v: 3, p: ['foo'], oi: 'qux'}
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
commitOpChain(db, mongo, collection, id, ops, function(error) {
|
|
64
|
+
if (error) done(error);
|
|
65
|
+
mongo.collection('o_' + collection).deleteOne({v: 1}, done);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('fetches ops 2-3 without fetching all ops', function(done) {
|
|
70
|
+
db.getOps(collection, id, 2, 4, null, function(error, ops) {
|
|
71
|
+
if (error) return done(error);
|
|
72
|
+
expect(ops.length).to.equal(2);
|
|
73
|
+
expect(ops[0].v).to.equal(2);
|
|
74
|
+
expect(ops[1].v).to.equal(3);
|
|
75
|
+
done();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('default option errors when missing ops', function(done) {
|
|
80
|
+
db.getOps(collection, id, 0, 4, null, function(error) {
|
|
81
|
+
expect(error.code).to.equal(5103);
|
|
82
|
+
expect(error.message).to.equal('Missing ops from requested version testcollection.document1 0');
|
|
83
|
+
done();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ignoreMissingOps option returns ops up to the first missing op', function(done) {
|
|
88
|
+
db.getOps(collection, id, 0, 4, {ignoreMissingOps: true}, function(error, ops) {
|
|
89
|
+
if (error) return done(error);
|
|
90
|
+
expect(ops.length).to.equal(2);
|
|
91
|
+
expect(ops[0].v).to.equal(2);
|
|
92
|
+
expect(ops[1].v).to.equal(3);
|
|
93
|
+
done();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('getOpsToSnapshot ignoreMissingOps option returns ops up to the first missing op', function(done) {
|
|
98
|
+
db.getSnapshot(collection, id, {$submit: true}, null, function(error, snapshot) {
|
|
99
|
+
if (error) done(error);
|
|
100
|
+
db.getOpsToSnapshot(collection, id, 0, snapshot, {ignoreMissingOps: true}, function(error, ops) {
|
|
101
|
+
if (error) return done(error);
|
|
102
|
+
expect(ops.length).to.equal(2);
|
|
103
|
+
expect(ops[0].v).to.equal(2);
|
|
104
|
+
expect(ops[1].v).to.equal(3);
|
|
105
|
+
done();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function commitOpChain(db, mongo, collection, id, ops, previousOpId, version, callback) {
|
|
114
|
+
if (typeof previousOpId === 'function') {
|
|
115
|
+
callback = previousOpId;
|
|
116
|
+
previousOpId = undefined;
|
|
117
|
+
version = 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ops = ops.slice();
|
|
121
|
+
var op = ops.shift();
|
|
122
|
+
|
|
123
|
+
if (!op) {
|
|
124
|
+
return callback();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var snapshot = {id: id, v: version + 1, type: 'json0', data: {}, m: null, _opLink: previousOpId};
|
|
128
|
+
db.commit(collection, id, op, snapshot, null, function(error) {
|
|
129
|
+
if (error) return callback(error);
|
|
130
|
+
mongo.collection('o_' + collection).find({d: id, v: version}).next(function(error, op) {
|
|
131
|
+
if (error) return callback(error);
|
|
132
|
+
commitOpChain(db, mongo, collection, id, ops, (op ? op._id : null), ++version, callback);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
var expect = require('chai').expect;
|
|
2
|
+
var ShareDbMongo = require('..');
|
|
3
|
+
var getQuery = require('sharedb-mingo-memory/get-query');
|
|
4
|
+
var sinon = require('sinon');
|
|
5
|
+
|
|
6
|
+
var mongoUrl = process.env.TEST_MONGO_URL || 'mongodb://localhost:27017/test';
|
|
7
|
+
|
|
8
|
+
function create(callback) {
|
|
9
|
+
var db = new ShareDbMongo(mongoUrl, {
|
|
10
|
+
mongoOptions: {},
|
|
11
|
+
getOpsWithoutStrictLinking: true
|
|
12
|
+
});
|
|
13
|
+
db.getDbs(function(err, mongo) {
|
|
14
|
+
if (err) return callback(err);
|
|
15
|
+
mongo.dropDatabase(function(err) {
|
|
16
|
+
if (err) return callback(err);
|
|
17
|
+
callback(null, db, mongo);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
require('sharedb/test/db')({create: create, getQuery: getQuery});
|
|
23
|
+
|
|
24
|
+
describe('getOpsWithoutStrictLinking: true', function() {
|
|
25
|
+
beforeEach(function(done) {
|
|
26
|
+
var self = this;
|
|
27
|
+
create(function(err, db, mongo) {
|
|
28
|
+
if (err) return done(err);
|
|
29
|
+
self.db = db;
|
|
30
|
+
self.mongo = mongo;
|
|
31
|
+
done();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(function(done) {
|
|
36
|
+
this.db.close(done);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('a chain of ops', function() {
|
|
40
|
+
var db;
|
|
41
|
+
var mongo;
|
|
42
|
+
var id;
|
|
43
|
+
var collection;
|
|
44
|
+
|
|
45
|
+
beforeEach(function(done) {
|
|
46
|
+
db = this.db;
|
|
47
|
+
mongo = this.mongo;
|
|
48
|
+
id = 'document1';
|
|
49
|
+
collection = 'testcollection';
|
|
50
|
+
|
|
51
|
+
sinon.spy(db, '_getOps');
|
|
52
|
+
sinon.spy(db, '_getSnapshotOpLink');
|
|
53
|
+
|
|
54
|
+
var ops = [
|
|
55
|
+
{v: 0, create: {}},
|
|
56
|
+
{v: 1, p: ['foo'], oi: 'bar'},
|
|
57
|
+
{v: 2, p: ['foo'], oi: 'baz'},
|
|
58
|
+
{v: 3, p: ['foo'], oi: 'qux'}
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
commitOpChain(db, mongo, collection, id, ops, done);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('fetches ops 0-1 without fetching all ops', function(done) {
|
|
65
|
+
db.getOps(collection, id, 0, 2, null, function(error, ops) {
|
|
66
|
+
if (error) return done(error);
|
|
67
|
+
expect(ops.length).to.equal(2);
|
|
68
|
+
expect(ops[0].v).to.equal(0);
|
|
69
|
+
expect(ops[1].v).to.equal(1);
|
|
70
|
+
expect(db._getSnapshotOpLink.notCalled).to.equal(true);
|
|
71
|
+
expect(db._getOps.calledOnceWith(collection, id, 0, 2)).to.equal(true);
|
|
72
|
+
done();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('fetches ops 0-1 when v1 has a spurious duplicate', function(done) {
|
|
77
|
+
var spuriousOp = {v: 1, d: id, p: ['foo'], oi: 'corrupt', o: null};
|
|
78
|
+
|
|
79
|
+
callInSeries([
|
|
80
|
+
function(next) {
|
|
81
|
+
mongo.collection('o_' + collection).insertOne(spuriousOp, next);
|
|
82
|
+
},
|
|
83
|
+
function(result, next) {
|
|
84
|
+
db.getOps(collection, id, 0, 2, null, next);
|
|
85
|
+
},
|
|
86
|
+
function(ops, next) {
|
|
87
|
+
expect(ops.length).to.equal(2);
|
|
88
|
+
expect(ops[1].oi).to.equal('bar');
|
|
89
|
+
expect(db._getSnapshotOpLink.notCalled).to.equal(true);
|
|
90
|
+
expect(db._getOps.calledOnceWith(collection, id, 0, 2)).to.equal(true);
|
|
91
|
+
next();
|
|
92
|
+
},
|
|
93
|
+
done
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('fetches ops 0-1 when the next op v2 has a spurious duplicate', function(done) {
|
|
98
|
+
var spuriousOp = {v: 2, d: id, p: ['foo'], oi: 'corrupt', o: null};
|
|
99
|
+
|
|
100
|
+
callInSeries([
|
|
101
|
+
function(next) {
|
|
102
|
+
mongo.collection('o_' + collection).insertOne(spuriousOp, next);
|
|
103
|
+
},
|
|
104
|
+
function(result, next) {
|
|
105
|
+
db.getOps(collection, id, 0, 2, null, next);
|
|
106
|
+
},
|
|
107
|
+
function(ops, next) {
|
|
108
|
+
expect(ops.length).to.equal(2);
|
|
109
|
+
expect(ops[1].oi).to.equal('bar');
|
|
110
|
+
expect(db._getSnapshotOpLink.notCalled).to.equal(true);
|
|
111
|
+
expect(db._getOps.calledOnceWith(collection, id, 0, 3)).to.equal(true);
|
|
112
|
+
next();
|
|
113
|
+
},
|
|
114
|
+
done
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('fetches ops 0-1 when all the ops have spurious duplicates', function(done) {
|
|
119
|
+
var spuriousOps = [
|
|
120
|
+
{v: 0, d: id, p: ['foo'], oi: 'corrupt', o: null},
|
|
121
|
+
{v: 1, d: id, p: ['foo'], oi: 'corrupt', o: null},
|
|
122
|
+
{v: 2, d: id, p: ['foo'], oi: 'corrupt', o: null},
|
|
123
|
+
{v: 3, d: id, p: ['foo'], oi: 'corrupt', o: null}
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
callInSeries([
|
|
127
|
+
function(next) {
|
|
128
|
+
mongo.collection('o_' + collection).insertMany(spuriousOps, next);
|
|
129
|
+
},
|
|
130
|
+
function(result, next) {
|
|
131
|
+
db.getOps(collection, id, 0, 2, null, next);
|
|
132
|
+
},
|
|
133
|
+
function(ops, next) {
|
|
134
|
+
expect(ops.length).to.equal(2);
|
|
135
|
+
expect(ops[0].create).to.eql({});
|
|
136
|
+
expect(ops[1].oi).to.equal('bar');
|
|
137
|
+
expect(db._getSnapshotOpLink.calledOnce).to.equal(true);
|
|
138
|
+
next();
|
|
139
|
+
},
|
|
140
|
+
done
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
function commitOpChain(db, mongo, collection, id, ops, previousOpId, version, callback) {
|
|
147
|
+
if (typeof previousOpId === 'function') {
|
|
148
|
+
callback = previousOpId;
|
|
149
|
+
previousOpId = undefined;
|
|
150
|
+
version = 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ops = ops.slice();
|
|
154
|
+
var op = ops.shift();
|
|
155
|
+
|
|
156
|
+
if (!op) {
|
|
157
|
+
return callback();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var snapshot = {id: id, v: version + 1, type: 'json0', data: {}, m: null, _opLink: previousOpId};
|
|
161
|
+
db.commit(collection, id, op, snapshot, null, function(error) {
|
|
162
|
+
if (error) return callback(error);
|
|
163
|
+
mongo.collection('o_' + collection).find({d: id, v: version}).next(function(error, op) {
|
|
164
|
+
if (error) return callback(error);
|
|
165
|
+
commitOpChain(db, mongo, collection, id, ops, op._id, ++version, callback);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function callInSeries(callbacks, args) {
|
|
171
|
+
if (!callbacks.length) return;
|
|
172
|
+
args = args || [];
|
|
173
|
+
var error = args.shift();
|
|
174
|
+
|
|
175
|
+
if (error) {
|
|
176
|
+
var finalCallback = callbacks[callbacks.length - 1];
|
|
177
|
+
return finalCallback(error);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var callback = callbacks.shift();
|
|
181
|
+
if (callbacks.length) {
|
|
182
|
+
args.push(function() {
|
|
183
|
+
var args = Array.from(arguments);
|
|
184
|
+
callInSeries(callbacks, args);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
callback.apply(callback, args);
|
|
189
|
+
}
|