Skip to content

Commit cdefa91

Browse files
committed
Change and standardize doc op events
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.
1 parent bb11214 commit cdefa91

File tree

5 files changed

+133
-41
lines changed

5 files changed

+133
-41
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,14 @@ The document was created. Technically, this means it has a type. `source` will b
220220
`doc.on('before op'), function(op, source) {...})`
221221
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.
222222

223-
`doc.on('op', function(op, source) {...})`
224-
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.
223+
`doc.on('after op', function(op, source) {...})`
224+
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.
225+
226+
`doc.on('before component', function(op, source) {...})`
227+
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.
228+
229+
`doc.on('after component', function(op, source) {...})`
230+
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.
225231

226232
`doc.on('del', function(data, source) {...})`
227233
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.

lib/client/doc.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ function Doc(connection, collection, id) {
8787
// The OT type of this document. An uncreated document has type `null`
8888
this.type = null;
8989

90+
// Enable ops be incrementally applied. OT Type must support type.shatter()
91+
this.applyLocalOpsIncremental = false;
92+
this.applyRemoteOpsIncremental = true;
93+
9094
// The applyStack enables us to track any ops submitted while we are
9195
// applying an op incrementally. This value is an array when we are
9296
// performing an incremental apply and null otherwise. When it is an array,
@@ -507,12 +511,11 @@ Doc.prototype._otApply = function(op, source) {
507511
return this.emit('error', err);
508512
}
509513

510-
// Iteratively apply multi-component remote operations and rollback ops
511-
// (source === false) for the default JSON0 OT type. It could use
512-
// type.shatter(), but since this code is so specific to use cases for the
513-
// JSON0 type and ShareDB explicitly bundles the default type, we might as
514-
// well write it this way and save needing to iterate through the op
515-
// components twice.
514+
// The 'before op' event enables clients to pull any necessary data out of
515+
// the snapshot before it gets changed
516+
this.emit('before op', op.op, source);
517+
518+
// Iteratively apply multi-component for the OT types that support type.shatter().
516519
//
517520
// Ideally, we would not need this extra complexity. However, it is
518521
// helpful for implementing bindings that update DOM nodes and other
@@ -522,12 +525,15 @@ Doc.prototype._otApply = function(op, source) {
522525
// that the snapshot only include updates from the particular op component
523526
// at the time of emission. Eliminating this would require rethinking how
524527
// such external bindings are implemented.
525-
if (!source && this.type === types.defaultType && op.op.length > 1) {
528+
if ( (this.type.shatter && op.op.length > 1) &&
529+
( (this.applyLocalOpsIncremental && source) ||
530+
(this.applyRemoteOpsIncremental && !source) ) ) {
531+
526532
if (!this.applyStack) this.applyStack = [];
527533
var stackLength = this.applyStack.length;
528-
for (var i = 0; i < op.op.length; i++) {
529-
var component = op.op[i];
530-
var componentOp = {op: [component]};
534+
var shatteredOps = this.type.shatter(op.op);
535+
for (var i = 0; i < shatteredOps.length; i++) {
536+
var componentOp = {op: shatteredOps[i]};
531537
// Transform componentOp against any ops that have been submitted
532538
// sychronously inside of an op event handler since we began apply of
533539
// our operation
@@ -536,26 +542,27 @@ Doc.prototype._otApply = function(op, source) {
536542
if (transformErr) return this._hardRollback(transformErr);
537543
}
538544
// Apply the individual op component
539-
this.emit('before op', componentOp.op, source);
545+
this.emit('before component', componentOp.op, source);
540546
this.data = this.type.apply(this.data, componentOp.op);
541-
this.emit('op', componentOp.op, source);
547+
this.emit('after component', componentOp.op, source);
542548
}
543549
// Pop whatever was submitted since we started applying this op
544550
this._popApplyStack(stackLength);
545-
return;
546551
}
547552

548-
// The 'before op' event enables clients to pull any necessary data out of
549-
// the snapshot before it gets changed
550-
this.emit('before op', op.op, source);
551-
// Apply the operation to the local data, mutating it in place
552-
this.data = this.type.apply(this.data, op.op);
553-
// Emit an 'op' event once the local data includes the changes from the
553+
// Apply the full operation to the local data, mutating it in place
554+
else {
555+
this.emit('before component', op.op, source);
556+
this.data = this.type.apply(this.data, op.op);
557+
this.emit('after component', op.op, source);
558+
}
559+
560+
// Emit an 'after op' event once the local data includes the changes from the
554561
// op. For locally submitted ops, this will be synchronously with
555562
// submission and before the server or other clients have received the op.
556563
// For ops from other clients, this will be after the op has been
557564
// committed to the database and published
558-
this.emit('op', op.op, source);
565+
this.emit('after op', op.op, source);
559566
return;
560567
}
561568

test/client/doc.js

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('client query subscribe', function() {
6767
});
6868
}
6969

70-
it('single component ops emit an `op` event', function(done) {
70+
it('single component ops emit an `after component` event', function(done) {
7171
var doc = this.doc;
7272
var doc2 = this.doc2;
7373
var doc3 = this.doc3;
@@ -92,7 +92,7 @@ describe('client query subscribe', function() {
9292
expect(doc.data).eql({color: 'black'});
9393
}
9494
];
95-
doc.on('op', function(op, source) {
95+
doc.on('after component', function(op, source) {
9696
var handler = handlers.shift();
9797
handler(op, source);
9898
});
@@ -104,7 +104,7 @@ describe('client query subscribe', function() {
104104
});
105105
});
106106

107-
it('remote multi component ops emit individual `op` events', function(done) {
107+
it('remote multi component ops emit multiple `after component` events', function(done) {
108108
var doc = this.doc;
109109
var doc2 = this.doc2;
110110
var doc3 = this.doc3;
@@ -141,7 +141,7 @@ describe('client query subscribe', function() {
141141
expect(doc.data).eql({color: 'black', weight: 40, age: 5, owner: 'sue'});
142142
}
143143
];
144-
doc.on('op', function(op, source) {
144+
doc.on('after component', function(op, source) {
145145
var handler = handlers.shift();
146146
handler(op, source);
147147
});
@@ -153,7 +153,7 @@ describe('client query subscribe', function() {
153153
});
154154
});
155155

156-
it('remote multi component ops are transformed by ops submitted in `op` event handlers', function(done) {
156+
it('remote multi component ops are transformed by ops submitted in `after component` event handlers', function(done) {
157157
var doc = this.doc;
158158
var doc2 = this.doc2;
159159
var doc3 = this.doc3;
@@ -192,7 +192,7 @@ describe('client query subscribe', function() {
192192
expect(doc.data).eql({tricks: ['shake', 'tug stick']});
193193
}
194194
];
195-
doc.on('op', function(op, source) {
195+
doc.on('after component', function(op, source) {
196196
var handler = handlers.shift();
197197
handler(op, source);
198198
});
@@ -210,6 +210,85 @@ describe('client query subscribe', function() {
210210
});
211211
});
212212

213+
214+
it('ops emit all lifecycle events', function(done) {
215+
var doc = this.doc;
216+
var doc2 = this.doc2;
217+
var doc3 = this.doc3;
218+
var handlers = [
219+
// doc submit before op
220+
function(op, source) {
221+
expect(source).equal(true);
222+
expect(op).eql([{p: ['make'], oi: 'bmw'}, {p: ['speed'], oi: 160}]);
223+
expect(doc.data).eql({});
224+
// doc submit before component 1
225+
}, function(op, source) {
226+
expect(source).equal(true);
227+
expect(op).eql([{p: ['make'], oi: 'bmw'}]);
228+
expect(doc.data).eql({});
229+
// doc submit after component 1
230+
}, function(op, source) {
231+
expect(source).equal(true);
232+
expect(op).eql([{p: ['make'], oi: 'bmw'}]);
233+
expect(doc.data).eql({make: 'bmw'});
234+
// doc submit before component 2
235+
}, function(op, source) {
236+
expect(source).equal(true);
237+
expect(op).eql([{p: ['speed'], oi: 160}]);
238+
expect(doc.data).eql({make: 'bmw'});
239+
// doc submit after component 2
240+
}, function(op, source) {
241+
expect(source).equal(true);
242+
expect(op).eql([{p: ['speed'], oi: 160}]);
243+
expect(doc.data).eql({make: 'bmw', speed: 160});
244+
// doc submit after op
245+
}, function(op, source) {
246+
expect(source).equal(true);
247+
expect(op).eql([{p: ['make'], oi: 'bmw'}, {p: ['speed'], oi: 160}]);
248+
expect(doc.data).eql({make: 'bmw', speed: 160});
249+
// doc2 submit before op
250+
}, function(op, source) {
251+
expect(source).equal(false);
252+
expect(op).eql([{p: ['model'], oi: '260e'}]);
253+
expect(doc.data).eql({make: 'bmw', speed: 160});
254+
// doc2 submit before component 1
255+
}, function(op, source) {
256+
expect(source).equal(false);
257+
expect(op).eql([{p: ['model'], oi: '260e'}]);
258+
expect(doc.data).eql({make: 'bmw', speed: 160});
259+
// doc2 submit after component 1
260+
}, function(op, source) {
261+
expect(source).equal(false);
262+
expect(op).eql([{p: ['model'], oi: '260e'}]);
263+
expect(doc.data).eql({make: 'bmw', model: '260e', speed: 160});
264+
// doc2 submit after op
265+
}, function(op, source) {
266+
expect(source).equal(false);
267+
expect(op).eql([{p: ['model'], oi: '260e'}]);
268+
expect(doc.data).eql({make: 'bmw', model: '260e', speed: 160});
269+
}
270+
];
271+
272+
doc.applyLocalOpsIncremental = true;
273+
doc.applyRemoteOpsIncremental = true;
274+
275+
var handleOpEvent = function(op, source) {
276+
var handler = handlers.shift();
277+
handler(op, source);
278+
};
279+
doc.on('before op', handleOpEvent);
280+
doc.on('before component', handleOpEvent);
281+
doc.on('after component', handleOpEvent);
282+
doc.on('after op', handleOpEvent);
283+
284+
doc2.submitOp([{p: ['make'], oi: 'mercedes'}, {p: ['model'], oi: '260e'}], function(err) {
285+
if (err) return done(err);
286+
doc.submitOp([{p: ['make'], oi: 'bmw'}, {p: ['speed'], oi: 160}]);
287+
expect(doc.data).eql({make: 'bmw', speed: 160});
288+
verifyConsistency(doc, doc2, doc3, handlers, done);
289+
});
290+
});
291+
213292
});
214293

215294
});

test/client/projections.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe('client projections', function() {
115115
var fido = connection2.get('dogs_summary', 'fido');
116116
fido.subscribe(function(err) {
117117
if (err) return done(err);
118-
fido.on('op', function() {
118+
fido.on('after op', function() {
119119
expect(fido.data).eql(expected);
120120
expect(fido.version).eql(2);
121121
done();
@@ -154,7 +154,7 @@ describe('client projections', function() {
154154
var fido = connection2.get('dogs_summary', 'fido');
155155
connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
156156
if (err) return done(err);
157-
fido.on('op', function() {
157+
fido.on('after op', function() {
158158
expect(fido.data).eql(expected);
159159
expect(fido.version).eql(2);
160160
done();

test/client/subscribe.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe('client subscribe', function() {
174174
if (err) return done(err);
175175
doc.submitOp({p: ['age'], na: 1}, function(err) {
176176
if (err) return done(err);
177-
doc2.on('op', function(op, context) {
177+
doc2.on('after op', function(op, context) {
178178
done();
179179
});
180180
doc2[method]();
@@ -343,7 +343,7 @@ describe('client subscribe', function() {
343343
if (err) return done(err);
344344
doc2.subscribe(function(err) {
345345
if (err) return done(err);
346-
doc2.on('op', function(op, context) {
346+
doc2.on('after op', function(op, context) {
347347
expect(doc2.version).eql(2);
348348
expect(doc2.data).eql({age: 4});
349349
done();
@@ -360,7 +360,7 @@ describe('client subscribe', function() {
360360
if (err) return done(err);
361361
doc2.subscribe(function(err) {
362362
if (err) return done(err);
363-
doc2.on('op', function(op, context) {
363+
doc2.on('after op', function(op, context) {
364364
done();
365365
});
366366
doc2.connection.close();
@@ -377,7 +377,7 @@ describe('client subscribe', function() {
377377
if (err) return done(err);
378378
doc2.subscribe(function(err) {
379379
if (err) return done(err);
380-
doc2.on('op', function(op, context) {
380+
doc2.on('after op', function(op, context) {
381381
done();
382382
});
383383
backend.suppressPublish = true;
@@ -393,7 +393,7 @@ describe('client subscribe', function() {
393393
if (err) return done(err);
394394
doc2.subscribe(function(err) {
395395
if (err) return done(err);
396-
doc2.on('op', function(op, context) {
396+
doc2.on('after op', function(op, context) {
397397
done();
398398
});
399399
doc2.unsubscribe(function(err) {
@@ -411,7 +411,7 @@ describe('client subscribe', function() {
411411
if (err) return done(err);
412412
doc2.subscribe(function(err) {
413413
if (err) return done(err);
414-
doc2.on('op', function(op, context) {
414+
doc2.on('after op', function(op, context) {
415415
done();
416416
});
417417
doc2.destroy(function(err) {
@@ -441,7 +441,7 @@ describe('client subscribe', function() {
441441
function(cb) { spot.unsubscribe(cb); }
442442
], function(err) {
443443
if (err) return done(err);
444-
fido.on('op', function(op, context) {
444+
fido.on('after op', function(op, context) {
445445
done();
446446
});
447447
doc.submitOp({p: ['age'], na: 1}, done);
@@ -459,7 +459,7 @@ describe('client subscribe', function() {
459459
if (err) return done(err);
460460
doc2.subscribe(function(err) {
461461
if (err) return done(err);
462-
doc2.on('op', function(op, context) {
462+
doc2.on('after op', function(op, context) {
463463
expect(doc2.version).eql(2);
464464
expect(doc2.data).eql({age: 4});
465465
done();
@@ -483,7 +483,7 @@ describe('client subscribe', function() {
483483
doc2.unsubscribe();
484484
doc2.subscribe(function(err) {
485485
if (err) return done(err);
486-
doc2.on('op', function(op, context) {
486+
doc2.on('after op', function(op, context) {
487487
done();
488488
});
489489
doc.submitOp({p: ['age'], na: 1});
@@ -503,7 +503,7 @@ describe('client subscribe', function() {
503503
[{p: ['age'], na: 1}],
504504
[{p: ['age'], na: 5}],
505505
];
506-
doc2.on('op', function(op, context) {
506+
doc2.on('after op', function(op, context) {
507507
var item = expected.shift();
508508
expect(op).eql(item);
509509
if (expected.length) return;
@@ -535,7 +535,7 @@ describe('client subscribe', function() {
535535
doc2.subscribe(function(err) {
536536
if (err) return done(err);
537537
var wait = 4;
538-
doc2.on('op', function(op, context) {
538+
doc2.on('after op', function(op, context) {
539539
if (--wait) return;
540540
expect(doc2.version).eql(5);
541541
expect(doc2.data).eql({age: 122});

0 commit comments

Comments
 (0)