diff --git a/lib/client/doc.js b/lib/client/doc.js index 8c88f4cbc..da65d6dc0 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -186,9 +186,9 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (this.version > snapshot.v) return callback && callback(); this.version = snapshot.v; - this.data = snapshot.data; var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); + this.data = (this.type && this.type.deserialize) ? this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); callback && callback(); }; @@ -552,7 +552,8 @@ Doc.prototype._otApply = function(op, source) { if (op.create) { this._setType(op.create.type); - this.data = this.type.create(op.create.data); + var method = (this.type.createDeserialized) ? 'createDeserialized' : 'create'; + this.data = this.type[method](op.create.data); this.emit('create', source); return; } diff --git a/test/client/submit.js b/test/client/submit.js index 41cf1947d..1a803b6b3 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1,5 +1,9 @@ var async = require('async'); var expect = require('expect.js'); +var types = require('../../lib/types'); +var serializable_type = require('../ot-mock-serializable-json0').type; + +types.register(serializable_type); module.exports = function() { describe('client submit', function() { @@ -39,6 +43,16 @@ describe('client submit', function() { }); }); + it('can create a new doc with a serializable type', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create(serializable_type.serialize({age: 3}), serializable_type.uri, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3}); + expect(doc.version).eql(1); + done(); + }); + }); + it('can create then delete then create a doc', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { @@ -74,6 +88,19 @@ describe('client submit', function() { }); }); + it('can create then submit an op on a serializable type', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create(serializable_type.serialize({age: 3}), serializable_type.uri, function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 5}); + expect(doc.version).eql(2); + done(); + }); + }); + }); + it('can create then submit an op sync', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}); @@ -140,6 +167,33 @@ describe('client submit', function() { }); }); + it('ops submitted sync get composed with a serializable type', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create(serializable_type.serialize({age: 3}), serializable_type.uri); + doc.submitOp({p: ['age'], na: 2}); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 7}); + // Version is 1 instead of 3, because the create and ops got composed + expect(doc.version).eql(1); + doc.submitOp({p: ['age'], na: 2}); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 11}); + // Ops get composed + expect(doc.version).eql(2); + doc.submitOp({p: ['age'], na: 2}); + doc.del(function(err) { + if (err) return done(err); + expect(doc.data).eql(undefined); + // del DOES NOT get composed + expect(doc.version).eql(4); + done(); + }); + }); + }); + }); + it('does not compose ops when doc.preventCompose is true', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.preventCompose = true; @@ -380,6 +434,23 @@ describe('client submit', function() { }); }); + it('can commit then fetch in a new connection to get the same data with a serializable type', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create(serializable_type.serialize({age: 3}), serializable_type.uri, function(err) { + if (err) return done(err); + doc2.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3}); + expect(doc2.data).eql({age: 3}); + expect(doc.version).eql(1); + expect(doc2.version).eql(1); + expect(doc.data).not.equal(doc2.data); + done(); + }); + }); + }); + it('an op submitted concurrently is transformed by the first', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); diff --git a/test/ot-mock-serializable-json0.js b/test/ot-mock-serializable-json0.js new file mode 100644 index 000000000..e0e8981e4 --- /dev/null +++ b/test/ot-mock-serializable-json0.js @@ -0,0 +1,62 @@ +var ot_json0 = require('ot-json0').type; +var HEADER = '!HEADER!'; + +/** ot-mock-serializable-json0 is a simple wrapper around + * the ot-json0 type providing support for custom serialization + */ +var ot_mock_serializable_json0 = { + name: 'ot_mock_serializable_json0', + uri: 'http://sharejs.org/types/ot_mock_serializable_json0', + + create: function( initialData ) { + if(!isSerialized(initialData)) { + throw new Error('SerializableJson0: initialData must be serialized'); + } + return initialData; + }, + + createDeserialized: function(initialData) { + if(!isSerialized(initialData)) { + throw new Error('SerializableJson0: initialData must be serialized'); + } + return ot_mock_serializable_json0.deserialize(initialData); + }, + + apply: function( data, op ) { + var is_serialized = isSerialized(data); + if(is_serialized) { + data = ot_mock_serializable_json0.deserialize(data); + } + data = ot_json0.apply(data, op); + if(is_serialized) { + data = ot_mock_serializable_json0.serialize(data); + } + return data; + }, + + transform: ot_json0.transform, + compose: ot_json0.compose, + invert: ot_json0.invert, + normalize: ot_json0.normalize, + + serialize: function( data ) { + if(isSerialized(data)) { + throw new Error('SerializableJson0: cannot serialize an already serialized ot type instance'); + } + return HEADER + JSON.stringify(data); + }, + + deserialize: function( data ) { + if(!isSerialized(data)) { + throw new Error('SerializableJson0: cannot deserialize an already deserialized ot type instance'); + } + return JSON.parse( data.substring(HEADER.length) ); + }, + +}; + +exports.type = ot_mock_serializable_json0; + +function isSerialized( data ) { + return typeof data === 'string' && data.substring(0, HEADER.length) === HEADER; +} \ No newline at end of file