Skip to content

Commit

Permalink
Change and standardize doc op events
Browse files Browse the repository at this point in the history
Standardized events so that they always work the same way, no exceptions. Changed `op` event to `after op`. Added `before component` and `after component` events.
  • Loading branch information
roark committed Oct 8, 2016
1 parent bb11214 commit cdefa91
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 41 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,14 @@ The document was created. Technically, this means it has a type. `source` will b
`doc.on('before op'), function(op, source) {...})`
An operation is about to be applied to the data. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally.

`doc.on('op', function(op, source) {...})`
An operation was applied to the data. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally.
`doc.on('after op', function(op, source) {...})`
An operation was entirely applied to the data. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally.

`doc.on('before component', function(op, source) {...})`
An operation component is about to be applied to the data. `op` will be part of a shattered operation consisting of an operation with only a single component to be applied. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally. If incremental apply is disabled or the doc ot type doesn't support shatter(), this event will still emit but with `op` being the entire operation.

`doc.on('after component', function(op, source) {...})`
An operation component was applied to the data. `op` will be part of a shattered operation consisting of an operation with only a single component that has been applied. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally. If incremental apply is disabled or the doc ot type doesn't support shatter(), this event will still emit but with `op` being the entire operation.

`doc.on('del', function(data, source) {...})`
The document was deleted. Document contents before deletion are passed in as an argument. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally.
Expand Down
47 changes: 27 additions & 20 deletions lib/client/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ function Doc(connection, collection, id) {
// The OT type of this document. An uncreated document has type `null`
this.type = null;

// Enable ops be incrementally applied. OT Type must support type.shatter()
this.applyLocalOpsIncremental = false;
this.applyRemoteOpsIncremental = true;

// The applyStack enables us to track any ops submitted while we are
// applying an op incrementally. This value is an array when we are
// performing an incremental apply and null otherwise. When it is an array,
Expand Down Expand Up @@ -507,12 +511,11 @@ Doc.prototype._otApply = function(op, source) {
return this.emit('error', err);
}

// Iteratively apply multi-component remote operations and rollback ops
// (source === false) for the default JSON0 OT type. It could use
// type.shatter(), but since this code is so specific to use cases for the
// JSON0 type and ShareDB explicitly bundles the default type, we might as
// well write it this way and save needing to iterate through the op
// components twice.
// The 'before op' event enables clients to pull any necessary data out of
// the snapshot before it gets changed
this.emit('before op', op.op, source);

// Iteratively apply multi-component for the OT types that support type.shatter().
//
// Ideally, we would not need this extra complexity. However, it is
// helpful for implementing bindings that update DOM nodes and other
Expand All @@ -522,12 +525,15 @@ Doc.prototype._otApply = function(op, source) {
// that the snapshot only include updates from the particular op component
// at the time of emission. Eliminating this would require rethinking how
// such external bindings are implemented.
if (!source && this.type === types.defaultType && op.op.length > 1) {
if ( (this.type.shatter && op.op.length > 1) &&
( (this.applyLocalOpsIncremental && source) ||
(this.applyRemoteOpsIncremental && !source) ) ) {

if (!this.applyStack) this.applyStack = [];
var stackLength = this.applyStack.length;
for (var i = 0; i < op.op.length; i++) {
var component = op.op[i];
var componentOp = {op: [component]};
var shatteredOps = this.type.shatter(op.op);
for (var i = 0; i < shatteredOps.length; i++) {
var componentOp = {op: shatteredOps[i]};
// Transform componentOp against any ops that have been submitted
// sychronously inside of an op event handler since we began apply of
// our operation
Expand All @@ -536,26 +542,27 @@ Doc.prototype._otApply = function(op, source) {
if (transformErr) return this._hardRollback(transformErr);
}
// Apply the individual op component
this.emit('before op', componentOp.op, source);
this.emit('before component', componentOp.op, source);
this.data = this.type.apply(this.data, componentOp.op);
this.emit('op', componentOp.op, source);
this.emit('after component', componentOp.op, source);
}
// Pop whatever was submitted since we started applying this op
this._popApplyStack(stackLength);
return;
}

// The 'before op' event enables clients to pull any necessary data out of
// the snapshot before it gets changed
this.emit('before op', op.op, source);
// Apply the operation to the local data, mutating it in place
this.data = this.type.apply(this.data, op.op);
// Emit an 'op' event once the local data includes the changes from the
// Apply the full operation to the local data, mutating it in place
else {
this.emit('before component', op.op, source);
this.data = this.type.apply(this.data, op.op);
this.emit('after component', op.op, source);
}

// Emit an 'after op' event once the local data includes the changes from the
// op. For locally submitted ops, this will be synchronously with
// submission and before the server or other clients have received the op.
// For ops from other clients, this will be after the op has been
// committed to the database and published
this.emit('op', op.op, source);
this.emit('after op', op.op, source);
return;
}

Expand Down
91 changes: 85 additions & 6 deletions test/client/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('client query subscribe', function() {
});
}

it('single component ops emit an `op` event', function(done) {
it('single component ops emit an `after component` event', function(done) {
var doc = this.doc;
var doc2 = this.doc2;
var doc3 = this.doc3;
Expand All @@ -92,7 +92,7 @@ describe('client query subscribe', function() {
expect(doc.data).eql({color: 'black'});
}
];
doc.on('op', function(op, source) {
doc.on('after component', function(op, source) {
var handler = handlers.shift();
handler(op, source);
});
Expand All @@ -104,7 +104,7 @@ describe('client query subscribe', function() {
});
});

it('remote multi component ops emit individual `op` events', function(done) {
it('remote multi component ops emit multiple `after component` events', function(done) {
var doc = this.doc;
var doc2 = this.doc2;
var doc3 = this.doc3;
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('client query subscribe', function() {
expect(doc.data).eql({color: 'black', weight: 40, age: 5, owner: 'sue'});
}
];
doc.on('op', function(op, source) {
doc.on('after component', function(op, source) {
var handler = handlers.shift();
handler(op, source);
});
Expand All @@ -153,7 +153,7 @@ describe('client query subscribe', function() {
});
});

it('remote multi component ops are transformed by ops submitted in `op` event handlers', function(done) {
it('remote multi component ops are transformed by ops submitted in `after component` event handlers', function(done) {
var doc = this.doc;
var doc2 = this.doc2;
var doc3 = this.doc3;
Expand Down Expand Up @@ -192,7 +192,7 @@ describe('client query subscribe', function() {
expect(doc.data).eql({tricks: ['shake', 'tug stick']});
}
];
doc.on('op', function(op, source) {
doc.on('after component', function(op, source) {
var handler = handlers.shift();
handler(op, source);
});
Expand All @@ -210,6 +210,85 @@ describe('client query subscribe', function() {
});
});


it('ops emit all lifecycle events', function(done) {
var doc = this.doc;
var doc2 = this.doc2;
var doc3 = this.doc3;
var handlers = [
// doc submit before op
function(op, source) {
expect(source).equal(true);
expect(op).eql([{p: ['make'], oi: 'bmw'}, {p: ['speed'], oi: 160}]);
expect(doc.data).eql({});
// doc submit before component 1
}, function(op, source) {
expect(source).equal(true);
expect(op).eql([{p: ['make'], oi: 'bmw'}]);
expect(doc.data).eql({});
// doc submit after component 1
}, function(op, source) {
expect(source).equal(true);
expect(op).eql([{p: ['make'], oi: 'bmw'}]);
expect(doc.data).eql({make: 'bmw'});
// doc submit before component 2
}, function(op, source) {
expect(source).equal(true);
expect(op).eql([{p: ['speed'], oi: 160}]);
expect(doc.data).eql({make: 'bmw'});
// doc submit after component 2
}, function(op, source) {
expect(source).equal(true);
expect(op).eql([{p: ['speed'], oi: 160}]);
expect(doc.data).eql({make: 'bmw', speed: 160});
// doc submit after op
}, function(op, source) {
expect(source).equal(true);
expect(op).eql([{p: ['make'], oi: 'bmw'}, {p: ['speed'], oi: 160}]);
expect(doc.data).eql({make: 'bmw', speed: 160});
// doc2 submit before op
}, function(op, source) {
expect(source).equal(false);
expect(op).eql([{p: ['model'], oi: '260e'}]);
expect(doc.data).eql({make: 'bmw', speed: 160});
// doc2 submit before component 1
}, function(op, source) {
expect(source).equal(false);
expect(op).eql([{p: ['model'], oi: '260e'}]);
expect(doc.data).eql({make: 'bmw', speed: 160});
// doc2 submit after component 1
}, function(op, source) {
expect(source).equal(false);
expect(op).eql([{p: ['model'], oi: '260e'}]);
expect(doc.data).eql({make: 'bmw', model: '260e', speed: 160});
// doc2 submit after op
}, function(op, source) {
expect(source).equal(false);
expect(op).eql([{p: ['model'], oi: '260e'}]);
expect(doc.data).eql({make: 'bmw', model: '260e', speed: 160});
}
];

doc.applyLocalOpsIncremental = true;
doc.applyRemoteOpsIncremental = true;

var handleOpEvent = function(op, source) {
var handler = handlers.shift();
handler(op, source);
};
doc.on('before op', handleOpEvent);
doc.on('before component', handleOpEvent);
doc.on('after component', handleOpEvent);
doc.on('after op', handleOpEvent);

doc2.submitOp([{p: ['make'], oi: 'mercedes'}, {p: ['model'], oi: '260e'}], function(err) {
if (err) return done(err);
doc.submitOp([{p: ['make'], oi: 'bmw'}, {p: ['speed'], oi: 160}]);
expect(doc.data).eql({make: 'bmw', speed: 160});
verifyConsistency(doc, doc2, doc3, handlers, done);
});
});

});

});
4 changes: 2 additions & 2 deletions test/client/projections.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('client projections', function() {
var fido = connection2.get('dogs_summary', 'fido');
fido.subscribe(function(err) {
if (err) return done(err);
fido.on('op', function() {
fido.on('after op', function() {
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('client projections', function() {
var fido = connection2.get('dogs_summary', 'fido');
connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
if (err) return done(err);
fido.on('op', function() {
fido.on('after op', function() {
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
Expand Down
22 changes: 11 additions & 11 deletions test/client/subscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
done();
});
doc2[method]();
Expand Down Expand Up @@ -343,7 +343,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
expect(doc2.version).eql(2);
expect(doc2.data).eql({age: 4});
done();
Expand All @@ -360,7 +360,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
done();
});
doc2.connection.close();
Expand All @@ -377,7 +377,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
done();
});
backend.suppressPublish = true;
Expand All @@ -393,7 +393,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
done();
});
doc2.unsubscribe(function(err) {
Expand All @@ -411,7 +411,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
done();
});
doc2.destroy(function(err) {
Expand Down Expand Up @@ -441,7 +441,7 @@ describe('client subscribe', function() {
function(cb) { spot.unsubscribe(cb); }
], function(err) {
if (err) return done(err);
fido.on('op', function(op, context) {
fido.on('after op', function(op, context) {
done();
});
doc.submitOp({p: ['age'], na: 1}, done);
Expand All @@ -459,7 +459,7 @@ describe('client subscribe', function() {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
expect(doc2.version).eql(2);
expect(doc2.data).eql({age: 4});
done();
Expand All @@ -483,7 +483,7 @@ describe('client subscribe', function() {
doc2.unsubscribe();
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
done();
});
doc.submitOp({p: ['age'], na: 1});
Expand All @@ -503,7 +503,7 @@ describe('client subscribe', function() {
[{p: ['age'], na: 1}],
[{p: ['age'], na: 5}],
];
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
var item = expected.shift();
expect(op).eql(item);
if (expected.length) return;
Expand Down Expand Up @@ -535,7 +535,7 @@ describe('client subscribe', function() {
doc2.subscribe(function(err) {
if (err) return done(err);
var wait = 4;
doc2.on('op', function(op, context) {
doc2.on('after op', function(op, context) {
if (--wait) return;
expect(doc2.version).eql(5);
expect(doc2.data).eql({age: 122});
Expand Down

0 comments on commit cdefa91

Please sign in to comment.