Skip to content

Commit a14175d

Browse files
author
Alec Gibson
committed
Add batch op events
This change adds two events: - `before op batch` - fired once before any set of ops is applied. - `op batch` - fired once after any set of ops is applied. This may follow multiple `op` events if the op had multiple `json0` components This is a non-breaking change that should allow clients to process ops in their entirety. There has already been some discussion around this: - #129 - #396 This is a much simpler approach than the existing pull request. Here we try not to change existing behaviour, and only add new, non-breaking events. Motivation for such an event would include clients applying some form of validation logic, which doesn't make sense if an op is shattered. For example, consider a client that wants to ensure a field `mustBePresent` is always populated: ```js doc.on('op', () => { if (!doc.data.mustBePresent) throw new Error('invalid'); }); remoteDoc.submitOp([ {p: ['mustBePresent', 0], sd: 'existing value'}, {p: ['mustBePresent', 0], si: 'new value'}, ]); ``` In the above example, the submitted op is clearly attempting to perform a replacement. However, the receiving `doc` only receives this replacement in parts, so it looks like the document reaches an invalid state, when actually the submitted op is perfectly valid. In this case we `throw`, but we could have also attempted to populate with a default value, which could interfere with the desired value. This change fixes the above issue, because now we can just listen for the `op batch` event, and consider the document once all the components of a given op have been applied.
1 parent 346eb0c commit a14175d

File tree

3 files changed

+69
-0
lines changed

3 files changed

+69
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,12 @@ An operation is about to be applied to the data. `source` will be `false` for op
364364
`doc.on('op', function(op, source) {...})`
365365
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.
366366

367+
`doc.on('before op batch'), function(op, source) {...})`
368+
A potentially multi-part 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.
369+
370+
`doc.on('op batch'), function(op, source) {...})`
371+
A potentially multi-part operation was applied to the data. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally.
372+
367373
`doc.on('del', function(data, source) {...})`
368374
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.
369375

lib/client/doc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,10 @@ Doc.prototype._otApply = function(op, source) {
581581
);
582582
}
583583

584+
// NB: If we need to add another argument to this event, we should consider
585+
// the fact that the 'op' event has op.src as its 3rd argument
586+
this.emit('before op batch', op.op, source);
587+
584588
// Iteratively apply multi-component remote operations and rollback ops
585589
// (source === false) for the default JSON0 OT type. It could use
586590
// type.shatter(), but since this code is so specific to use cases for the
@@ -614,6 +618,7 @@ Doc.prototype._otApply = function(op, source) {
614618
this.data = this.type.apply(this.data, componentOp.op);
615619
this.emit('op', componentOp.op, source, op.src);
616620
}
621+
this.emit('op batch', op.op, source);
617622
// Pop whatever was submitted since we started applying this op
618623
this._popApplyStack(stackLength);
619624
return;
@@ -630,6 +635,7 @@ Doc.prototype._otApply = function(op, source) {
630635
// For ops from other clients, this will be after the op has been
631636
// committed to the database and published
632637
this.emit('op', op.op, source, op.src);
638+
this.emit('op batch', op.op, source);
633639
return;
634640
}
635641

test/client/doc.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var Backend = require('../../lib/backend');
22
var expect = require('chai').expect;
33
var async = require('async');
4+
var errorHandler = require('../util').errorHandler;
45

56
describe('Doc', function() {
67
beforeEach(function() {
@@ -266,6 +267,62 @@ describe('Doc', function() {
266267
verifyConsistency(doc, doc2, doc3, handlers, done);
267268
});
268269
});
270+
271+
it('emits batch op events for a multi-component local op', function(done) {
272+
var doc = this.doc;
273+
var beforeOpBatchCount = 0;
274+
275+
var submittedOp = [
276+
{p: ['tricks'], oi: ['fetching']},
277+
{p: ['tricks', 0], li: 'stand'}
278+
];
279+
280+
doc.on('before op batch', function(op, source) {
281+
expect(op).to.eql(submittedOp);
282+
expect(source).to.be.true;
283+
beforeOpBatchCount++;
284+
});
285+
286+
doc.on('op batch', function(op, source) {
287+
expect(op).to.eql(submittedOp);
288+
expect(source).to.be.true;
289+
expect(beforeOpBatchCount).to.equal(1);
290+
expect(doc.data).to.eql({tricks: ['stand', 'fetching']});
291+
done();
292+
});
293+
294+
doc.submitOp(submittedOp, errorHandler(done));
295+
});
296+
297+
it('emits batch op events for a multi-component remote op', function(done) {
298+
var doc = this.doc;
299+
var doc2 = this.doc2;
300+
var beforeOpBatchCount = 0;
301+
302+
var submittedOp = [
303+
{p: ['tricks'], oi: ['fetching']},
304+
{p: ['tricks', 0], li: 'stand'}
305+
];
306+
307+
doc.on('before op batch', function(op, source) {
308+
expect(op).to.eql(submittedOp);
309+
expect(source).to.be.false;
310+
beforeOpBatchCount++;
311+
});
312+
313+
doc.on('op batch', function(op, source) {
314+
expect(op).to.eql(submittedOp);
315+
expect(source).to.be.false;
316+
expect(beforeOpBatchCount).to.equal(1);
317+
expect(doc.data).to.eql({tricks: ['stand', 'fetching']});
318+
done();
319+
});
320+
321+
async.series([
322+
doc.subscribe.bind(doc),
323+
doc2.submitOp.bind(doc2, submittedOp)
324+
], errorHandler(done));
325+
});
269326
});
270327

271328
describe('submitting ops in callbacks', function() {

0 commit comments

Comments
 (0)