sharedb 4.1.5 → 5.0.1
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/lib/db/memory.js +17 -3
- package/lib/submit-request.js +24 -1
- package/package.json +1 -1
- package/test/client/snapshot-version-request.js +12 -1
- package/test/client/submit.js +113 -32
- package/test/db-memory.js +80 -12
package/lib/db/memory.js
CHANGED
|
@@ -84,10 +84,12 @@ MemoryDB.prototype.getOps = function(collection, id, from, to, options, callback
|
|
|
84
84
|
if (typeof callback !== 'function') throw new Error('Callback required');
|
|
85
85
|
util.nextTick(function() {
|
|
86
86
|
var opLog = db._getOpLogSync(collection, id);
|
|
87
|
-
if (
|
|
88
|
-
|
|
87
|
+
if (!from) from = 0;
|
|
88
|
+
if (to == null) to = opLog.length;
|
|
89
|
+
var ops = clone(opLog.slice(from, to).filter(Boolean));
|
|
90
|
+
if (ops.length < to - from) {
|
|
91
|
+
return callback(new Error('Missing ops'));
|
|
89
92
|
}
|
|
90
|
-
var ops = clone(opLog.slice(from, to));
|
|
91
93
|
if (!includeMetadata) {
|
|
92
94
|
for (var i = 0; i < ops.length; i++) {
|
|
93
95
|
delete ops[i].m;
|
|
@@ -97,6 +99,18 @@ MemoryDB.prototype.getOps = function(collection, id, from, to, options, callback
|
|
|
97
99
|
});
|
|
98
100
|
};
|
|
99
101
|
|
|
102
|
+
MemoryDB.prototype.deleteOps = function(collection, id, from, to, options, callback) {
|
|
103
|
+
if (typeof callback !== 'function') throw new Error('Callback required');
|
|
104
|
+
var db = this;
|
|
105
|
+
util.nextTick(function() {
|
|
106
|
+
var opLog = db._getOpLogSync(collection, id);
|
|
107
|
+
if (!from) from = 0;
|
|
108
|
+
if (to == null) to = opLog.length;
|
|
109
|
+
for (var i = from; i < to; i++) opLog[i] = null;
|
|
110
|
+
callback(null);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
100
114
|
// The memory database query function returns all documents in a collection
|
|
101
115
|
// regardless of query by default
|
|
102
116
|
MemoryDB.prototype.query = function(collection, query, fields, options, callback) {
|
package/lib/submit-request.js
CHANGED
|
@@ -109,7 +109,7 @@ SubmitRequest.prototype.submit = function(callback) {
|
|
|
109
109
|
// case, we should return a non-fatal 'Op already submitted' error. We
|
|
110
110
|
// must get the past ops and check their src and seq values to
|
|
111
111
|
// differentiate.
|
|
112
|
-
|
|
112
|
+
request._fetchCreateOpVersion(function(error, version) {
|
|
113
113
|
if (err) return callback(err);
|
|
114
114
|
if (version == null) {
|
|
115
115
|
callback(request.alreadyCreatedError());
|
|
@@ -194,6 +194,15 @@ SubmitRequest.prototype.commit = function(callback) {
|
|
|
194
194
|
backend.trigger(backend.MIDDLEWARE_ACTIONS.commit, this.agent, this, function(err) {
|
|
195
195
|
if (err) return callback(err);
|
|
196
196
|
if (request._fixupOps.length) request.op.m.fixup = request._fixupOps;
|
|
197
|
+
if (request.op.create) {
|
|
198
|
+
// When we create the snapshot, we store a pointer to the op that created
|
|
199
|
+
// it. This allows us to return OP_ALREADY_SUBMITTED errors when appropriate.
|
|
200
|
+
request.snapshot.m._create = {
|
|
201
|
+
src: request.op.src,
|
|
202
|
+
seq: request.op.seq,
|
|
203
|
+
v: request.op.v
|
|
204
|
+
};
|
|
205
|
+
}
|
|
197
206
|
|
|
198
207
|
// Try committing the operation and snapshot to the database atomically
|
|
199
208
|
backend.db.commit(
|
|
@@ -290,6 +299,20 @@ SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) {
|
|
|
290
299
|
return this.saveMilestoneSnapshot;
|
|
291
300
|
};
|
|
292
301
|
|
|
302
|
+
SubmitRequest.prototype._fetchCreateOpVersion = function(callback) {
|
|
303
|
+
var create = this.snapshot.m._create;
|
|
304
|
+
if (create) {
|
|
305
|
+
var version = (create.src === this.op.src && create.seq === this.op.seq) ? create.v : null;
|
|
306
|
+
return callback(null, version);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// We can only reach here if the snapshot is missing the create metadata.
|
|
310
|
+
// This can happen if a client tries to re-create or resubmit a create op to
|
|
311
|
+
// a "legacy" snapshot that existed before we started adding the meta (should
|
|
312
|
+
// be uncommon) or when using a driver that doesn't support metadata (eg Postgres).
|
|
313
|
+
this.backend.db.getCommittedOpVersion(this.collection, this.id, this.snapshot, this.op, null, callback);
|
|
314
|
+
};
|
|
315
|
+
|
|
293
316
|
// Non-fatal client errors:
|
|
294
317
|
SubmitRequest.prototype.alreadySubmittedError = function() {
|
|
295
318
|
return new ShareDBError(ERROR_CODE.ERR_OP_ALREADY_SUBMITTED, 'Op already submitted');
|
package/package.json
CHANGED
|
@@ -145,7 +145,18 @@ describe('SnapshotVersionRequest', function() {
|
|
|
145
145
|
|
|
146
146
|
it('errors if asking for a version that does not exist', function(done) {
|
|
147
147
|
backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) {
|
|
148
|
-
expect(error
|
|
148
|
+
expect(error).to.be.ok;
|
|
149
|
+
expect(snapshot).to.equal(undefined);
|
|
150
|
+
done();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('errors if asking for a version that does not exist if the DB adapter does not error', function(done) {
|
|
155
|
+
sinon.stub(MemoryDb.prototype, 'getOps').callsFake(function(collection, id, from, to, options, callback) {
|
|
156
|
+
callback(null, []);
|
|
157
|
+
});
|
|
158
|
+
backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) {
|
|
159
|
+
expect(error).to.be.ok;
|
|
149
160
|
expect(snapshot).to.equal(undefined);
|
|
150
161
|
done();
|
|
151
162
|
});
|
package/test/client/submit.js
CHANGED
|
@@ -6,6 +6,7 @@ var deserializedType = require('./deserialized-type');
|
|
|
6
6
|
var numberType = require('./number-type');
|
|
7
7
|
var errorHandler = require('../util').errorHandler;
|
|
8
8
|
var richText = require('rich-text');
|
|
9
|
+
var MemoryDB = require('../../lib/db/memory');
|
|
9
10
|
types.register(deserializedType.type);
|
|
10
11
|
types.register(deserializedType.type2);
|
|
11
12
|
types.register(numberType.type);
|
|
@@ -206,44 +207,124 @@ module.exports = function() {
|
|
|
206
207
|
});
|
|
207
208
|
});
|
|
208
209
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
doc.fetch(function(err) {
|
|
214
|
-
if (err) return done(err);
|
|
215
|
-
expect(doc.data).eql({age: 3});
|
|
216
|
-
expect(doc.version).eql(1);
|
|
217
|
-
done();
|
|
218
|
-
});
|
|
210
|
+
|
|
211
|
+
describe('create', function() {
|
|
212
|
+
describe('metadata enabled', function() {
|
|
213
|
+
runCreateTests();
|
|
219
214
|
});
|
|
220
|
-
});
|
|
221
215
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
216
|
+
describe('no snapshot metadata available', function() {
|
|
217
|
+
beforeEach(function() {
|
|
218
|
+
var getSnapshot = MemoryDB.prototype.getSnapshot;
|
|
219
|
+
sinon.stub(MemoryDB.prototype, 'getSnapshot')
|
|
220
|
+
.callsFake(function() {
|
|
221
|
+
var args = Array.from(arguments);
|
|
222
|
+
var callback = args.pop();
|
|
223
|
+
args.push(function(error, snapshot) {
|
|
224
|
+
if (snapshot) delete snapshot.m;
|
|
225
|
+
callback(error, snapshot);
|
|
226
|
+
});
|
|
227
|
+
getSnapshot.apply(this, args);
|
|
228
|
+
});
|
|
231
229
|
});
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
230
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
var doc2 = this.backend.connect().get('dogs', 'fido');
|
|
238
|
-
doc.create({age: 3}, function(err) {
|
|
239
|
-
if (err) return done(err);
|
|
240
|
-
doc2.create({age: 4}, function(err) {
|
|
241
|
-
expect(err).instanceOf(Error);
|
|
242
|
-
expect(doc2.version).equal(1);
|
|
243
|
-
expect(doc2.data).eql({age: 3});
|
|
244
|
-
done();
|
|
231
|
+
afterEach(function() {
|
|
232
|
+
sinon.restore();
|
|
245
233
|
});
|
|
234
|
+
|
|
235
|
+
runCreateTests();
|
|
246
236
|
});
|
|
237
|
+
|
|
238
|
+
function runCreateTests() {
|
|
239
|
+
it('can create a new doc then fetch', function(done) {
|
|
240
|
+
var doc = this.backend.connect().get('dogs', 'fido');
|
|
241
|
+
doc.create({age: 3}, function(err) {
|
|
242
|
+
if (err) return done(err);
|
|
243
|
+
doc.fetch(function(err) {
|
|
244
|
+
if (err) return done(err);
|
|
245
|
+
expect(doc.data).eql({age: 3});
|
|
246
|
+
expect(doc.version).eql(1);
|
|
247
|
+
done();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('calling create on the same doc twice fails', function(done) {
|
|
253
|
+
var doc = this.backend.connect().get('dogs', 'fido');
|
|
254
|
+
doc.create({age: 3}, function(err) {
|
|
255
|
+
if (err) return done(err);
|
|
256
|
+
doc.create({age: 4}, function(err) {
|
|
257
|
+
expect(err).instanceOf(Error);
|
|
258
|
+
expect(doc.version).equal(1);
|
|
259
|
+
expect(doc.data).eql({age: 3});
|
|
260
|
+
done();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('trying to create an already created doc without fetching fails and fetches', function(done) {
|
|
266
|
+
var doc = this.backend.connect().get('dogs', 'fido');
|
|
267
|
+
var doc2 = this.backend.connect().get('dogs', 'fido');
|
|
268
|
+
doc.create({age: 3}, function(err) {
|
|
269
|
+
if (err) return done(err);
|
|
270
|
+
doc2.create({age: 4}, function(err) {
|
|
271
|
+
expect(err).instanceOf(Error);
|
|
272
|
+
expect(doc2.version).equal(1);
|
|
273
|
+
expect(doc2.data).eql({age: 3});
|
|
274
|
+
done();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('does not fail when resubmitting a create op', function(done) {
|
|
280
|
+
var backend = this.backend;
|
|
281
|
+
var connection = backend.connect();
|
|
282
|
+
var submitted = false;
|
|
283
|
+
backend.use('submit', function(request, next) {
|
|
284
|
+
if (!submitted) {
|
|
285
|
+
submitted = true;
|
|
286
|
+
connection.close();
|
|
287
|
+
backend.connect(connection);
|
|
288
|
+
}
|
|
289
|
+
next();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
var doc = connection.get('dogs', 'fido');
|
|
293
|
+
doc.create({age: 3}, function(error) {
|
|
294
|
+
expect(doc.version).to.equal(1);
|
|
295
|
+
done(error);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does not fail when resubmitting a create op on a doc that was deleted', function(done) {
|
|
300
|
+
var backend = this.backend;
|
|
301
|
+
var connection1 = backend.connect();
|
|
302
|
+
var connection2 = backend.connect();
|
|
303
|
+
var doc1 = connection1.get('dogs', 'fido');
|
|
304
|
+
var doc2 = connection2.get('dogs', 'fido');
|
|
305
|
+
|
|
306
|
+
async.series([
|
|
307
|
+
doc1.create.bind(doc1, {age: 3}),
|
|
308
|
+
doc1.del.bind(doc1),
|
|
309
|
+
function(next) {
|
|
310
|
+
var submitted = false;
|
|
311
|
+
backend.use('submit', function(request, next) {
|
|
312
|
+
if (!submitted) {
|
|
313
|
+
submitted = true;
|
|
314
|
+
connection2.close();
|
|
315
|
+
backend.connect(connection2);
|
|
316
|
+
}
|
|
317
|
+
next();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
doc2.create({name: 'Fido'}, function(error) {
|
|
321
|
+
expect(doc2.version).to.equal(3);
|
|
322
|
+
next(error);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
], done);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
247
328
|
});
|
|
248
329
|
|
|
249
330
|
it('server fetches and transforms by already committed op', function(done) {
|
package/test/db-memory.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
var expect = require('chai').expect;
|
|
2
|
+
var Backend = require('../lib/backend');
|
|
2
3
|
var DB = require('../lib/db');
|
|
3
4
|
var BasicQueryableMemoryDB = require('./BasicQueryableMemoryDB');
|
|
5
|
+
var async = require('async');
|
|
4
6
|
|
|
5
7
|
describe('DB base class', function() {
|
|
6
8
|
it('can call db.close() without callback', function() {
|
|
@@ -54,17 +56,83 @@ describe('DB base class', function() {
|
|
|
54
56
|
});
|
|
55
57
|
});
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
describe('MemoryDB', function() {
|
|
60
|
+
// Run all the DB-based tests against the BasicQueryableMemoryDB.
|
|
61
|
+
require('./db')({
|
|
62
|
+
create: function(options, callback) {
|
|
63
|
+
if (typeof options === 'function') {
|
|
64
|
+
callback = options;
|
|
65
|
+
options = null;
|
|
66
|
+
}
|
|
67
|
+
var db = new BasicQueryableMemoryDB(options);
|
|
68
|
+
callback(null, db);
|
|
69
|
+
},
|
|
70
|
+
getQuery: function(options) {
|
|
71
|
+
return {filter: options.query, sort: options.sort};
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('deleteOps', function() {
|
|
76
|
+
describe('with some ops', function() {
|
|
77
|
+
var backend;
|
|
78
|
+
var db;
|
|
79
|
+
var connection;
|
|
80
|
+
var doc;
|
|
81
|
+
|
|
82
|
+
beforeEach(function(done) {
|
|
83
|
+
backend = new Backend();
|
|
84
|
+
db = backend.db;
|
|
85
|
+
connection = backend.connect();
|
|
86
|
+
doc = connection.get('dogs', 'fido');
|
|
87
|
+
|
|
88
|
+
async.waterfall([
|
|
89
|
+
doc.create.bind(doc, {name: 'Fido'}),
|
|
90
|
+
doc.submitOp.bind(doc, [{p: ['tricks'], oi: ['fetch']}]),
|
|
91
|
+
db.getOps.bind(db, 'dogs', 'fido', null, null, null),
|
|
92
|
+
function(ops, next) {
|
|
93
|
+
expect(ops).to.have.length(2);
|
|
94
|
+
next();
|
|
95
|
+
}
|
|
96
|
+
], done);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('deletes all ops', function(done) {
|
|
100
|
+
async.waterfall([
|
|
101
|
+
db.deleteOps.bind(db, 'dogs', 'fido', null, null, null),
|
|
102
|
+
function(next) {
|
|
103
|
+
db.getOps('dogs', 'fido', null, null, null, function(error) {
|
|
104
|
+
expect(error.message).to.equal('Missing ops');
|
|
105
|
+
next();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
], done);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('deletes some ops', function(done) {
|
|
112
|
+
async.waterfall([
|
|
113
|
+
db.deleteOps.bind(db, 'dogs', 'fido', 0, 1, null),
|
|
114
|
+
db.getOps.bind(db, 'dogs', 'fido', 1, 2, null),
|
|
115
|
+
function(ops, next) {
|
|
116
|
+
expect(ops).to.have.length(1);
|
|
117
|
+
expect(ops[0].op).to.eql([{p: ['tricks'], oi: ['fetch']}]);
|
|
118
|
+
db.getOps('dogs', 'fido', 0, 1, null, function(error) {
|
|
119
|
+
expect(error.message).to.equal('Missing ops');
|
|
120
|
+
next();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
], done);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('submits more ops after deleting ops', function(done) {
|
|
127
|
+
async.series([
|
|
128
|
+
db.deleteOps.bind(db, 'dogs', 'fido', null, null, null),
|
|
129
|
+
doc.submitOp.bind(doc, [{p: ['tricks', 1], li: 'sit'}]),
|
|
130
|
+
function(next) {
|
|
131
|
+
expect(doc.data.tricks).to.eql(['fetch', 'sit']);
|
|
132
|
+
next();
|
|
133
|
+
}
|
|
134
|
+
], done);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
70
138
|
});
|