diff --git a/README.md b/README.md index af0b1f64b..591eaf7a8 100644 --- a/README.md +++ b/README.md @@ -321,12 +321,18 @@ same time as callbacks to `fetch` and `subscribe`. `doc.on('create', function(source) {...})` The document was created. Technically, this means it has a type. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally. +`doc.on('before op batch'), function() {...})` +An operation batch is about to be applied to the data. For each partial operation a pair of `before op` and `op` events will be emitted after this event. + `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 batch'), function() {...})` +An operation batch was applied to the data. + `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. diff --git a/lib/client/doc.js b/lib/client/doc.js index 95776db4b..ec8cdfa3b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -33,10 +33,13 @@ var types = require('../types'); * ------ * * You can use doc.on(eventName, callback) to subscribe to the following events: + * * - `before op batch ()` Fired before an operation batch is applied to the data. + * It may be used to read the old data just before applying an operation * - `before op (op, source)` Fired before a partial operation is applied to the data. * It may be used to read the old data just before applying an operation * - `op (op, source)` Fired after every partial operation with this operation as the * first argument + * - `after op batch ()` Fired after an operation batch was applied to the data. * - `create (source)` The document was created. That means its type was * set and it has some initial data. * - `del (data, source)` Fired after the document is deleted, that is @@ -503,10 +506,10 @@ function transformX(client, server) { /** * Applies the operation to the snapshot * - * If the operation is create or delete it emits `create` or `del`. Then the - * operation is applied to the snapshot and `op` and `after op` are emitted. - * If the type supports incremental updates and `this.incremental` is true we - * fire `op` after every small operation. + * If the operation is create or delete it emits `create` or `del`. + * For each partial operation `before op` is emitted, then the operation is applied + * to the snapshot and `op` is emitted. For each operation batch `before op batch` and + * `after op batch` are emitted before and after applying all partial operations. * * This is the only function to fire the above mentioned events. * @@ -537,6 +540,7 @@ Doc.prototype._otApply = function(op, source) { if (!source && this.type === types.defaultType && op.op.length > 1) { if (!this.applyStack) this.applyStack = []; var stackLength = this.applyStack.length; + this.emit('before op batch'); for (var i = 0; i < op.op.length; i++) { var component = op.op[i]; var componentOp = {op: [component]}; @@ -552,11 +556,13 @@ Doc.prototype._otApply = function(op, source) { this.data = this.type.apply(this.data, componentOp.op); this.emit('op', componentOp.op, source); } + this.emit('after op batch'); // Pop whatever was submitted since we started applying this op this._popApplyStack(stackLength); return; } + this.emit('before op batch'); // 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); @@ -568,6 +574,7 @@ Doc.prototype._otApply = function(op, source) { // 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 batch'); return; } @@ -753,7 +760,7 @@ Doc.prototype._tryCompose = function(op) { // @param options {source: ...} // @param [callback] called after operation submitted // -// @fires before op, op, after op +// @fires before op batch, before op, op, after op batch Doc.prototype.submitOp = function(component, options, callback) { if (typeof options === 'function') { callback = options; diff --git a/test/client/doc.js b/test/client/doc.js index f7529d71e..770f5b3ec 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,5 +1,6 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); +var sinon = require('sinon'); var util = require('../util') describe('client query subscribe', function() { @@ -154,6 +155,42 @@ describe('client query subscribe', function() { }); }); + it('remote multi component ops are emit only one of `before op batch` and `after op batch` events', function(done) { + var doc = this.doc; + var doc2 = this.doc2; + var doc3 = this.doc3; + + var receivedOps = []; + + var beforeOpBatchHandler = sinon.fake(); + var afterOpBatchHandler = sinon.fake(); + + doc.on('before op', beforeOpBatchHandler); + doc.on('after op', afterOpBatchHandler); + doc.on('op', function(op, source) { + receivedOps.push(op); + }); + + var remoteOp = [ + {p: ['tricks'], oi: ['fetching']}, + {p: ['tricks', 0], li: 'stand'}, + {p: ['tricks', 1], li: 'shake'}, + {p: ['tricks', 2, 5], sd: 'ing'}, + {p: ['tricks', 0], lm: 2} + ]; + + doc2.submitOp(remoteOp, function(err) { + if (err) return done(err); + doc.fetch(); + verifyConsistency(doc, doc2, doc3, [], done); + }); + + expect(beforeOpBatchHandler.calledOnce); + expect(afterOpBatchHandler.calledOnce); + expect(receivedOps.length == remoteOp.length); + + }); + it('remote multi component ops are transformed by ops submitted in `op` event handlers', function(done) { var doc = this.doc; var doc2 = this.doc2;