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 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 (to == null) {
88
- to = opLog.length;
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) {
@@ -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
- backend.db.getCommittedOpVersion(collection, id, snapshot, op, null, function(err, version) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "4.1.5",
3
+ "version": "5.0.1",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -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.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
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
  });
@@ -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
- it('can create a new doc then fetch', function(done) {
210
- var doc = this.backend.connect().get('dogs', 'fido');
211
- doc.create({age: 3}, function(err) {
212
- if (err) return done(err);
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
- it('calling create on the same doc twice fails', function(done) {
223
- var doc = this.backend.connect().get('dogs', 'fido');
224
- doc.create({age: 3}, function(err) {
225
- if (err) return done(err);
226
- doc.create({age: 4}, function(err) {
227
- expect(err).instanceOf(Error);
228
- expect(doc.version).equal(1);
229
- expect(doc.data).eql({age: 3});
230
- done();
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
- it('trying to create an already created doc without fetching fails and fetches', function(done) {
236
- var doc = this.backend.connect().get('dogs', 'fido');
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
- // Run all the DB-based tests against the BasicQueryableMemoryDB.
58
- require('./db')({
59
- create: function(options, callback) {
60
- if (typeof options === 'function') {
61
- callback = options;
62
- options = null;
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
- var db = new BasicQueryableMemoryDB(options);
65
- callback(null, db);
66
- },
67
- getQuery: function(options) {
68
- return {filter: options.query, sort: options.sort};
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
  });